From e72e995c26ee93ff8979e325d22892501895fad0 Mon Sep 17 00:00:00 2001 From: Rudra-clrscr Date: Tue, 14 Jul 2026 01:19:04 +0530 Subject: [PATCH 1/8] fix: resolve all pre-existing backend eslint errors and add eslint config Adds backend/eslint.config.js (none existed before) and fixes every error it surfaces except middleware/fileValidation.js (separately tracked, in progress). Several of these were one-off typos or single duplicate lines; a few turned out to be much larger merge-corruption issues. Small fixes: - config/index.js: `configs.development` -> `config.development` typo. - utils/emailRules.js: `matchingRule.type` -> `matchedType` (the actual variable holding the matched rule's type in that scope). - utils/fileValidation.js: drop an unnecessary regex escape. - utils/utils/emailTransporter.js -> utils/emailTransporter.js: fix a duplicated-directory path bug (authController.js requires '../utils/emailTransporter', which never existed at that path). - server.js: fix an empty catch block (log the error instead of silently swallowing it) and remove a require for './routes/visualRoutes', a file that never existed anywhere in the repo (dead "VBSF routes" stub, not implemented on the Node side). Larger fixes (duplicate/corrupted code from bad merges): - middleware/adversarialGuard.js: removed a duplicated `detectAdversarialPatterns` call, a duplicated `adversarialGuard` function stub, a duplicated outer `if` in `monitorConfidence`, and two `catch` clauses attached to the same `try` - all leftover copies with a working original alongside them. - server.js: removed a broken, never-closed `logStartupTime` stub and its duplicated EvoMail/startup-timer requires that preceded the real, complete versions; moved the EvoMail/visual route `app.use()` calls to after `const app = express()` is defined (they previously ran before `app` existed, which would throw at runtime once the file parsed). - routes/authRoutes.js: this file had ~11 controller functions (register, login, logout, getMe, googleLogin, updateAvatar, forgotPassword, resetPassword, changePassword, updateWebhook, getSessionStatus) each duplicated 2-3x (verified byte-identical copies), plus assignRole/getUserPermissions/getRolesAndPermissions whose bodies were corrupted/incomplete in every copy. More importantly, despite creating an Express Router, the file never once called router.get/post() - it only exported bare handler functions - while server.js did `app.use("/api/v1/auth", authRoutes)`, passing a plain object where Express requires a router/middleware function. The whole auth API was not actually routed. Rewrote the file as a thin router that imports the (already complete, working) handlers from controllers/authController.js and wires each one to its documented `@route` (recovered from the file's own JSDoc comments) with the appropriate validator/rate-limiter/protect middleware, then exports the router. Deleted routes/prediction.route.js, an orphaned, never- required duplicate of predictionRoutes.js's /stats route that referenced a `Prediction` model that doesn't exist anywhere in the codebase; fixed predictionRoutes.js's own /stats route (same bug) to use the History model it already imports. Verified: `node --check` on every touched file, `npx eslint .` (1 remaining error is middleware/fileValidation.js, tracked separately), a full server.js boot with dummy env vars (previously impossible - it couldn't get past its first require), and the existing Jest (16/16) and node:test (26/26) suites still pass. Left as pre-existing, separately-tracked issues (not touched here): - middleware/fileValidation.js's own parsing error (your in-progress work). - services/otpService.js exports rate limiters instead of OTP functions - unrelated corruption discovered while tracing authRoutes.js's dead requestOTP/verifyOTP/getOTPStatus imports (no working implementation of those three exists anywhere, so they were dropped from the new router rather than inventing the feature). - 63 remaining eslint warnings (mostly unused vars/imports across many files) left non-blocking, consistent with the frontend cleanup PR. --- backend/config/index.js | 2 +- backend/eslint.config.js | 25 + backend/middleware/adversarialGuard.js | 24 +- backend/package-lock.json | 605 +++++++ backend/package.json | 6 +- backend/routes/authRoutes.js | 1486 +---------------- backend/routes/prediction.route.js | 41 - backend/routes/predictionRoutes.js | 19 +- backend/server.js | 16 +- backend/utils/emailRules.js | 2 +- backend/utils/{utils => }/emailTransporter.js | 0 backend/utils/fileValidation.js | 2 +- 12 files changed, 679 insertions(+), 1549 deletions(-) create mode 100644 backend/eslint.config.js delete mode 100644 backend/routes/prediction.route.js rename backend/utils/{utils => }/emailTransporter.js (100%) diff --git a/backend/config/index.js b/backend/config/index.js index 1146ddd2..3a080c1d 100644 --- a/backend/config/index.js +++ b/backend/config/index.js @@ -10,4 +10,4 @@ const config = { const env = process.env.NODE_ENV || 'development'; -module.exports = config[env] || configs.development; \ No newline at end of file +module.exports = config[env] || config.development; \ No newline at end of file diff --git a/backend/eslint.config.js b/backend/eslint.config.js new file mode 100644 index 00000000..2613713c --- /dev/null +++ b/backend/eslint.config.js @@ -0,0 +1,25 @@ +const js = require('@eslint/js'); +const globals = require('globals'); + +module.exports = [ + js.configs.recommended, + { + languageOptions: { + ecmaVersion: 2022, + sourceType: 'commonjs', + globals: { + ...globals.node, + ...globals.jest, + // Set on `global` by server.js's Sentry setup before route modules + // are required; real at runtime, just invisible to static analysis. + Sentry: 'readonly', + }, + }, + rules: { + 'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + }, + }, + { + ignores: ['node_modules/**', 'output/**', '**/__pycache__/**'], + }, +]; diff --git a/backend/middleware/adversarialGuard.js b/backend/middleware/adversarialGuard.js index a7d1950f..216301cf 100644 --- a/backend/middleware/adversarialGuard.js +++ b/backend/middleware/adversarialGuard.js @@ -61,12 +61,9 @@ function detectAdversarialPatterns(text) { } } - const normalizedScore = Math.min(score, 1.0); - - // Normalize score const normalizedScore = Math.min(score, 1.0); - + // Check for extremely long text (potential DoS) if (text.length > 10000) { @@ -86,9 +83,6 @@ function detectAdversarialPatterns(text) { }; } -function adversarialGuard(req, res, next) { - const text = req.body?.text || req.query?.text || ''; - /** * Middleware to check for adversarial patterns */ @@ -102,10 +96,6 @@ function adversarialGuard(req, res, next) { return next(); } - const analysis = detectAdversarialPatterns(text); - req.adversarialAnalysis = analysis; - - // Check for adversarial patterns const analysis = detectAdversarialPatterns(text); req.adversarialAnalysis = analysis; @@ -145,14 +135,10 @@ function monitorConfidence(req, res, next) { responseData = data; } - if (responseData && typeof responseData === 'object') { - const confidence = responseData.confidence_score || responseData.confidence || 0; - - // Check if it's a prediction response if (responseData && typeof responseData === 'object') { const confidence = responseData.confidence_score || responseData.confidence || 0; - + // Add adversarial analysis if (req.adversarialAnalysis) { @@ -173,11 +159,6 @@ function monitorConfidence(req, res, next) { console.log(`⚠️ [CONFIDENCE MONITOR] Low confidence prediction:`, { confidence, threshold: CONFIDENCE_THRESHOLD, - prediction: responseData.result || responseData.prediction - }); - } - - prediction: responseData.result || responseData.prediction, textPreview: req.body?.text?.substring(0, 100) }); @@ -195,7 +176,6 @@ function monitorConfidence(req, res, next) { originalSend.call(this, jsonData); return; } - } catch (e) {} } catch (e) { // If parsing fails, just pass through } diff --git a/backend/package-lock.json b/backend/package-lock.json index 795687d7..6426fb31 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -45,6 +45,9 @@ "uuid": "^14.0.1" }, "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.7.0", + "globals": "^17.7.0", "jest": "^30.4.2", "supertest": "^7.2.2" } @@ -717,6 +720,200 @@ "tslib": "^2.4.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -2352,6 +2549,13 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -2814,6 +3018,16 @@ "acorn": "^8" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/after": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/after/-/after-0.8.1.tgz", @@ -4066,6 +4280,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -4478,6 +4699,214 @@ "node": ">=8" } }, + "node_modules/eslint": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -4504,6 +4933,19 @@ "node": ">=0.10" } }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -4700,6 +5142,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -4780,6 +5229,19 @@ "node": ">=20" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4842,6 +5304,27 @@ "integrity": "sha512-1q/3kz+ZwmrrWpJcCCrBZ3JnBzB1BMA5EVW9nxnIP1LxDZ16Cqs9VdolqLWlExet1vU+bar3WSkAa4/YrA9bIw==", "license": "MIT" }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -5185,6 +5668,19 @@ "integrity": "sha512-O91OcV/NbdmQJPHaRu2ekSP7bqFRLWgqSwaJvqHPZHUwmHBagQYTOra29+LnzzG3lZkXH1ANzHzfCxtAPM9HMA==", "license": "MIT" }, + "node_modules/globals": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/google-auth-library": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", @@ -5433,6 +5929,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -6441,6 +6947,13 @@ "bignumber.js": "^9.0.0" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -6454,6 +6967,13 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-3.0.0.tgz", @@ -6579,6 +7099,16 @@ "node": ">=18.0.0" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -6589,6 +7119,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -7445,6 +7989,24 @@ "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", "license": "BSD-2-Clause" }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/options": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", @@ -7742,6 +8304,16 @@ "browserify-zlib": "^0.2.0" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-format": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", @@ -9467,6 +10039,19 @@ "node": "*" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -9633,6 +10218,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/utf8": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.0.0.tgz", @@ -9785,6 +10380,16 @@ "node": ">= 0.6.0" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", diff --git a/backend/package.json b/backend/package.json index 7d14cde9..c7f674d2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,8 @@ "scripts": { "start": "node server.js", "dev": "nodemon server.js", - "test": "jest --testPathIgnorePatterns config --testPathIgnorePatterns avatarUpload --testPathIgnorePatterns keywordRules --testPathIgnorePatterns rateLimiter --testPathIgnorePatterns fileValidation && node --test tests/keywordRules.test.js tests/rateLimiter.test.js tests/avatarUpload.test.js tests/fileValidation.test.js" + "test": "jest --testPathIgnorePatterns config --testPathIgnorePatterns avatarUpload --testPathIgnorePatterns keywordRules --testPathIgnorePatterns rateLimiter --testPathIgnorePatterns fileValidation && node --test tests/keywordRules.test.js tests/rateLimiter.test.js tests/avatarUpload.test.js tests/fileValidation.test.js", + "lint": "eslint ." }, "keywords": [], "author": "", @@ -49,6 +50,9 @@ "uuid": "^14.0.1" }, "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.7.0", + "globals": "^17.7.0", "jest": "^30.4.2", "supertest": "^7.2.2" } diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js index aaf1de6b..c898b2af 100644 --- a/backend/routes/authRoutes.js +++ b/backend/routes/authRoutes.js @@ -1,15 +1,8 @@ - -const jwt = require('jsonwebtoken'); -const nodemailer = require('nodemailer'); -const { validationResult } = require('express-validator'); -const { OAuth2Client } = require('google-auth-library'); -const User = require('../models/User'); -const fs = require('fs'); - const express = require('express'); const router = express.Router(); +const multer = require('multer'); +const upload = multer(); -const { body } = require('express-validator'); const { register, login, @@ -19,992 +12,72 @@ const { updateAvatar, forgotPassword, resetPassword, - updateWebhook, - requestOTP, - verifyOTP, - getOTPStatus, changePassword, - getSessionStatus + updateWebhook, + getSessionStatus, + assignRole, + getUserPermissions, + getRolesAndPermissions, } = require('../controllers/authController'); const { registerValidation, loginValidation, forgotPasswordValidation, - resetPasswordValidation + resetPasswordValidation, } = require('../validators/auth.validator'); const { protect } = require('../middleware/authMiddleware'); -const { - registerLimiter, - loginLimiter, +const { + registerLimiter, + loginLimiter, resetLimiter, - otpLimiter, - verificationLimiter, - apiLimiter + apiLimiter, } = require('../middleware/rateLimiter'); -const multer = require('multer'); - -const path = require('path'); -const sharp = require('sharp'); -const BlacklistedToken = require('../models/BlacklistedToken'); -const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID); - -// ============================================ -// TOKEN GENERATION -// ============================================ - -const generateToken = (userId) => { - return jwt.sign({ id: userId }, process.env.JWT_SECRET, { - expiresIn: process.env.JWT_EXPIRES_IN || '7d', - }); -}; - -const buildAuthResponse = (user, token) => ({ - token, - user: { - id: user._id, - username: user.username, - email: user.email, - avatarUrl: user.avatarUrl, - provider: user.provider, - role: user.role || 'user', - permissions: user.permissions || [], - }, -}); - - -// ============================================ -// TOKEN GENERATION -// ============================================ - -const generateToken = (userId) => { - return jwt.sign({ id: userId }, process.env.JWT_SECRET, { - expiresIn: process.env.JWT_EXPIRES_IN || '7d', - }); -}; - -const buildAuthResponse = (user, token) => ({ - token, - user: { - id: user._id, - username: user.username, - email: user.email, - avatarUrl: user.avatarUrl, - provider: user.provider, - role: user.role || 'user', - permissions: user.permissions || [], - }, -}); - - -// ============================================ -// AUTH CONTROLLERS -// ============================================ // @desc Register user // @route POST /api/auth/register -const register = async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ error: errors.array()[0].msg }); - } - - const { username, email, password } = req.body; - - const existingUser = await User.findOne({ $or: [{ email }, { username }] }); - if (existingUser) { - const field = existingUser.email === email ? 'Email' : 'Username'; - return res.status(400).json({ error: `${field} is already in use.` }); - } - - const user = await User.create({ username, email, password }); - const token = generateToken(user._id); - - res.status(201).json({ - message: 'Account created successfully!', - ...buildAuthResponse(user, token), - }); - } catch (err) { - console.error('Register error:', err); - if (err.code === 11000) { - const field = err.keyPattern?.email ? 'Email' : 'Username'; - return res.status(400).json({ error: `${field} is already in use.` }); - } - res.status(500).json({ error: 'Server error. Please try again.' }); - } -}; +router.post('/register', registerLimiter, registerValidation, register); // @desc Login user // @route POST /api/auth/login -const login = async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ error: errors.array()[0].msg }); - } - - const { email, password } = req.body; - - const user = await User.findOne({ email }); - if (!user) { - return res.status(401).json({ error: 'Invalid email or password.' }); - } - - // Check if account is locked - if (user.isLocked && user.isLocked()) { - return res.status(429).json({ - success: false, - error: 'Account locked due to too many failed attempts. Please try again later.' - }); - } - - const isMatch = await user.comparePassword(password); - if (!isMatch) { - // Increment login attempts - if (user.incrementLoginAttempts) { - await user.incrementLoginAttempts(); - } - return res.status(401).json({ error: 'Invalid email or password.' }); - } - - // Reset login attempts on success - if (user.updateLastLogin) { - await user.updateLastLogin(); - } - - const token = generateToken(user._id); - - res.json({ - message: 'Login successful!', - ...buildAuthResponse(user, token), - }); - } catch (err) { - console.error('Login error:', err); - res.status(500).json({ error: 'Server error. Please try again.' }); - } -}; +router.post('/login', loginLimiter, loginValidation, login); // @desc Logout user - Blacklist token // @route POST /api/auth/logout -const logout = async (req, res) => { - try { - const token = req.token; - - if (!token) { - return res.status(400).json({ - success: false, - error: 'No token provided for logout.' - }); - } - - await BlacklistedToken.blacklist( - token, - req.user._id, - 'LOGOUT', - req.ip || req.connection?.remoteAddress, - req.headers['user-agent'] - ); - - res.json({ - success: true, - message: 'Successfully logged out. Token revoked.' - }); - } catch (err) { - console.error('Logout error:', err); - res.status(500).json({ - success: false, - error: 'Server error during logout.' - }); - } -}; +router.post('/logout', protect, logout); // @desc Get current user // @route GET /api/auth/me -const getMe = async (req, res) => { - try { - const user = await User.findById(req.user.id).select('-password'); - if (!user) return res.status(404).json({ error: 'User not found.' }); - res.json({ user }); - } catch (err) { - res.status(500).json({ error: 'Server error.' }); - } -}; +router.get('/me', protect, getMe); // @desc Google OAuth login // @route POST /api/auth/google -const googleLogin = async (req, res) => { - try { - const { idToken } = req.body; - if (!idToken) { - return res.status(400).json({ error: 'Google ID Token is required.' }); - } - - const ticket = await client.verifyIdToken({ - idToken: idToken, - audience: process.env.GOOGLE_CLIENT_ID, - }); - - const payload = ticket.getPayload(); - const { sub: googleId, email, name, picture } = payload; - - let user = await User.findOne({ email }); - - if (user) { - if (!user.googleId) { - user.googleId = googleId; - user.provider = 'google'; - if (picture && !user.avatarUrl) { - user.avatarUrl = picture; - } - await user.save(); - } - } else { - let baseUsername = name ? name.replace(/\s+/g, '').toLowerCase() : email.split('@')[0]; - let username = baseUsername; - - const regex = new RegExp(`^${baseUsername}(\\d*)$`); - const existingUsers = await User.find({ username: regex }).select('username').lean(); - - if (existingUsers.length > 0) { - const exactMatch = existingUsers.find(u => u.username === baseUsername); - if (exactMatch) { - let maxCounter = 0; - existingUsers.forEach(u => { - const match = u.username.match(regex); - if (match && match[1]) { - const num = parseInt(match[1]); - if (num > maxCounter) maxCounter = num; - } - }); - username = `${baseUsername}${maxCounter + 1}`; - } - } - - user = await User.create({ - username, - email, - googleId, - avatarUrl: picture, - provider: 'google', - }); - } - - const token = generateToken(user._id); - - res.json({ - message: 'Login successful!', - ...buildAuthResponse(user, token), - }); - } catch (err) { - console.error('Google Auth Error:', err); - res.status(400).json({ error: 'Invalid Google token.' }); - } -}; - -// @desc Update user avatar -// @route POST /api/auth/avatar -const updateAvatar = async (req, res) => { - try { - if (!req.file) { - return res.status(400).json({ error: 'No file uploaded' }); - } - - const filename = `${req.user.id}-${Date.now()}.webp`; - const filepath = path.join(__dirname, '..', 'uploads', filename); - - await sharp(req.file.buffer) - .resize(250, 250, { fit: 'cover' }) - .toFormat('webp') - .toFile(filepath); - - const avatarUrl = `${req.protocol}://${req.get('host')}/uploads/${filename}`; - - const currentUser = await User.findById(req.user.id); - if (currentUser && currentUser.avatarUrl && currentUser.avatarUrl.includes('/uploads/')) { - try { - const oldFilename = currentUser.avatarUrl.split('/uploads/')[1]; - const oldFilePath = path.join(__dirname, '..', 'uploads', oldFilename); - await fs.promises.access(oldFilePath); - await fs.promises.unlink(oldFilePath); - } catch (err) { - if (err.code !== 'ENOENT') { - console.error('Failed to delete old avatar:', err); - } - } - } - - const user = await User.findByIdAndUpdate( - req.user.id, - { avatarUrl }, - { new: true } - ).select('-password'); - - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } - - res.json({ message: 'Avatar updated successfully', user }); - } catch (err) { - console.error('Avatar upload error:', err); - res.status(500).json({ error: 'Server error. Please try again.' }); - } -}; - -// @desc Forgot password - Send reset link -// @route POST /api/auth/forgot-password -const forgotPassword = async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ error: errors.array()[0].msg }); - } - - const { email } = req.body; - const user = await User.findOne({ email }); - if (!user) { - return res.json({ message: 'If an account with that email exists, a reset link has been sent.' }); - } - - const secret = process.env.JWT_SECRET + user.password; - const token = jwt.sign( - { id: user._id, email: user.email }, - secret, - { expiresIn: process.env.PASSWORD_RESET_TOKEN_EXPIRES || '15m' } - ); - - const clientUrl = process.env.CLIENT_URL || 'http://localhost:3000'; - const resetLink = `${clientUrl}/reset-password/${user._id}/${token}`; - - const transporter = nodemailer.createTransport({ - host: process.env.EMAIL_HOST || 'smtp.ethereal.email', - port: process.env.EMAIL_PORT || 587, - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS, - }, - }); - - const emailFrom = process.env.EMAIL_FROM || '"Spam Detection System" '; - - if (process.env.EMAIL_USER && process.env.EMAIL_PASS) { - await transporter.sendMail({ - from: emailFrom, - to: user.email, - subject: 'Password Reset Request', - text: `Please use the following link to reset your password: ${resetLink} \n\nThis link expires in 15 minutes.`, - }); - } else { - console.log(`[DEMO] Password Reset Link for ${user.email}: ${resetLink}`); - } - - res.json({ message: 'If an account with that email exists, a reset link has been sent.' }); - } catch (err) { - console.error('Forgot password error:', err); - res.status(500).json({ error: 'Server error. Please try again later.' }); - } -}; - -// @desc Reset password -// @route POST /api/auth/reset-password/:id/:token -const resetPassword = async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ error: errors.array()[0].msg }); - } - - const { id, token } = req.params; - const { password } = req.body; - - const user = await User.findById(id); - if (!user) { - return res.status(400).json({ error: 'Invalid or expired token.' }); - } - - const secret = process.env.JWT_SECRET + user.password; - try { - jwt.verify(token, secret); - } catch (err) { - return res.status(400).json({ error: 'Invalid or expired token.' }); - } - - user.password = password; - await user.save(); - - res.json({ message: 'Password has been successfully reset. You can now login.' }); - } catch (err) { - console.error('Reset password error:', err); - res.status(500).json({ error: 'Server error. Please try again later.' }); - } -}; - -// @desc Change password (authenticated) -// @route POST /api/auth/change-password -const changePassword = async (req, res) => { - try { - const { currentPassword, newPassword } = req.body; - - if (!currentPassword || !newPassword) { - return res.status(400).json({ - success: false, - error: 'Current password and new password are required.' - }); - } - - if (newPassword.length < 6) { - return res.status(400).json({ - success: false, - error: 'New password must be at least 6 characters.' - }); - } - - const user = await User.findById(req.user.id).select('+password'); - if (!user) { - return res.status(404).json({ - success: false, - error: 'User not found.' - }); - } - - const isMatch = await user.comparePassword(currentPassword); - if (!isMatch) { - return res.status(401).json({ - success: false, - error: 'Current password is incorrect.' - }); - } - - user.password = newPassword; - await user.save(); - - await BlacklistedToken.invalidateAllUserTokens( - user._id, - 'PASSWORD_CHANGE', - req.token, - req.ip || req.connection?.remoteAddress, - req.headers['user-agent'] - ); - - res.json({ - success: true, - message: 'Password changed successfully. All sessions invalidated. Please login again.' - }); - } catch (err) { - console.error('Change password error:', err); - res.status(500).json({ - success: false, - error: 'Server error. Please try again later.' - }); - } -}; - -// @desc Update webhook URL -// @route PUT /api/auth/webhook -const updateWebhook = async (req, res) => { - try { - const { webhookUrl } = req.body; - - const newWebhookValue = (webhookUrl && webhookUrl.trim() !== '') ? webhookUrl.trim() : null; - - const user = await User.findByIdAndUpdate( - req.user.id, - { webhookUrl: newWebhookValue }, - { new: true, runValidators: true } - ).select('-password'); - - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } - - res.json({ message: 'Webhook URL updated successfully!', user }); - } catch (err) { - console.error('Webhook update error:', err); - if (err.name === 'ValidationError') { - return res.status(400).json({ error: 'Invalid Webhook URL format. Must start with http:// or https://' }); - } - res.status(500).json({ error: 'Server error. Please try again.' }); - } -}; - -// @desc Get user's session status -// @route GET /api/auth/session-status -const getSessionStatus = async (req, res) => { - try { - const token = req.token; - const decoded = jwt.decode(token); - - const now = Math.floor(Date.now() / 1000); - const timeUntilExpiry = decoded.exp - now; - - res.json({ - success: true, - expiresIn: timeUntilExpiry, - expiresAt: new Date(decoded.exp * 1000), - isExpiringSoon: timeUntilExpiry < 300 - }); - } catch (err) { - res.status(500).json({ - success: false, - error: 'Failed to get session status' - }); - } -}; - -// ============================================ -// AUTH CONTROLLERS -// ============================================ - -// @desc Register user -// @route POST /api/auth/register -const register = async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ error: errors.array()[0].msg }); - } - - const { username, email, password } = req.body; - - const existingUser = await User.findOne({ $or: [{ email }, { username }] }); - if (existingUser) { - const field = existingUser.email === email ? 'Email' : 'Username'; - return res.status(400).json({ error: `${field} is already in use.` }); - } - - const user = await User.create({ username, email, password }); - const token = generateToken(user._id); - - res.status(201).json({ - message: 'Account created successfully!', - ...buildAuthResponse(user, token), - }); - } catch (err) { - console.error('Register error:', err); - if (err.code === 11000) { - const field = err.keyPattern?.email ? 'Email' : 'Username'; - return res.status(400).json({ error: `${field} is already in use.` }); - } - res.status(500).json({ error: 'Server error. Please try again.' }); - } -}; - -// @desc Login user -// @route POST /api/auth/login -const login = async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ error: errors.array()[0].msg }); - } - - const { email, password } = req.body; - - const user = await User.findOne({ email }); - if (!user) { - return res.status(401).json({ error: 'Invalid email or password.' }); - } - - // Check if account is locked - if (user.isLocked && user.isLocked()) { - return res.status(429).json({ - success: false, - error: 'Account locked due to too many failed attempts. Please try again later.' - }); - } - - const isMatch = await user.comparePassword(password); - if (!isMatch) { - // Increment login attempts - if (user.incrementLoginAttempts) { - await user.incrementLoginAttempts(); - } - return res.status(401).json({ error: 'Invalid email or password.' }); - } - - // Reset login attempts on success - if (user.updateLastLogin) { - await user.updateLastLogin(); - } - - const token = generateToken(user._id); - - res.json({ - message: 'Login successful!', - ...buildAuthResponse(user, token), - }); - } catch (err) { - console.error('Login error:', err); - res.status(500).json({ error: 'Server error. Please try again.' }); - } -}; - -// @desc Logout user - Blacklist token -// @route POST /api/auth/logout -const logout = async (req, res) => { - try { - const token = req.token; - - if (!token) { - return res.status(400).json({ - success: false, - error: 'No token provided for logout.' - }); - } - - await BlacklistedToken.blacklist( - token, - req.user._id, - 'LOGOUT', - req.ip || req.connection?.remoteAddress, - req.headers['user-agent'] - ); - - res.json({ - success: true, - message: 'Successfully logged out. Token revoked.' - }); - } catch (err) { - console.error('Logout error:', err); - res.status(500).json({ - success: false, - error: 'Server error during logout.' - }); - } -}; - -// @desc Get current user -// @route GET /api/auth/me -const getMe = async (req, res) => { - try { - const user = await User.findById(req.user.id).select('-password'); - if (!user) return res.status(404).json({ error: 'User not found.' }); - res.json({ user }); - } catch (err) { - res.status(500).json({ error: 'Server error.' }); - } -}; - -// @desc Google OAuth login -// @route POST /api/auth/google -const googleLogin = async (req, res) => { - try { - const { idToken } = req.body; - if (!idToken) { - return res.status(400).json({ error: 'Google ID Token is required.' }); - } - - const ticket = await client.verifyIdToken({ - idToken: idToken, - audience: process.env.GOOGLE_CLIENT_ID, - }); - - const payload = ticket.getPayload(); - const { sub: googleId, email, name, picture } = payload; - - let user = await User.findOne({ email }); - - if (user) { - if (!user.googleId) { - user.googleId = googleId; - user.provider = 'google'; - if (picture && !user.avatarUrl) { - user.avatarUrl = picture; - } - await user.save(); - } - } else { - let baseUsername = name ? name.replace(/\s+/g, '').toLowerCase() : email.split('@')[0]; - let username = baseUsername; - - const regex = new RegExp(`^${baseUsername}(\\d*)$`); - const existingUsers = await User.find({ username: regex }).select('username').lean(); - - if (existingUsers.length > 0) { - const exactMatch = existingUsers.find(u => u.username === baseUsername); - if (exactMatch) { - let maxCounter = 0; - existingUsers.forEach(u => { - const match = u.username.match(regex); - if (match && match[1]) { - const num = parseInt(match[1]); - if (num > maxCounter) maxCounter = num; - } - }); - username = `${baseUsername}${maxCounter + 1}`; - } - } - - user = await User.create({ - username, - email, - googleId, - avatarUrl: picture, - provider: 'google', - }); - } - - const token = generateToken(user._id); - - res.json({ - message: 'Login successful!', - ...buildAuthResponse(user, token), - }); - } catch (err) { - console.error('Google Auth Error:', err); - res.status(400).json({ error: 'Invalid Google token.' }); - } -}; +router.post('/google', apiLimiter, googleLogin); // @desc Update user avatar // @route POST /api/auth/avatar -const updateAvatar = async (req, res) => { - try { - if (!req.file) { - return res.status(400).json({ error: 'No file uploaded' }); - } - - const filename = `${req.user.id}-${Date.now()}.webp`; - const filepath = path.join(__dirname, '..', 'uploads', filename); - - await sharp(req.file.buffer) - .resize(250, 250, { fit: 'cover' }) - .toFormat('webp') - .toFile(filepath); - - const avatarUrl = `${req.protocol}://${req.get('host')}/uploads/${filename}`; - - const currentUser = await User.findById(req.user.id); - if (currentUser && currentUser.avatarUrl && currentUser.avatarUrl.includes('/uploads/')) { - try { - const oldFilename = currentUser.avatarUrl.split('/uploads/')[1]; - const oldFilePath = path.join(__dirname, '..', 'uploads', oldFilename); - await fs.promises.access(oldFilePath); - await fs.promises.unlink(oldFilePath); - } catch (err) { - if (err.code !== 'ENOENT') { - console.error('Failed to delete old avatar:', err); - } - } - } - - const user = await User.findByIdAndUpdate( - req.user.id, - { avatarUrl }, - { new: true } - ).select('-password'); - - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } - - res.json({ message: 'Avatar updated successfully', user }); - } catch (err) { - console.error('Avatar upload error:', err); - res.status(500).json({ error: 'Server error. Please try again.' }); - } -}; +router.post('/avatar', protect, upload.single('avatar'), updateAvatar); // @desc Forgot password - Send reset link // @route POST /api/auth/forgot-password -const forgotPassword = async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ error: errors.array()[0].msg }); - } - - const { email } = req.body; - const user = await User.findOne({ email }); - if (!user) { - return res.json({ message: 'If an account with that email exists, a reset link has been sent.' }); - } - - const secret = process.env.JWT_SECRET + user.password; - const token = jwt.sign( - { id: user._id, email: user.email }, - secret, - { expiresIn: process.env.PASSWORD_RESET_TOKEN_EXPIRES || '15m' } - ); - - const clientUrl = process.env.CLIENT_URL || 'http://localhost:3000'; - const resetLink = `${clientUrl}/reset-password/${user._id}/${token}`; - - const transporter = nodemailer.createTransport({ - host: process.env.EMAIL_HOST || 'smtp.ethereal.email', - port: process.env.EMAIL_PORT || 587, - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS, - }, - }); - - const emailFrom = process.env.EMAIL_FROM || '"Spam Detection System" '; - - if (process.env.EMAIL_USER && process.env.EMAIL_PASS) { - await transporter.sendMail({ - from: emailFrom, - to: user.email, - subject: 'Password Reset Request', - text: `Please use the following link to reset your password: ${resetLink} \n\nThis link expires in 15 minutes.`, - }); - } else { - console.log(`[DEMO] Password Reset Link for ${user.email}: ${resetLink}`); - } - - res.json({ message: 'If an account with that email exists, a reset link has been sent.' }); - } catch (err) { - console.error('Forgot password error:', err); - res.status(500).json({ error: 'Server error. Please try again later.' }); - } -}; +router.post('/forgot-password', resetLimiter, forgotPasswordValidation, forgotPassword); // @desc Reset password // @route POST /api/auth/reset-password/:id/:token -const resetPassword = async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ error: errors.array()[0].msg }); - } - - const { id, token } = req.params; - const { password } = req.body; - - const user = await User.findById(id); - if (!user) { - return res.status(400).json({ error: 'Invalid or expired token.' }); - } - - const secret = process.env.JWT_SECRET + user.password; - try { - jwt.verify(token, secret); - } catch (err) { - return res.status(400).json({ error: 'Invalid or expired token.' }); - } - - user.password = password; - await user.save(); - - res.json({ message: 'Password has been successfully reset. You can now login.' }); - } catch (err) { - console.error('Reset password error:', err); - res.status(500).json({ error: 'Server error. Please try again later.' }); - } -}; +router.post('/reset-password/:id/:token', resetLimiter, resetPasswordValidation, resetPassword); // @desc Change password (authenticated) // @route POST /api/auth/change-password -const changePassword = async (req, res) => { - try { - const { currentPassword, newPassword } = req.body; - - if (!currentPassword || !newPassword) { - return res.status(400).json({ - success: false, - error: 'Current password and new password are required.' - }); - } - - if (newPassword.length < 6) { - return res.status(400).json({ - success: false, - error: 'New password must be at least 6 characters.' - }); - } - - const user = await User.findById(req.user.id).select('+password'); - if (!user) { - return res.status(404).json({ - success: false, - error: 'User not found.' - }); - } - - const isMatch = await user.comparePassword(currentPassword); - if (!isMatch) { - return res.status(401).json({ - success: false, - error: 'Current password is incorrect.' - }); - } - - user.password = newPassword; - await user.save(); - - await BlacklistedToken.invalidateAllUserTokens( - user._id, - 'PASSWORD_CHANGE', - req.token, - req.ip || req.connection?.remoteAddress, - req.headers['user-agent'] - ); - - res.json({ - success: true, - message: 'Password changed successfully. All sessions invalidated. Please login again.' - }); - } catch (err) { - console.error('Change password error:', err); - res.status(500).json({ - success: false, - error: 'Server error. Please try again later.' - }); - } -}; +router.post('/change-password', protect, changePassword); // @desc Update webhook URL // @route PUT /api/auth/webhook -const updateWebhook = async (req, res) => { - try { - const { webhookUrl } = req.body; - - const newWebhookValue = (webhookUrl && webhookUrl.trim() !== '') ? webhookUrl.trim() : null; - - const user = await User.findByIdAndUpdate( - req.user.id, - { webhookUrl: newWebhookValue }, - { new: true, runValidators: true } - ).select('-password'); - - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } - - res.json({ message: 'Webhook URL updated successfully!', user }); - } catch (err) { - console.error('Webhook update error:', err); - if (err.name === 'ValidationError') { - return res.status(400).json({ error: 'Invalid Webhook URL format. Must start with http:// or https://' }); - } - res.status(500).json({ error: 'Server error. Please try again.' }); - } -}; +router.put('/webhook', protect, updateWebhook); // @desc Get user's session status // @route GET /api/auth/session-status -const getSessionStatus = async (req, res) => { - try { - const token = req.token; - const decoded = jwt.decode(token); - - const now = Math.floor(Date.now() / 1000); - const timeUntilExpiry = decoded.exp - now; - - res.json({ - success: true, - expiresIn: timeUntilExpiry, - expiresAt: new Date(decoded.exp * 1000), - isExpiringSoon: timeUntilExpiry < 300 - }); - } catch (err) { - res.status(500).json({ - success: false, - error: 'Failed to get session status' - }); - } -}; +router.get('/session-status', protect, getSessionStatus); // ============================================ // ZERO TRUST - ROLE MANAGEMENT @@ -1012,519 +85,14 @@ const getSessionStatus = async (req, res) => { // @desc Assign role to user (Admin only) // @route POST /api/auth/admin/assign-role -const assignRole = async (req, res) => { - try { - const { userId, role } = req.body; - - if (!userId || !role) { - return res.status(400).json({ - success: false, - error: 'userId and role are required' - }); - } - - const validRoles = ['user', 'moderator', 'admin']; - if (!validRoles.includes(role)) { - return res.status(400).json({ - success: false, - error: `Invalid role. Must be one of: ${validRoles.join(', ')}` - }); - } - -// ZERO TRUST - ROLE MANAGEMENT -// ============================================ - -// @desc Assign role to user (Admin only) -// @route POST /api/auth/admin/assign-role -const assignRole = async (req, res) => { - try { - const { userId, role } = req.body; - - if (!userId || !role) { - return res.status(400).json({ - success: false, - error: 'userId and role are required' - }); - } - - const validRoles = ['user', 'moderator', 'admin']; - if (!validRoles.includes(role)) { - return res.status(400).json({ - success: false, - error: `Invalid role. Must be one of: ${validRoles.join(', ')}` - }); - } - - - } - - - user = await User.create({ - username, - email, - googleId, - avatarUrl: picture, - provider: 'google', - }); - } - - const token = generateToken(user._id); - - res.json({ - message: 'Login successful!', - ...buildAuthResponse(user, token), - }); - } catch (err) { - console.error('Google Auth Error:', err); - res.status(400).json({ error: 'Invalid Google token.' }); - } -}; - -// @desc Update user avatar -// @route POST /api/auth/avatar -const updateAvatar = async (req, res) => { - try { - if (!req.file) { - return res.status(400).json({ error: 'No file uploaded' }); - } - - const filename = `${req.user.id}-${Date.now()}.webp`; - const filepath = path.join(__dirname, '..', 'uploads', filename); - - await sharp(req.file.buffer) - .resize(250, 250, { fit: 'cover' }) - .toFormat('webp') - .toFile(filepath); - - const avatarUrl = `${req.protocol}://${req.get('host')}/uploads/${filename}`; - - const currentUser = await User.findById(req.user.id); - if (currentUser && currentUser.avatarUrl && currentUser.avatarUrl.includes('/uploads/')) { - try { - const oldFilename = currentUser.avatarUrl.split('/uploads/')[1]; - const oldFilePath = path.join(__dirname, '..', 'uploads', oldFilename); - await fs.promises.access(oldFilePath); - await fs.promises.unlink(oldFilePath); - } catch (err) { - if (err.code !== 'ENOENT') { - console.error('Failed to delete old avatar:', err); - } - - return true; - }) -]; - -/** - * Change Password Validation Rules - */ -const changePasswordValidation = [ - body('currentPassword') - .notEmpty() - .withMessage('Current password is required'), - body('newPassword') - .isLength({ min: 6 }) - .withMessage('New password must be at least 6 characters') - .matches(/^(?=.*[A-Za-z])(?=.*\d)/) - .withMessage('Password must contain at least one letter and one number'), - body('confirmPassword') - .custom((value, { req }) => { - if (value !== req.body.newPassword) { - throw new Error('Passwords do not match'); - - } - } - - const user = await User.findByIdAndUpdate( - req.user.id, - { avatarUrl }, - { new: true } - ).select('-password'); - - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } - - res.json({ message: 'Avatar updated successfully', user }); - } catch (err) { - console.error('Avatar upload error:', err); - res.status(500).json({ error: 'Server error. Please try again.' }); - } -}; - -// @desc Forgot password - Send reset link -// @route POST /api/auth/forgot-password -const forgotPassword = async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ error: errors.array()[0].msg }); - } - - const { email } = req.body; - const user = await User.findOne({ email }); - if (!user) { - return res.json({ message: 'If an account with that email exists, a reset link has been sent.' }); - } - - const secret = process.env.JWT_SECRET + user.password; - const token = jwt.sign( - { id: user._id, email: user.email }, - secret, - { expiresIn: process.env.PASSWORD_RESET_TOKEN_EXPIRES || '15m' } - ); - - const clientUrl = process.env.CLIENT_URL || 'http://localhost:3000'; - const resetLink = `${clientUrl}/reset-password/${user._id}/${token}`; - - const transporter = nodemailer.createTransport({ - host: process.env.EMAIL_HOST || 'smtp.ethereal.email', - port: process.env.EMAIL_PORT || 587, - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS, - }, - }); - - const emailFrom = process.env.EMAIL_FROM || '"Spam Detection System" '; - - if (process.env.EMAIL_USER && process.env.EMAIL_PASS) { - await transporter.sendMail({ - from: emailFrom, - to: user.email, - subject: 'Password Reset Request', - text: `Please use the following link to reset your password: ${resetLink} \n\nThis link expires in 15 minutes.`, - }); - } else { - console.log(`[DEMO] Password Reset Link for ${user.email}: ${resetLink}`); - } - - res.json({ message: 'If an account with that email exists, a reset link has been sent.' }); - } catch (err) { - console.error('Forgot password error:', err); - res.status(500).json({ error: 'Server error. Please try again later.' }); - } -}; - -// @desc Reset password -// @route POST /api/auth/reset-password/:id/:token -const resetPassword = async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ error: errors.array()[0].msg }); - } - - const { id, token } = req.params; - const { password } = req.body; - - const user = await User.findById(id); - if (!user) { - return res.status(400).json({ error: 'Invalid or expired token.' }); - } - - const secret = process.env.JWT_SECRET + user.password; - try { - jwt.verify(token, secret); - } catch (err) { - return res.status(400).json({ error: 'Invalid or expired token.' }); - } - - user.password = password; - await user.save(); - - res.json({ message: 'Password has been successfully reset. You can now login.' }); - } catch (err) { - console.error('Reset password error:', err); - res.status(500).json({ error: 'Server error. Please try again later.' }); - } -}; - -// @desc Change password (authenticated) -// @route POST /api/auth/change-password -const changePassword = async (req, res) => { - try { - const { currentPassword, newPassword } = req.body; - - if (!currentPassword || !newPassword) { - return res.status(400).json({ - success: false, - error: 'Current password and new password are required.' - }); - } - - if (newPassword.length < 6) { - return res.status(400).json({ - success: false, - error: 'New password must be at least 6 characters.' - }); - } - - const user = await User.findById(req.user.id).select('+password'); - if (!user) { - return res.status(404).json({ - success: false, - error: 'User not found.' - }); - } - - const isMatch = await user.comparePassword(currentPassword); - if (!isMatch) { - return res.status(401).json({ - success: false, - error: 'Current password is incorrect.' - }); - } - - user.password = newPassword; - await user.save(); - - await BlacklistedToken.invalidateAllUserTokens( - user._id, - 'PASSWORD_CHANGE', - req.token, - req.ip || req.connection?.remoteAddress, - req.headers['user-agent'] - ); - - res.json({ - success: true, - message: 'Password changed successfully. All sessions invalidated. Please login again.' - }); - } catch (err) { - console.error('Change password error:', err); - res.status(500).json({ - success: false, - error: 'Server error. Please try again later.' - }); - } -}; - -// @desc Update webhook URL -// @route PUT /api/auth/webhook -const updateWebhook = async (req, res) => { - try { - const { webhookUrl } = req.body; - - const newWebhookValue = (webhookUrl && webhookUrl.trim() !== '') ? webhookUrl.trim() : null; - - const user = await User.findByIdAndUpdate( - req.user.id, - { webhookUrl: newWebhookValue }, - { new: true, runValidators: true } - ).select('-password'); - - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } - - res.json({ message: 'Webhook URL updated successfully!', user }); - } catch (err) { - console.error('Webhook update error:', err); - if (err.name === 'ValidationError') { - return res.status(400).json({ error: 'Invalid Webhook URL format. Must start with http:// or https://' }); - } - res.status(500).json({ error: 'Server error. Please try again.' }); - } -}; - -// @desc Get user's session status -// @route GET /api/auth/session-status -const getSessionStatus = async (req, res) => { - try { - const token = req.token; - const decoded = jwt.decode(token); - - const now = Math.floor(Date.now() / 1000); - const timeUntilExpiry = decoded.exp - now; - - res.json({ - success: true, - expiresIn: timeUntilExpiry, - expiresAt: new Date(decoded.exp * 1000), - isExpiringSoon: timeUntilExpiry < 300 - }); - } catch (err) { - res.status(500).json({ - success: false, - error: 'Failed to get session status' - }); - } -}; - -// ============================================ -// ZERO TRUST - ROLE MANAGEMENT -// ============================================ - -// @desc Assign role to user (Admin only) -// @route POST /api/auth/admin/assign-role -const assignRole = async (req, res) => { - try { - const { userId, role } = req.body; - - if (!userId || !role) { - return res.status(400).json({ - success: false, - error: 'userId and role are required' - }); - } - - const validRoles = ['user', 'moderator', 'admin']; - if (!validRoles.includes(role)) { - return res.status(400).json({ - success: false, - error: `Invalid role. Must be one of: ${validRoles.join(', ')}` - }); - } - - - const user = await User.findById(userId); - if (!user) { - return res.status(404).json({ - success: false, - error: 'User not found' - }); - } - - // Check if requester has permission - if (req.user.role !== 'admin') { - return res.status(403).json({ - success: false, - error: 'Admin access required to assign roles' - }); - } - - // Update user role (permissions auto-assigned via pre-save hook) - user.role = role; - await user.save(); - - res.json({ - success: true, - message: `Role '${role}' assigned successfully to ${user.email}`, - user: { - id: user._id, - email: user.email, - username: user.username, - role: user.role, - permissions: user.permissions - } - }); - } catch (err) { - console.error('Assign role error:', err); - res.status(500).json({ - success: false, - error: 'Failed to assign role' - }); - } -}; +router.post('/admin/assign-role', protect, assignRole); // @desc Get user's permissions (Admin only) // @route GET /api/auth/admin/user-permissions/:userId -const getUserPermissions = async (req, res) => { - try { - const { userId } = req.params; - - if (req.user.role !== 'admin') { - return res.status(403).json({ - success: false, - error: 'Admin access required' - }); - } - - const user = await User.findById(userId).select('email username role permissions'); - if (!user) { - return res.status(404).json({ - success: false, - error: 'User not found' - }); - } - - res.json({ - success: true, - user: { - id: user._id, - email: user.email, - username: user.username, - role: user.role, - permissions: user.permissions - } - }); - } catch (err) { - console.error('Get user permissions error:', err); - res.status(500).json({ - success: false, - error: 'Failed to get user permissions' - }); - } -}; - - } -}; - +router.get('/admin/user-permissions/:userId', protect, getUserPermissions); // @desc Get all roles and permissions (Public) // @route GET /api/auth/roles -const getRolesAndPermissions = async (req, res) => { - try { - const roles = User.getRoles ? User.getRoles() : ['user', 'moderator', 'admin']; - const permissions = User.getPermissions ? User.getPermissions() : []; - - res.json({ - success: true, - roles: roles, - permissions: permissions, - rolePermissions: User.ROLE_PERMISSIONS || {} - }); - } catch (err) { - res.status(500).json({ - success: false, - error: 'Failed to get roles and permissions' - }); - } -}; - +router.get('/roles', getRolesAndPermissions); - - -// @desc Get all roles and permissions (Public) -// @route GET /api/auth/roles -const getRolesAndPermissions = async (req, res) => { - try { - const roles = User.getRoles ? User.getRoles() : ['user', 'moderator', 'admin']; - const permissions = User.getPermissions ? User.getPermissions() : []; - - res.json({ - success: true, - roles: roles, - permissions: permissions, - rolePermissions: User.ROLE_PERMISSIONS || {} - }); - } catch (err) { - res.status(500).json({ - success: false, - error: 'Failed to get roles and permissions' - }); - } -}; - -// ============================================ -// 📌 EXPORTS - ONLY ONCE AT THE VERY END -// ============================================ - -module.exports = { - register, - login, - logout, - getMe, - googleLogin, - updateAvatar, - forgotPassword, - resetPassword, - changePassword, - updateWebhook, - getSessionStatus, - assignRole, - getUserPermissions, - getRolesAndPermissions, - generateToken, - buildAuthResponse -}; \ No newline at end of file +module.exports = router; diff --git a/backend/routes/prediction.route.js b/backend/routes/prediction.route.js deleted file mode 100644 index b79679ab..00000000 --- a/backend/routes/prediction.route.js +++ /dev/null @@ -1,41 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const auth = require('../middleware/auth'); - -// GET /api/predictions/stats -router.get('/stats', auth, async (req, res) => { - try { - const userId = req.user.id; - const today = new Date(); - today.setHours(0, 0, 0, 0); - - // Get predictions from database - const predictions = await Prediction.find({ userId }); - - // Calculate stats - const total = predictions.length; - const todayCount = predictions.filter(p => - new Date(p.createdAt) >= today - ).length; - - // Spam vs Ham breakdown - const spamCount = predictions.filter(p => - p.result === 'spam' || p.result === 'smishing' - ).length; - const hamCount = predictions.filter(p => - p.result === 'ham' || p.result === 'safe' - ).length; - - res.json({ - today: todayCount, - total: total, - spamCount: spamCount, - hamCount: hamCount - }); - } catch (error) { - console.error('Stats error:', error); - res.status(500).json({ error: 'Failed to fetch stats' }); - } -}); - -module.exports = router; \ No newline at end of file diff --git a/backend/routes/predictionRoutes.js b/backend/routes/predictionRoutes.js index 7a680672..15b0084e 100644 --- a/backend/routes/predictionRoutes.js +++ b/backend/routes/predictionRoutes.js @@ -290,9 +290,8 @@ router.post("/predict", predictLimiter, preventCacheStampede, protect, checkCach }); router.post("/feedback", protect, async (req, res) => { + const { text, predicted_label, correct_label } = req.body; try { - const { text, predicted_label, correct_label } = req.body; - if (!text || !correct_label) { return res .status(400) @@ -630,20 +629,20 @@ router.get('/stats', protect, async (req, res) => { today.setHours(0, 0, 0, 0); // Get all predictions for user - const predictions = await Prediction.find({ userId }); - + const predictions = await History.find({ user: userId }); + // Calculate stats const total = predictions.length; - const todayCount = predictions.filter(p => + const todayCount = predictions.filter(p => new Date(p.createdAt) >= today ).length; - + // Spam vs Ham breakdown - const spamCount = predictions.filter(p => - p.result === 'spam' || p.result === 'smishing' + const spamCount = predictions.filter(p => + p.prediction === 'spam' || p.prediction === 'smishing' ).length; - const hamCount = predictions.filter(p => - p.result === 'ham' || p.result === 'safe' + const hamCount = predictions.filter(p => + p.prediction === 'ham' || p.prediction === 'safe' ).length; res.json({ diff --git a/backend/server.js b/backend/server.js index 2f08dbed..298a5f3a 100644 --- a/backend/server.js +++ b/backend/server.js @@ -29,19 +29,6 @@ require('./jobs/webhookRetryCron'); const { preventCacheStampede } = require('./middleware/cacheMiddleware'); // Add EvoMail routes const evoMailRoutes = require('./routes/evoMailRoutes'); -app.use('/api/evomail', evoMailRoutes); -// ===== STARTUP TIMER ===== -const SERVER_START_TIME = Date.now(); -const startupLogs = []; -// Add VBSF routes -const visualRoutes = require('./routes/visualRoutes'); -app.use('/api/visual', visualRoutes); -const logStartupTime= (component, startTime) => { - - -// Add EvoMail routes -const evoMailRoutes = require('./routes/evoMailRoutes'); -app.use('/api/evomail', evoMailRoutes); const healthRoutes = require("./routes/healthRoutes"); const predictionRoutes = require("./routes/predictionRoutes"); @@ -79,6 +66,8 @@ const app = express(); const { apiLimiter } = require('./middleware/rateLimiter'); app.use('/predict', apiLimiter); +app.use('/api/evomail', evoMailRoutes); + // Trust the first proxy so express-rate-limit correctly identifies user IPs @@ -162,6 +151,7 @@ const monitorConnectionPool = () => { } } } catch (err) { + logger.error('[DB Pool] Failed to log connection pool stats:', err.message); } }, 60000); // every 60 seconds diff --git a/backend/utils/emailRules.js b/backend/utils/emailRules.js index ddbe8165..a45a6514 100644 --- a/backend/utils/emailRules.js +++ b/backend/utils/emailRules.js @@ -71,7 +71,7 @@ async function applyRulesToEmails(userId, emails) { return { ...email, prediction: updatedPrediction, - rule_applied: matchingRule.type + rule_applied: matchedType }; } diff --git a/backend/utils/utils/emailTransporter.js b/backend/utils/emailTransporter.js similarity index 100% rename from backend/utils/utils/emailTransporter.js rename to backend/utils/emailTransporter.js diff --git a/backend/utils/fileValidation.js b/backend/utils/fileValidation.js index 4061f6ff..c137d449 100644 --- a/backend/utils/fileValidation.js +++ b/backend/utils/fileValidation.js @@ -74,7 +74,7 @@ function sanitizeCSVCell(value) { let sanitized = value.replace(//g, '>'); // Neutralize formula injection - if (/^[=\+\-@]/.test(sanitized)) { + if (/^[=+\-@]/.test(sanitized)) { sanitized = "'" + sanitized; } From 921aa6a5b1514db45a1b03bf6279730999c6d418 Mon Sep 17 00:00:00 2001 From: Rudra-clrscr Date: Tue, 14 Jul 2026 04:38:58 +0530 Subject: [PATCH 2/8] fix: bind ML API to 0.0.0.0 in Docker CI smoke test The container's Flask process defaults to FLASK_HOST=127.0.0.1 (a deliberate secure default for bare-metal runs), so inside the CI container it never listens on an interface reachable through the -p 5000:5000 port mapping, and the smoke test's curl always times out. docker-compose.yml already sets FLASK_HOST=0.0.0.0 for the same reason; the CI workflow's docker run step needs the same override. --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2095f3b7..e034a292 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -24,7 +24,7 @@ jobs: run: docker build -t spam-frontend:ci -f frontend/Dockerfile --build-arg VITE_API_URI=/predict frontend - name: Start ML API container - run: docker run --rm -d -p 5000:5000 --name spam_ml_api_ci spam-ml-api:ci + run: docker run --rm -d -p 5000:5000 -e FLASK_HOST=0.0.0.0 --name spam_ml_api_ci spam-ml-api:ci - name: Smoke test - ML API responds on port 5000 run: | From ddc5a6aa5e290615670a8e6e3d2b0e7dc569dee9 Mon Sep 17 00:00:00 2001 From: Rudra-clrscr Date: Tue, 14 Jul 2026 05:04:51 +0530 Subject: [PATCH 3/8] fix: pull in App.jsx and ManipulationIndex.jsx parse fixes main's copies of these files have a duplicate 'function App()' declaration and a stray '#Manipulation' line before an import respectively - both invalid JS that break 'vite build' in the frontend Docker image stage. Only fixed inside the open fix/frontend-eslint-errors PR branch, never merged to main, so any branch cut from main (including this one) inherits them. Pulling in the already-verified fix. --- frontend/src/components/ManipulationIndex.jsx | 1 - frontend/src/pages/App.jsx | 135 +++--------------- 2 files changed, 23 insertions(+), 113 deletions(-) diff --git a/frontend/src/components/ManipulationIndex.jsx b/frontend/src/components/ManipulationIndex.jsx index a0afaf14..9500a6d3 100644 --- a/frontend/src/components/ManipulationIndex.jsx +++ b/frontend/src/components/ManipulationIndex.jsx @@ -1,4 +1,3 @@ -#Manipulation import React from 'react'; const ManipulationIndex = ({ text, result, darkMode }) => { diff --git a/frontend/src/pages/App.jsx b/frontend/src/pages/App.jsx index 86004c54..ceddcd6f 100644 --- a/frontend/src/pages/App.jsx +++ b/frontend/src/pages/App.jsx @@ -20,7 +20,6 @@ import Skeleton from 'react-loading-skeleton'; import 'react-loading-skeleton/dist/skeleton.css'; import EmailHeaderAnalyzer from "../components/EmailHeaderAnalyzer"; import BulkSpamDetection from "../components/BulkSpamDetection"; -import { ResultBadge } from './components/ResultBadge'; import SpamInsightsDashboard from "../components/SpamInsightsDashboard"; import EmailScannerDashboard from "../components/EmailScannerDashboard"; import Chatbot from "../components/Chatbot"; @@ -50,12 +49,6 @@ function App() { const [hasCelebrated, setHasCelebrated] = useState(() => { return localStorage.getItem("firstPrediction") === "true"; }); - const [showCelebration, setShowCelebration] = useState(false); - - const [darkMode, setDarkMode] = useState(false); - const [showHistory, setShowHistory] = useState(false); - const [theme, setTheme] = useState("ocean"); - const [showThemes, setShowThemes] = useState(false); const [showSettings, setShowSettings] = useState(false); const [activeTab, setActiveTab] = useState(() => { @@ -68,14 +61,6 @@ function App() { const [soundEnabled, setSoundEnabled] = useState(true); - // Detect URLs in text - const detectURLs = (text) => { - if (!text) return []; - const urlRegex = /(https?:\/\/[^\s]+)/g; - const matches = text.match(urlRegex); - return matches || []; - }; - const playSpamSound = () => { if (!soundEnabled) return; try { @@ -92,65 +77,11 @@ function App() { osc.start(ctx.currentTime + delay); osc.stop(ctx.currentTime + delay + 0.15); }); - } catch (e) { + } catch { /* silent fail */ } }; - // Helper to get earned badges (returns array of badge objects) - const getEarnedBadges = () => { - try { - const streakCount = parseInt(localStorage.getItem('predictionStreak') || '0', 10); - return Object.keys(Badges) - .map((k) => ({ day: Number(k), ...Badges[k] })) - .filter((b) => streakCount >= b.day); - } catch (e) { - return []; - } - }; - - // Placeholder for badge checking logic - const checkNewBadge = (newStreak) => { - // simple implementation: if new streak matches a badge threshold, show popup - if (Badges[newStreak]) { - setNewBadgeEarned(true); - setShowBadgePopup(true); - setTimeout(() => setShowBadgePopup(false), 4000); - } - }; - - //Streak tracking - const [streak, setStreak] = useState(() => { - const lastDate = localStorage.getItem("lastPredictionDate"); - const streakCount = parseInt(localStorage.getItem("streakCount") || "0", 10); - const today = new Date().toDateString(); - - if (lastDate === today) return streakCount; - if(lastDate){ - const last = new Date(lastDate); - const now = new Date(); - const diffTime = now - last; - const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); - - if (diffDays === 1) return streakCount + 1; - if (diffDays > 1) return 0; - } - return streakCount; - }); - - const [newBadgeEarned, setNewBadgeEarned] = useState(false); - const [showBadgePopup, setShowBadgePopup] = useState(false); - - //Badge Definitions - const Badges = { - 3: { name: '🔥 Novice Streaker', icon: '🔥', color: 'bg-orange-500', description: '3 day streak' }, - 7: { name: '⚡ Weekly Warrior', icon: '⚡', color: 'bg-blue-500', description: '7 day streak' }, - 14: { name: '🌟 Fortnight Champion', icon: '🌟', color: 'bg-purple-500', description: '14 day streak' }, - 30: { name: '🏆 Monthly Master', icon: '🏆', color: 'bg-yellow-500', description: '30 day streak' }, - 50: { name: '💎 Diamond Streaker', icon: '💎', color: 'bg-cyan-500', description: '50 day streak' }, - 100: { name: '👑 Legendary Streaker', icon: '👑', color: 'bg-red-500', description: '100 day streak' }, - }; - const playHamSound = () => { if (!soundEnabled) return; try { @@ -167,7 +98,7 @@ function App() { osc.start(ctx.currentTime + i * 0.12); osc.stop(ctx.currentTime + i * 0.12 + 0.15); }); - } catch (e) { /* silent fail */ } + } catch { /* silent fail */ } }; const { user, login, logout } = useAuth(); @@ -272,7 +203,7 @@ function App() { const analyzeEmojiSentiment = (text) => { if (!text) return { positive: 0, negative: 0, neutral: 0 }; - const emojiRegex = /([\u2700-\u27BF]|[\uE000-\uF8FF]|[\uD83C-\uDBFF\uDC00-\uDFFF])/g; + const emojiRegex = /([\u2700-\u27BF]|[\uE000-\uF8FF]|[\uD83C-\uDBFF\uDC00-\uDFFF])/gu; const matches = text.match(emojiRegex) || []; if (matches.length === 0) return { positive: 0, negative: 0, neutral: 0 }; @@ -396,31 +327,8 @@ const analyzeEmojiSentiment = (text) => { message: errorMessage, retryable: retryable }); - } finally { + } finally { setLoading(false); - -function App() { - const [showButton, setShowButton] = useState(false); - - useEffect(() => { - const handleScroll = () => { - setShowButton(window.scrollY > 300); - }; - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); - }, []); - - const scrollToTop = () => { - window.scrollTo({ top: 0, behavior: 'smooth' }); - }; - const getColor = () => { - if (result === "ham" || result === "safe") - return "text-green-600 dark:text-green-400"; - if (result === "spam" || result === "malicious") - return "text-red-600 dark:text-red-400"; - if (result === "smishing") return "text-orange-600 dark:text-orange-400"; - if (result === "Error") { - return isDark ? "text-yellow-300" : "text-yellow-700"; } }; @@ -438,9 +346,6 @@ function App() { setTimeout(() => { confetti({ particleCount: 50, spread: 50, origin: { y: 0.6, x: 0.7 } }); }, 400); - setTimeout(() => { - setShowCelebration(true); - }, 500); }; const confidencePct = confidence !== null ? Math.min(confidence * 50 + 50, 100).toFixed(1) : "0.0"; @@ -635,18 +540,20 @@ function App() { onClick={() => setActiveTab("rules")} className={`pb-1 px-4 transition-all border-b-2 ${activeTab === "rules" ? "border-current opacity-100" : "border-transparent opacity-50 hover:opacity-75"}`} > + Rules Manager + - Rules Manager {(result === "spam" || result === "malicious" || result === "smishing") && ( )} - - History - + Rules Manager {(result === "spam" || result === "malicious" || result === "smishing") && ( )} + + History + + Rules Manager {(result === "spam" || result === "malicious" || result === "smishing") && ( )} + + History +