diff --git a/frontend/src/test/JobAnalyzer.test.jsx b/frontend/src/test/JobAnalyzer.test.jsx
new file mode 100644
index 0000000..0d4f277
--- /dev/null
+++ b/frontend/src/test/JobAnalyzer.test.jsx
@@ -0,0 +1,36 @@
+import { render, screen, fireEvent } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
+import JobAnalyzer from '../components/JobAnalyzer'
+
+describe('JobAnalyzer duplicate URL detection (L10)', () => {
+ const existingApplications = [
+ {
+ id: 'app-1',
+ company: 'TechCorp',
+ role: 'Backend Engineer',
+ status: 'Wishlist',
+ url: 'https://careers.techcorp.com/jobs/123',
+ },
+ ]
+
+ it('displays inline notice when entered URL matches an existing application', () => {
+ render(
)
+
+ const input = screen.getByPlaceholderText(/Paste LinkedIn, Greenhouse, Workday/i)
+ fireEvent.change(input, { target: { value: 'https://careers.techcorp.com/jobs/123/' } })
+
+ expect(screen.getByText(/Already tracked:/i)).toBeInTheDocument()
+ expect(screen.getByText('TechCorp')).toBeInTheDocument()
+ expect(screen.getByText('(Backend Engineer)')).toBeInTheDocument()
+ expect(screen.getByText('[Wishlist]')).toBeInTheDocument()
+ })
+
+ it('does not display inline notice for novel URLs', () => {
+ render(
)
+
+ const input = screen.getByPlaceholderText(/Paste LinkedIn, Greenhouse, Workday/i)
+ fireEvent.change(input, { target: { value: 'https://careers.newco.com/jobs/456' } })
+
+ expect(screen.queryByText(/Already tracked:/i)).not.toBeInTheDocument()
+ })
+})
From a9b5b0c33b6b189589d7a53e617374d75e4c70fe Mon Sep 17 00:00:00 2001
From: Sourav Chandhok <114119117+souravC01@users.noreply.github.com>
Date: Sat, 12 Sep 2026 00:07:56 -0400
Subject: [PATCH 10/14] fix(deps): Upgrade vitest to 4.1.11 to resolve
GHSA-82fw-gwwq-j7x9 vulnerability
---
frontend/package-lock.json | 332 ++++++++++++++-----------------------
frontend/package.json | 2 +-
2 files changed, 128 insertions(+), 206 deletions(-)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 31615c0..e843197 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -20,7 +20,7 @@
"jsdom": "25.0.1",
"tailwindcss": "^4.3.3",
"vite": "^6.4.3",
- "vitest": "^3.2.7"
+ "vitest": "^4.1.11"
}
},
"node_modules/@adobe/css-tools": {
@@ -1366,6 +1366,13 @@
"win32"
]
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@tailwindcss/node": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
@@ -1827,39 +1834,40 @@
}
},
"node_modules/@vitest/expect": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
- "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
+ "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
"dev": true,
"license": "MIT",
"dependencies": {
+ "@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "3.2.7",
- "@vitest/utils": "3.2.7",
- "chai": "^5.2.0",
- "tinyrainbow": "^2.0.0"
+ "@vitest/spy": "4.1.11",
+ "@vitest/utils": "4.1.11",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
- "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
+ "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "3.2.7",
+ "@vitest/spy": "4.1.11",
"estree-walker": "^3.0.3",
- "magic-string": "^0.30.17"
+ "magic-string": "^0.30.21"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"msw": {
@@ -1871,42 +1879,42 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
- "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
+ "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tinyrainbow": "^2.0.0"
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
- "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
+ "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "3.2.7",
- "pathe": "^2.0.3",
- "strip-literal": "^3.0.0"
+ "@vitest/utils": "4.1.11",
+ "pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
- "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
+ "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "3.2.7",
- "magic-string": "^0.30.17",
+ "@vitest/pretty-format": "4.1.11",
+ "@vitest/utils": "4.1.11",
+ "magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
"funding": {
@@ -1914,28 +1922,25 @@
}
},
"node_modules/@vitest/spy": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
- "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
+ "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "tinyspy": "^4.0.3"
- },
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
- "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
+ "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "3.2.7",
- "loupe": "^3.1.4",
- "tinyrainbow": "^2.0.0"
+ "@vitest/pretty-format": "4.1.11",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
@@ -2052,16 +2057,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
- "node_modules/cac": {
- "version": "6.7.14",
- "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
- "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/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",
@@ -2098,18 +2093,11 @@
"license": "CC-BY-4.0"
},
"node_modules/chai": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
- "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "assertion-error": "^2.0.1",
- "check-error": "^2.1.1",
- "deep-eql": "^5.0.1",
- "loupe": "^3.1.0",
- "pathval": "^2.0.0"
- },
"engines": {
"node": ">=18"
}
@@ -2128,16 +2116,6 @@
"node": ">=8"
}
},
- "node_modules/check-error": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
- "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 16"
- }
- },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2245,16 +2223,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/deep-eql": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
- "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -2363,9 +2331,9 @@
}
},
"node_modules/es-module-lexer": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+ "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
"dev": true,
"license": "MIT"
},
@@ -3097,13 +3065,6 @@
"loose-envify": "cli.js"
}
},
- "node_modules/loupe": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
- "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3230,6 +3191,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/obug": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
+ "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -3250,16 +3225,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/pathval": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
- "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14.16"
- }
- },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -3523,9 +3488,9 @@
"license": "MIT"
},
"node_modules/std-env": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
- "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
+ "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
"dev": true,
"license": "MIT"
},
@@ -3542,26 +3507,6 @@
"node": ">=8"
}
},
- "node_modules/strip-literal": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
- "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^9.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/strip-literal/node_modules/js-tokens": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
- "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -3611,11 +3556,14 @@
"license": "MIT"
},
"node_modules/tinyexec": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
- "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
+ "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
},
"node_modules/tinyglobby": {
"version": "0.2.17",
@@ -3634,30 +3582,10 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
- "node_modules/tinypool": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
- "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- }
- },
"node_modules/tinyrainbow": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
- "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tinyspy": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz",
- "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
+ "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3816,89 +3744,80 @@
}
}
},
- "node_modules/vite-node": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
- "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cac": "^6.7.14",
- "debug": "^4.4.1",
- "es-module-lexer": "^1.7.0",
- "pathe": "^2.0.3",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
- },
- "bin": {
- "vite-node": "vite-node.mjs"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
"node_modules/vitest": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
- "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
+ "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/chai": "^5.2.2",
- "@vitest/expect": "3.2.7",
- "@vitest/mocker": "3.2.7",
- "@vitest/pretty-format": "^3.2.7",
- "@vitest/runner": "3.2.7",
- "@vitest/snapshot": "3.2.7",
- "@vitest/spy": "3.2.7",
- "@vitest/utils": "3.2.7",
- "chai": "^5.2.0",
- "debug": "^4.4.1",
- "expect-type": "^1.2.1",
- "magic-string": "^0.30.17",
+ "@vitest/expect": "4.1.11",
+ "@vitest/mocker": "4.1.11",
+ "@vitest/pretty-format": "4.1.11",
+ "@vitest/runner": "4.1.11",
+ "@vitest/snapshot": "4.1.11",
+ "@vitest/spy": "4.1.11",
+ "@vitest/utils": "4.1.11",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
"pathe": "^2.0.3",
- "picomatch": "^4.0.2",
- "std-env": "^3.9.0",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
"tinybench": "^2.9.0",
- "tinyexec": "^0.3.2",
- "tinyglobby": "^0.2.14",
- "tinypool": "^1.1.1",
- "tinyrainbow": "^2.0.0",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
- "vite-node": "3.2.4",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
- "@types/debug": "^4.1.12",
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
- "@vitest/browser": "3.2.7",
- "@vitest/ui": "3.2.7",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.11",
+ "@vitest/browser-preview": "4.1.11",
+ "@vitest/browser-webdriverio": "4.1.11",
+ "@vitest/coverage-istanbul": "4.1.11",
+ "@vitest/coverage-v8": "4.1.11",
+ "@vitest/ui": "4.1.11",
"happy-dom": "*",
- "jsdom": "*"
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
"optional": true
},
- "@types/debug": {
+ "@opentelemetry/api": {
"optional": true
},
"@types/node": {
"optional": true
},
- "@vitest/browser": {
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
"optional": true
},
"@vitest/ui": {
@@ -3909,6 +3828,9 @@
},
"jsdom": {
"optional": true
+ },
+ "vite": {
+ "optional": false
}
}
},
diff --git a/frontend/package.json b/frontend/package.json
index 16b64cf..5f58213 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -23,6 +23,6 @@
"tailwindcss": "^4.3.3",
"jsdom": "25.0.1",
"vite": "^6.4.3",
- "vitest": "^3.2.7"
+ "vitest": "^4.1.11"
}
}
From 925090d789bbd8f1ff9c525a42ebaba6e6fb3146 Mon Sep 17 00:00:00 2001
From: Sourav Chandhok <114119117+souravC01@users.noreply.github.com>
Date: Sat, 12 Sep 2026 00:08:18 -0400
Subject: [PATCH 11/14] docs(launch): Add public launch runbook for deployment,
migration, and verification
---
docs/PUBLIC_LAUNCH_RUNBOOK.md | 113 ++++++++++++++++++++++++++++++++++
1 file changed, 113 insertions(+)
create mode 100644 docs/PUBLIC_LAUNCH_RUNBOOK.md
diff --git a/docs/PUBLIC_LAUNCH_RUNBOOK.md b/docs/PUBLIC_LAUNCH_RUNBOOK.md
new file mode 100644
index 0000000..0bf7983
--- /dev/null
+++ b/docs/PUBLIC_LAUNCH_RUNBOOK.md
@@ -0,0 +1,113 @@
+# Public Launch Runbook
+
+This runbook defines the operational deployment, migration, and pre-flight verification procedures for releasing JobHelperGuru to production.
+
+---
+
+## 1. Required Environment Variables
+
+Ensure the following environment variables are set in production before starting the service:
+
+### Core Configuration
+- `PORT`: HTTP port to bind (default `8000` or assigned by host).
+- `APP_BASE_URL`: Public canonical base URL (e.g. `https://jobhelper.guru`).
+- `ENVIRONMENT`: Set to `production`.
+- `JWT_SECRET`: High-entropy secret key for signing user authentication tokens.
+- `ENCRYPTION_KEY`: Fernet encryption key for securing stored provider API keys (base64 url-safe 32-byte key).
+
+### Database Configuration
+- `DATABASE_URL`: PostgreSQL connection string (e.g. `postgresql://user:pass@ep-xyz.neon.tech/neondb?sslmode=require`).
+- `JOB_HELPER_DB`: Optional path to local SQLite database when running outside PostgreSQL.
+
+### Email Delivery (Brevo)
+- `EMAIL_TRANSPORT`: Set to `brevo` for production delivery.
+- `BREVO_API_KEY`: API key generated from Brevo account dashboard (SMTP & API -> API Keys).
+- `BREVO_SENDER_EMAIL`: Verified sender email address registered with Brevo.
+- `BREVO_SENDER_NAME`: Display name for outbound emails (e.g. `JobHelperGuru Support`).
+
+### Storage Backend (S3 / Cloudflare R2 / GCS)
+- `STORAGE_BACKEND`: `s3` (or `local` in staging environments).
+- `S3_BUCKET_NAME`: Name of the private object storage bucket.
+- `S3_ENDPOINT_URL`: Endpoint URL for S3-compatible providers (leave empty for AWS S3).
+- `AWS_ACCESS_KEY_ID`: IAM access key.
+- `AWS_SECRET_ACCESS_KEY`: IAM secret key.
+- `AWS_REGION`: Storage region (e.g. `auto` or `us-east-1`).
+
+### OAuth & Security
+- `GOOGLE_CLIENT_ID`: Google OAuth client ID for SSO.
+- `ALLOWED_ORIGINS`: Comma-separated list of allowed CORS origins (e.g. `https://jobhelper.guru`).
+
+---
+
+## 2. Pre-Deployment Data Operations
+
+### Step A: Audit and Repair Inconsistent Data
+Before performing migrations, run the automated repair utility to resolve any legacy schema inconsistencies or orphaned attachments:
+
+1. Dry run (read-only audit):
+ ```bash
+ python -m backend.repair_data
+ ```
+2. Apply repairs:
+ ```bash
+ python -m backend.repair_data --apply
+ ```
+
+### Step B: Database Migration to Production
+Migrate data from local/staging SQLite database to the target PostgreSQL / Neon instance:
+
+1. Verify record counts with dry-run:
+ ```bash
+ python -m backend.migrate --source data/tracker.db --dest "$DATABASE_URL" --dry-run
+ ```
+2. Execute migration:
+ ```bash
+ python -m backend.migrate --source data/tracker.db --dest "$DATABASE_URL" --apply
+ ```
+3. Verify migration summary logs:
+ - Check `Users`, `Applications`, `Resumes`, `Attachments`, `User Settings`, `Global Settings`, and `Provider Profiles`.
+ - Ensure `errors` list is empty.
+
+---
+
+## 3. Pre-Flight Verification Checklist
+
+Run through the following verification scenarios prior to directing public traffic:
+
+### 1. User Registration & Email Delivery
+- Register a new user account with a valid test email.
+- Verify delivery of verification email via Brevo transactional API.
+- Click the verification link and confirm the SPA deep link loads `/verify-email?token=...` correctly without 404.
+
+### 2. Account Recovery & Deep Link Navigation
+- Request a password reset.
+- Confirm reset email arrives with one-time token.
+- Access `/reset-password?token=...` and verify form loads and completes password update.
+
+### 3. Resume Lifecycle & Quota Enforcement
+- Upload a PDF/DOCX resume.
+- Verify file metadata is recorded and storage object is created.
+- Download the resume and verify Unicode filename preservation (`filename*` RFC 5987).
+- Verify upload quota enforces maximum 10 active resumes per user.
+- Delete the resume and confirm idempotent cleanup in database and object storage.
+
+### 4. Job Pipeline & Duplicate Detection
+- Ingest a job URL into the Analyzer.
+- Enter a duplicate URL already in the pipeline and verify inline notice displays: `Already tracked: [Company] ([Role]) [[Status]]`.
+- Verify Kanban board horizontally scrolls smoothly with clear column widths on mobile and desktop viewports.
+
+### 5. Outbound Connection & SSRF Protection
+- Ensure requests targeting internal or link-local IP addresses (`127.0.0.1`, `169.254.169.254`, `10.0.0.0/8`, `192.168.0.0/16`) are blocked with HTTP 400.
+
+### 6. Concurrency & Rate Limiting
+- Confirm rate limiting returns `Retry-After` header when limit is exceeded.
+- Confirm frontend automatically parses `Retry-After` and handles throttling transparently.
+
+---
+
+## 4. Rollback Plan
+
+If unexpected failures occur during launch:
+1. Revert DNS/routing to previous stable environment.
+2. In the event of a database rollback, point `DATABASE_URL` back to the previous snapshot or restore from the pre-migration dump.
+3. Review application logs via `journalctl` or hosting platform log stream.
From 92248a6fd67882b53309794f7347ff070477cf3d Mon Sep 17 00:00:00 2001
From: Sourav Chandhok <114119117+souravC01@users.noreply.github.com>
Date: Sat, 12 Sep 2026 00:56:06 -0400
Subject: [PATCH 12/14] docs(launch): Add step-by-step production launch guide
---
docs/STEP_BY_STEP_LAUNCH_GUIDE.md | 222 ++++++++++++++++++++++++++++++
1 file changed, 222 insertions(+)
create mode 100644 docs/STEP_BY_STEP_LAUNCH_GUIDE.md
diff --git a/docs/STEP_BY_STEP_LAUNCH_GUIDE.md b/docs/STEP_BY_STEP_LAUNCH_GUIDE.md
new file mode 100644
index 0000000..926912b
--- /dev/null
+++ b/docs/STEP_BY_STEP_LAUNCH_GUIDE.md
@@ -0,0 +1,222 @@
+# Step-by-Step Production Launch Guide
+
+This guide walks you through every action required on your end to launch JobHelperGuru to production.
+
+---
+
+## Overview of Tasks
+
+1. [Step 1: Set Up Brevo Transactional Email](#step-1-set-up-brevo-transactional-email) (5 minutes)
+2. [Step 2: Generate Production Cryptographic Secrets](#step-2-generate-production-cryptographic-secrets) (1 minute)
+3. [Step 3: Database & Object Storage Credentials](#step-3-database--object-storage-credentials) (5 minutes)
+4. [Step 4: Run Database Repair & Migration](#step-4-run-database-repair--migration) (2 minutes)
+5. [Step 5: Push Branch to GitHub & Deploy](#step-5-push-branch-to-github--deploy) (3 minutes)
+6. [Step 6: Live Smoke Test Checklist](#step-6-live-smoke-test-checklist) (5 minutes)
+
+---
+
+## Step 1: Set Up Brevo Transactional Email
+
+JobHelperGuru uses Brevo (formerly Sendinblue) to deliver account verification emails and password reset links. Brevo provides 300 free emails per day with high deliverability.
+
+### 1. Create / Log In to Brevo
+1. Go to [https://www.brevo.com](https://www.brevo.com) and sign up or sign in.
+2. Complete your organization details (you can use "JobHelperGuru").
+
+### 2. Verify Your Sender Email Address
+1. In the top-right corner, click your account name and select **Senders, Domains & Dedicated IPs** (or go directly to [https://app.brevo.com/senders](https://app.brevo.com/senders)).
+2. Click **Add a Sender**.
+3. Enter:
+ - **Sender Name**: `JobHelperGuru Support`
+ - **Sender Email**: Your email (e.g. `sourav.chandhok01@gmail.com` or your domain email `support@jobhelper.guru`).
+4. Brevo will send a 6-digit verification code to that email address. Enter the code in Brevo to verify.
+
+### 3. Generate Your Brevo API Key
+1. In the top-right account dropdown, click **SMTP & API** (or visit [https://app.brevo.com/settings/keys/api](https://app.brevo.com/settings/keys/api)).
+2. Go to the **API Keys** tab.
+3. Click **Generate a new API key**.
+4. Name it `jobhelperguru-production`.
+5. Click **Generate** and immediately copy the key (it starts with `xkeysib-...`). Save it securely.
+
+### Your Brevo Configuration:
+```env
+EMAIL_TRANSPORT=brevo
+BREVO_API_KEY=xkeysib-your-copied-api-key
+BREVO_SENDER_EMAIL=your-verified-sender-email@gmail.com
+BREVO_SENDER_NAME=JobHelperGuru Support
+```
+
+---
+
+## Step 2: Generate Production Cryptographic Secrets
+
+You need two random high-entropy secrets for production. Run these two quick one-liners directly in your terminal:
+
+### 1. Generate `JWT_SECRET` (For signing user auth tokens):
+Run in PowerShell / Terminal:
+```bash
+python -c "import secrets; print('JWT_SECRET=' + secrets.token_hex(32))"
+```
+*Copy the generated 64-character string.*
+
+### 2. Generate `ENCRYPTION_KEY` (For encrypting stored LLM provider keys):
+Run in PowerShell / Terminal:
+```bash
+python -c "from cryptography.fernet import Fernet; print('ENCRYPTION_KEY=' + Fernet.generate_key().decode())"
+```
+*Copy the generated 44-character base64 string.*
+
+---
+
+## Step 3: Database & Object Storage Credentials
+
+### 1. PostgreSQL Database (Neon Serverless Postgres)
+If you are using Neon (free tier Postgres):
+1. Log in to [https://console.neon.tech](https://console.neon.tech).
+2. Select your project (or click **New Project** -> name: `jobhelperguru-db`).
+3. Under **Dashboard**, copy your connection string:
+ ```
+ postgresql://user:password@ep-xyz.neon.tech/neondb?sslmode=require
+ ```
+4. Set this as `DATABASE_URL`.
+
+### 2. Cloud Storage for Resume Files (AWS S3 or Cloudflare R2)
+If you want resume PDF uploads stored in the cloud:
+- **Cloudflare R2** (Recommended: 10GB free, $0 egress fees):
+ - In Cloudflare Dashboard -> R2 -> Create bucket: `jobhelperguru-resumes`.
+ - Create API Token with Admin Read/Write permissions.
+ - Set:
+ ```env
+ STORAGE_BACKEND=s3
+ S3_BUCKET_NAME=jobhelperguru-resumes
+ AWS_ACCESS_KEY_ID=your-r2-access-key-id
+ AWS_SECRET_ACCESS_KEY=your-r2-secret-access-key
+ AWS_REGION=auto
+ S3_ENDPOINT_URL=https://
.r2.cloudflarestorage.com
+ ```
+- **Local Storage Alternative** (If running locally or with a persistent disk):
+ ```env
+ STORAGE_BACKEND=local
+ ```
+
+---
+
+## Step 4: Run Database Repair & Migration
+
+Before sending traffic to your production database, run the automated repair and migration scripts from your repository root.
+
+### 1. Clean Local Data Inconsistencies
+Run this command to audit and fix any corrupted records in your local `data/tracker.db`:
+```pwsh
+python -m backend.repair_data --apply
+```
+*Expected output: `Audit results: {'corrupt_applications': ..., 'corrupt_settings': ..., 'orphaned_attachments': ..., 'applied': True}`.*
+
+### 2. Migrate Data to PostgreSQL
+Set your production `DATABASE_URL` in your terminal session and run the migration:
+
+**In PowerShell:**
+```pwsh
+$env:DATABASE_URL = "postgresql://user:password@ep-xyz.neon.tech/neondb?sslmode=require"
+
+# First run a dry-run check (read-only):
+python -m backend.migrate --source data/tracker.db --dest $env:DATABASE_URL --dry-run
+
+# Then apply the migration to production:
+python -m backend.migrate --source data/tracker.db --dest $env:DATABASE_URL --apply
+```
+
+**In Linux / macOS Bash:**
+```bash
+export DATABASE_URL="postgresql://user:password@ep-xyz.neon.tech/neondb?sslmode=require"
+
+# First run a dry-run check (read-only):
+python -m backend.migrate --source data/tracker.db --dest "$DATABASE_URL" --dry-run
+
+# Then apply the migration to production:
+python -m backend.migrate --source data/tracker.db --dest "$DATABASE_URL" --apply
+```
+
+Verify the output shows `Mode: APPLIED` and `Errors encountered:` is not present.
+
+---
+
+## Step 5: Push Branch to GitHub & Deploy
+
+All fixes have been committed locally to branch `launch-readiness`.
+
+### 1. Push `launch-readiness` to GitHub
+Run in PowerShell:
+```pwsh
+git push origin launch-readiness
+```
+
+### 2. Merge into `main` (When ready to release)
+You can either open a Pull Request on GitHub and merge it, or merge locally:
+```pwsh
+git checkout main
+git merge launch-readiness
+git push origin main
+```
+
+### 3. Configure Production Environment Variables on Host
+On your hosting dashboard (e.g. Render, Google Cloud Run, or Railway), enter the environment variables gathered from Steps 1, 2, and 3:
+
+```env
+ENVIRONMENT=production
+APP_BASE_URL=https://your-domain.com
+JWT_SECRET=
+ENCRYPTION_KEY=
+DATABASE_URL=
+EMAIL_TRANSPORT=brevo
+BREVO_API_KEY=
+BREVO_SENDER_EMAIL=
+BREVO_SENDER_NAME=JobHelperGuru Support
+STORAGE_BACKEND=s3
+S3_BUCKET_NAME=jobhelperguru-resumes
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_REGION=auto
+GOOGLE_CLIENT_ID=
+```
+
+---
+
+## Step 6: Live Smoke Test Checklist
+
+Once the production service is live, perform this 5-minute end-to-end verification:
+
+### Test 1: User Registration & Email Delivery
+1. Open your production site in an incognito window.
+2. Register a new account using your real email address.
+3. Check your email inbox for the message: **"Verify your email - JobHelperGuru"**.
+4. Click the verification button in the email.
+5. Confirm it loads `https://your-domain.com/verify-email?token=...` and displays:
+ **"Email verified successfully! You can now log in."** (no 404 error).
+
+### Test 2: Password Recovery
+1. On the login screen, click **Forgot password?**.
+2. Enter your email and submit.
+3. Check your inbox for the reset link: **"Reset your password - JobHelperGuru"**.
+4. Click the link and enter a new password.
+5. Confirm successful reset and log in with the new password.
+
+### Test 3: Resume Management & Unicode Download
+1. Log in to your account.
+2. Go to **Resumes** and upload a resume (PDF or DOCX).
+3. Verify the resume appears in your Resume Library.
+4. Click **Download** and verify the file downloads intact with the correct filename.
+5. Delete the resume and confirm it is removed cleanly.
+
+### Test 4: Job Analyzer & Duplicate Detection
+1. In the **Analyzer**, paste a job URL (e.g., from LinkedIn or Greenhouse) and click **Analyze Job**.
+2. Click **Add to Pipeline**.
+3. Now paste the exact same job URL into the input field again.
+4. Verify the inline notice appears immediately:
+ `Already tracked: [Company] ([Role]) [[Status]]`.
+5. Switch to the **Application Pipeline** tab and switch to **Kanban view**.
+6. Resize your browser down to mobile width (or open on your phone) and verify the columns scroll smoothly horizontally without clipping.
+
+---
+
+When all 6 steps are complete, JobHelperGuru is officially public-launch ready!
From 2bc6858cbcc6887046f825772cdd34d907bc0af0 Mon Sep 17 00:00:00 2001
From: Sourav Chandhok <114119117+souravC01@users.noreply.github.com>
Date: Sat, 12 Sep 2026 01:32:50 -0400
Subject: [PATCH 13/14] fix(storage): Support STORAGE_BACKEND=local and improve
upload diagnostic logging
---
backend/main.py | 6 ++++--
backend/routers/auth.py | 11 +++++++++--
backend/services/email_service.py | 7 +++++++
backend/services/object_storage.py | 5 ++++-
4 files changed, 24 insertions(+), 5 deletions(-)
diff --git a/backend/main.py b/backend/main.py
index 99f0d1e..771d95a 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -450,9 +450,11 @@ def upload_resume_file(
)
except ValueError as ve:
raise HTTPException(status_code=422, detail=str(ve))
- except RuntimeError:
- raise HTTPException(status_code=503, detail="Storage service temporarily unavailable.")
+ except RuntimeError as re:
+ print(f"[RESUME UPLOAD ERROR] {re}")
+ raise HTTPException(status_code=503, detail=f"Storage service temporarily unavailable: {str(re)}")
except Exception as e:
+ print(f"[RESUME UPLOAD ERROR] {e}")
raise HTTPException(status_code=500, detail=f"Failed to save resume record: {str(e)}")
resume.download_url = f"/api/resumes/{resume.id}/download"
diff --git a/backend/routers/auth.py b/backend/routers/auth.py
index e473391..96e1215 100644
--- a/backend/routers/auth.py
+++ b/backend/routers/auth.py
@@ -236,10 +236,12 @@ def register(req: UserRegisterRequest, request: Request):
expires_at=expires_at,
)
+ print(f"\n[AUTH] Generated verification link for {user.email}:\n{cfg.app_url}/verify-email?token={verify_token}\n")
try:
email_service.send_verification(recipient=user.email, token=verify_token, app_url=cfg.app_url)
+ print(f"[AUTH] Successfully dispatched verification email to {user.email}")
except Exception as e:
- print(f"[WARN] Failed to send verification email: {e}")
+ print(f"[AUTH ERROR] Failed to send verification email: {e}")
if cfg.require_email_verification:
return AuthResponse(
@@ -404,10 +406,15 @@ def request_password_reset(req: PasswordResetRequest, request: Request):
expires_at = (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat()
storage.invalidate_prior_auth_tokens(raw_user["id"], "reset_password")
storage.create_auth_token(raw_user["id"], token_hash, "reset_password", expires_at)
+
+ print(f"\n[AUTH] Generated password reset link for {email}:\n{cfg.app_url}/reset-password?token={reset_token}\n")
try:
email_service.send_password_reset(recipient=email, token=reset_token, app_url=cfg.app_url)
+ print(f"[AUTH] Successfully dispatched password reset email to {email}")
except Exception as e:
- print(f"[WARN] Failed to send password reset email: {e}")
+ print(f"[AUTH ERROR] Failed to send password reset email: {e}")
+ else:
+ print(f"\n[AUTH] Password reset requested for '{email}', but no user account exists with this email address in the database.\n")
# Generic response to prevent account enumeration
return {"message": "If an account exists with that email, a password reset link has been sent."}
diff --git a/backend/services/email_service.py b/backend/services/email_service.py
index 9c63ff3..159dad0 100644
--- a/backend/services/email_service.py
+++ b/backend/services/email_service.py
@@ -99,6 +99,13 @@ def send(self, recipient: str, subject: str, text_body: str) -> None:
import urllib.request
import urllib.error
+ if self.api_key.startswith("xsmtpsib-"):
+ raise ValueError(
+ "Configured BREVO_API_KEY starts with 'xsmtpsib-', which is an SMTP password, not a REST API key. "
+ "The Brevo REST API requires an API key starting with 'xkeysib-'. "
+ "In your Brevo dashboard, go to: Profile (top right) -> SMTP & API -> API Keys tab -> Generate a new API key."
+ )
+
payload = {
"sender": {"name": self.sender_name, "email": self.sender_email},
"to": [{"email": recipient}],
diff --git a/backend/services/object_storage.py b/backend/services/object_storage.py
index a73af2d..955f597 100644
--- a/backend/services/object_storage.py
+++ b/backend/services/object_storage.py
@@ -48,8 +48,11 @@ def __init__(
self.is_configured = False
self._upload_dir = Path(upload_dir or "data/uploads").resolve()
+ backend_choice = (os.getenv("STORAGE_BACKEND") or "").strip().lower()
+
if (
- BOTO3_AVAILABLE
+ backend_choice != "local"
+ and BOTO3_AVAILABLE
and self.access_key_id
and self.secret_access_key
and self.endpoint_url
From a7829bfbd7497a21ea6eed879cb29357dfa52622 Mon Sep 17 00:00:00 2001
From: Sourav Chandhok <114119117+souravC01@users.noreply.github.com>
Date: Sat, 12 Sep 2026 01:42:56 -0400
Subject: [PATCH 14/14] fix(env): ensure project .env overrides stale parent
shell variables
---
backend/main.py | 3 +++
backend/services/object_storage.py | 2 +-
backend/storage.py | 2 +-
3 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/backend/main.py b/backend/main.py
index 771d95a..17cc437 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -2,6 +2,9 @@
import re
from pathlib import Path
from typing import Optional, List
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
from fastapi import FastAPI, HTTPException, Response, Depends, UploadFile, File, Form, Request
from fastapi.responses import RedirectResponse, FileResponse
diff --git a/backend/services/object_storage.py b/backend/services/object_storage.py
index 955f597..89e9cd6 100644
--- a/backend/services/object_storage.py
+++ b/backend/services/object_storage.py
@@ -5,7 +5,7 @@
from typing import Optional, Union
from dotenv import load_dotenv
-load_dotenv()
+load_dotenv(override=True)
try:
import boto3
diff --git a/backend/storage.py b/backend/storage.py
index 9ebafe6..8fe58a4 100644
--- a/backend/storage.py
+++ b/backend/storage.py
@@ -9,7 +9,7 @@
from typing import List, Optional, Dict, Any
from dotenv import load_dotenv
-load_dotenv()
+load_dotenv(override=True)
try:
import psycopg2