Developed Settings Page and Job Forms Features.#55
Conversation
📝 WalkthroughWalkthroughAdds a settings API (view/update/change password) and a job-forms subsystem (models, controllers, routes, public submission), extends the User model with profile and notification fields, and wires client-side APIs, pages, and storage-event signaling to the new endpoints. Changes
Sequence Diagram(s)sequenceDiagram
actor PublicUser as Public User
participant Frontend as Frontend App
participant Backend as Backend API
participant DB as Database
PublicUser->>Frontend: Open /apply/:formId
Frontend->>Backend: GET /api/public/forms/:formId
Backend->>DB: Find JobForm by _id
DB-->>Backend: Return JobForm
Backend-->>Frontend: Send form definition
Frontend->>PublicUser: Render form
PublicUser->>Frontend: Submit form
Frontend->>Frontend: Validate required fields
Frontend->>Backend: POST /api/public/forms/:formId/submit
Backend->>Backend: Sanitize, set submitterEmail/ip
Backend->>DB: Create FormSubmission
DB-->>Backend: Return submission id
Backend->>DB: Increment JobForm.submissionCount
Backend-->>Frontend: Return success + submissionId
Frontend->>PublicUser: Show success confirmation
sequenceDiagram
actor AuthUser as Authenticated User
participant Frontend as Frontend App
participant Backend as Backend API
participant DB as Database
AuthUser->>Frontend: Open /settings
Frontend->>Backend: GET /api/settings
Backend->>DB: Find User by _id
DB-->>Backend: Return user
Backend-->>Frontend: Return mapped user (with notificationPreferences)
Frontend->>AuthUser: Populate settings form
AuthUser->>Frontend: Edit profile & notifications
Frontend->>Backend: PUT /api/settings
Backend->>Backend: Validate email, check uniqueness
Backend->>DB: Update User
DB-->>Backend: Return updated user
Backend-->>Frontend: Return updated user
Frontend->>Window: Dispatch USER_UPDATED_EVENT
Frontend->>AuthUser: Show success
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (4)
backend/dashboard/src/server.js (1)
1-2: Avoid hardcoding DNS servers—make this configurable or remove.Hardcoding Google's DNS servers (
8.8.8.8,8.8.4.4) can cause issues in environments where these are blocked (corporate networks, certain regions). This also overrides system DNS configuration without a fallback.Consider making this configurable via environment variables or removing it if not strictly necessary:
♻️ Proposed fix
const dns = require("dns"); -dns.setServers(["8.8.8.8", "8.8.4.4"]); +if (process.env.DNS_SERVERS) { + dns.setServers(process.env.DNS_SERVERS.split(",")); +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/dashboard/src/server.js` around lines 1 - 2, The dns.setServers call currently hardcodes Google's DNS; change it to be configurable: read a DNS_SERVERS environment variable (comma-separated IPs), validate/trim into an array and call dns.setServers(parsedArray) only if DNS_SERVERS is set and non-empty; otherwise remove the dns.setServers call so system defaults are used; reference the dns module and the setServers usage in server.js and add a short console.warn/log when custom servers are applied or when an invalid value is ignored.backend/dashboard/src/models/JobForm.js (2)
25-25: Potential race condition withsubmissionCountincrement.If
submissionCountis incremented viafindByIdAndUpdatewith$inc, concurrent submissions could still lead to inconsistent counts if not handled atomically. Consider using MongoDB's atomic$incoperator consistently, or alternatively, compute the count dynamically fromFormSubmissiondocuments when needed (trading storage for accuracy).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/dashboard/src/models/JobForm.js` at line 25, submissionCount on the JobForm model can become inconsistent under concurrent updates; ensure increments use MongoDB's atomic $inc operator (e.g., when updating in handlers that currently call findByIdAndUpdate) or remove the stored submissionCount and derive it on demand by counting FormSubmission documents (use FormSubmission.countDocuments({ formId }))—pick one approach, update all code paths that modify submissionCount (references to submissionCount and findByIdAndUpdate) to use $inc atomically or switch callers to compute the count dynamically from FormSubmission.
7-11: Consider adding an index oncreatedByfor query performance.Queries filtering job forms by creator will benefit from an index. You can add it to the schema:
♻️ Proposed addition after the schema definition
+jobFormSchema.index({ createdBy: 1 }); + module.exports = mongoose.model("JobForm", jobFormSchema);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/dashboard/src/models/JobForm.js` around lines 7 - 11, Add a single-field index on the createdBy field of the JobForm schema to improve query performance when filtering by creator: after the JobForm schema definition (the schema that defines createdBy: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }), call schema.index({ createdBy: 1 }) (or the equivalent on the JobForm schema variable) so mongoose creates an index for createdBy; keep the index creation adjacent to the schema definition/export to ensure it is applied.backend/dashboard/src/models/FormSubmission.js (1)
5-9: Consider adding indexes onformIdandsubmitterEmailfor query performance.Queries listing submissions by form or looking up by email will benefit from indexes:
♻️ Proposed addition after the schema definition
+formSubmissionSchema.index({ formId: 1 }); +formSubmissionSchema.index({ submitterEmail: 1 }); + module.exports = mongoose.model("FormSubmission", formSubmissionSchema);Also applies to: 15-20
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/dashboard/src/models/FormSubmission.js` around lines 5 - 9, Add indexes on the FormSubmission schema for faster lookups by form and email: update the schema that defines formId and submitterEmail (fields named formId and submitterEmail in the FormSubmission model) to create indexes for those fields—either set index: true on the submitterEmail and/or formId field definitions or call schema.index({ formId: 1 }) and schema.index({ submitterEmail: 1 }) after the schema is defined; ensure the indexes are created before exporting the FormSubmission model.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/dashboard/src/controllers/dashboardController.js`:
- Around line 149-152: The code persists the plaintext newPassword to
user.visiblePassword after hashing (bcrypt.genSalt / bcrypt.hash and user.save),
which exposes credentials; remove the assignment to user.visiblePassword and any
DB column that stores plaintext passwords, and instead implement/trigger a
password reset flow (e.g., generate a one-time reset token, store only a hashed
token and expiry on the user record, send reset link to the user) or store only
metadata (lastPasswordChange timestamp) if you need auditability; ensure only
the bcrypt-hashed password is saved before calling user.save().
In `@backend/dashboard/src/controllers/jobFormController.js`:
- Around line 71-85: The public read handler currently returns forms regardless
of active state; update the handler that builds and returns publicForm to reject
inactive forms by checking the form's activity flag (e.g., form.active or
form.isActive, or form.status === 'inactive') and return a 404 (or same error
used elsewhere) if the form is not active before constructing publicForm; note
submitFormResponse already blocks inactive forms so mirror that same
active-check logic here to prevent closed forms from being served to
/apply/[formId].
- Around line 186-193: The submitterEmail is hard-coded to submittedData.email
but field labels are arbitrary; update the submitterEmail logic to derive the
email by scanning submittedData for an email-typed field or a value matching an
email regex (e.g., check each entry: if field.type === 'email' use its value;
otherwise if the value string matches an email pattern use that), then normalize
with .toLowerCase().trim(); fall back to "unknown@example.com" only if no
candidate is found; apply this change where submitterEmail is constructed before
creating the FormSubmission.
- Around line 195-199: The current code does submission.save() then mutates
form.submissionCount and calls form.save(), which races and can lose increments;
instead perform an atomic increment on the Form collection using the $inc
operator (e.g., call the Form model's updateOne/findByIdAndUpdate with {$inc:
{submissionCount: 1}} targeting form._id) rather than modifying
form.submissionCount and calling form.save(); leave submission.save() as-is but
replace the form.save() block with the atomic update to ensure concurrent
submissions can't drop counts.
In `@backend/dashboard/src/models/FormSubmission.js`:
- Line 28: The FormSubmission model currently stores raw IPs in the ipAddress
field; update the implementation and docs to meet privacy requirements by either
removing the ipAddress field from the schema or implementing
anonymization/short-term retention: add a clear retention policy in your privacy
documentation and code (e.g., a TTL cleanup or a post-save middleware that
hashes/anonymizes ipAddress after X days), ensure the FormSubmission schema and
any create/update flows reference the new behavior (ipAddress → anonymizedIp or
removed), and update any related tests and API docs to reflect that raw IPs are
not retained indefinitely.
In `@backend/dashboard/src/models/User.js`:
- Line 42: Remove the insecure visiblePassword property from the User schema
(delete the visiblePassword field declaration in the User model) and remove any
code that assigns to it: delete the assignment user.visiblePassword =
newPassword and remove visiblePassword: password in any user creation/update
object literals; ensure the code continues to use bcrypt-hashed passwords only
(e.g., keep/verify bcrypt.hash and bcrypt.compare usage in the password
update/create flows) so plaintext passwords are never stored or persisted.
In `@backend/package.json`:
- Line 22: The package.json dependency "sqlite3" was bumped to "^6.0.1", which
is marked unmaintained and introduces breaking changes (Node >=20.17.0
requirement, dropped win32-ia32 binaries, SQLite engine bump); either revert/pin
"sqlite3" back to the last known working version (e.g., "^5.1.7") or migrate the
backend/desktop DB code to a maintained driver (e.g., "better-sqlite3" or
"sql.js"); if you choose to keep 6.x, update project Node engine checks and CI
to ensure Node >=20.17.0 and verify desktop Windows 32-bit support is
acceptable—search for "sqlite3" in package.json and DB initialization code (DB
open/prepare/serialize methods) to apply the change and update any API
differences accordingly.
In `@reqruita-web-dashboard/src/app/`(dashboard)/job-forms/page.tsx:
- Around line 142-149: fetchSubmissions currently reads sort/filter from state,
so calls immediately after setSortBy/setFilterStatus use stale values; update
fetchSubmissions(formId, sortByOverride?, filterStatusOverride?) to accept
optional overrides and use those instead of state when provided, then change the
call sites that setSortBy or setFilterStatus (and any other places that update
sort/filter and then call fetchSubmissions) to pass the newly selected values
(e.g., fetchSubmissions(selectedFormId, newSortBy, newFilterStatus)) so the
modal refreshes with the selected options rather than the previous state.
- Around line 158-188: Add an "in-flight" guard state (e.g., isSubmitting)
around async form mutations to prevent repeated clicks: in handleCreateForm (and
the analogous update handler for updateJobForm) return early if isSubmitting is
true, set isSubmitting = true before calling createJobForm/updateJobForm, and
always reset isSubmitting = false in a finally block; also tie the modal primary
action (submit button) to this state so it is disabled while submitting and show
a loading indicator to block double submissions and concurrent updates.
In `@reqruita-web-dashboard/src/lib/api.ts`:
- Around line 331-334: The error handling currently only reads (data as
ApiError).message when responses are not ok; update each failing-response branch
(the blocks that call const data = await res.json(); if (!res.ok) { throw new
Error(...) }) to check both data.message and data.error (e.g. (data as
ApiError).message || (data as any).error) and use that value in the thrown
Error, falling back to the existing generic text like "Failed to create job
form"; apply the same change to the other similar blocks noted (the other
occurrences that follow the same pattern).
- Around line 432-448: The submitJobFormResponse helper currently
JSON-stringifies FormSubmissionPayload so any File values are lost; change
submitJobFormResponse to detect if submittedData contains any File instances
and, if so, build a FormData instead of JSON: for each key in submittedData
append File objects and append string values normally, omit setting the
"Content-Type" header so the browser adds the correct multipart boundary, and
send the FormData as the request body; otherwise keep the existing JSON path.
Update references to FormSubmissionPayload and submitJobFormResponse
accordingly.
---
Nitpick comments:
In `@backend/dashboard/src/models/FormSubmission.js`:
- Around line 5-9: Add indexes on the FormSubmission schema for faster lookups
by form and email: update the schema that defines formId and submitterEmail
(fields named formId and submitterEmail in the FormSubmission model) to create
indexes for those fields—either set index: true on the submitterEmail and/or
formId field definitions or call schema.index({ formId: 1 }) and schema.index({
submitterEmail: 1 }) after the schema is defined; ensure the indexes are created
before exporting the FormSubmission model.
In `@backend/dashboard/src/models/JobForm.js`:
- Line 25: submissionCount on the JobForm model can become inconsistent under
concurrent updates; ensure increments use MongoDB's atomic $inc operator (e.g.,
when updating in handlers that currently call findByIdAndUpdate) or remove the
stored submissionCount and derive it on demand by counting FormSubmission
documents (use FormSubmission.countDocuments({ formId }))—pick one approach,
update all code paths that modify submissionCount (references to submissionCount
and findByIdAndUpdate) to use $inc atomically or switch callers to compute the
count dynamically from FormSubmission.
- Around line 7-11: Add a single-field index on the createdBy field of the
JobForm schema to improve query performance when filtering by creator: after the
JobForm schema definition (the schema that defines createdBy: { type:
mongoose.Schema.Types.ObjectId, ref: "User", required: true }), call
schema.index({ createdBy: 1 }) (or the equivalent on the JobForm schema
variable) so mongoose creates an index for createdBy; keep the index creation
adjacent to the schema definition/export to ensure it is applied.
In `@backend/dashboard/src/server.js`:
- Around line 1-2: The dns.setServers call currently hardcodes Google's DNS;
change it to be configurable: read a DNS_SERVERS environment variable
(comma-separated IPs), validate/trim into an array and call
dns.setServers(parsedArray) only if DNS_SERVERS is set and non-empty; otherwise
remove the dns.setServers call so system defaults are used; reference the dns
module and the setServers usage in server.js and add a short console.warn/log
when custom servers are applied or when an invalid value is ignored.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 7fa0c33e-47de-4c2d-a43c-b23dc84886c8
⛔ Files ignored due to path filters (2)
backend/package-lock.jsonis excluded by!**/package-lock.jsonreqruita-web-dashboard/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (16)
backend/dashboard/src/controllers/dashboardController.jsbackend/dashboard/src/controllers/jobFormController.jsbackend/dashboard/src/models/FormSubmission.jsbackend/dashboard/src/models/JobForm.jsbackend/dashboard/src/models/User.jsbackend/dashboard/src/routes/dashboardRoutes.jsbackend/dashboard/src/routes/jobFormRoutes.jsbackend/dashboard/src/server.jsbackend/package.jsonreqruita-web-dashboard/package.jsonreqruita-web-dashboard/src/app/(dashboard)/job-forms/page.tsxreqruita-web-dashboard/src/app/(dashboard)/layout.tsxreqruita-web-dashboard/src/app/(dashboard)/settings/page.tsxreqruita-web-dashboard/src/app/apply/[formId]/page.tsxreqruita-web-dashboard/src/app/signup/page.tsxreqruita-web-dashboard/src/lib/api.ts
| if (!form) { | ||
| return res.status(404).json({ error: "Form not found" }); | ||
| } | ||
|
|
||
| // Don't expose createdBy email for public view | ||
| const publicForm = { | ||
| id: form._id, | ||
| title: form.title, | ||
| description: form.description, | ||
| jobRole: form.jobRole, | ||
| fields: form.fields, | ||
| company: form.createdBy?.companyName || "Our Company", | ||
| }; | ||
|
|
||
| res.json(publicForm); |
There was a problem hiding this comment.
Stop serving inactive forms from the public read endpoint.
submitFormResponse blocks inactive forms, but this handler still returns them. The new /apply/[formId] page will let candidates fill a closed form and only fail on submit.
🔧 Suggested fix
if (!form) {
return res.status(404).json({ error: "Form not found" });
}
+ if (!form.isActive) {
+ return res.status(404).json({ error: "Form not found" });
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!form) { | |
| return res.status(404).json({ error: "Form not found" }); | |
| } | |
| // Don't expose createdBy email for public view | |
| const publicForm = { | |
| id: form._id, | |
| title: form.title, | |
| description: form.description, | |
| jobRole: form.jobRole, | |
| fields: form.fields, | |
| company: form.createdBy?.companyName || "Our Company", | |
| }; | |
| res.json(publicForm); | |
| if (!form) { | |
| return res.status(404).json({ error: "Form not found" }); | |
| } | |
| if (!form.isActive) { | |
| return res.status(404).json({ error: "Form not found" }); | |
| } | |
| // Don't expose createdBy email for public view | |
| const publicForm = { | |
| id: form._id, | |
| title: form.title, | |
| description: form.description, | |
| jobRole: form.jobRole, | |
| fields: form.fields, | |
| company: form.createdBy?.companyName || "Our Company", | |
| }; | |
| res.json(publicForm); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/dashboard/src/controllers/jobFormController.js` around lines 71 - 85,
The public read handler currently returns forms regardless of active state;
update the handler that builds and returns publicForm to reject inactive forms
by checking the form's activity flag (e.g., form.active or form.isActive, or
form.status === 'inactive') and return a 404 (or same error used elsewhere) if
the form is not active before constructing publicForm; note submitFormResponse
already blocks inactive forms so mirror that same active-check logic here to
prevent closed forms from being served to /apply/[formId].
| const submitterEmail = submittedData.email || "unknown@example.com"; | ||
|
|
||
| const submission = new FormSubmission({ | ||
| formId, | ||
| submittedData, | ||
| submitterEmail: submitterEmail.toLowerCase().trim(), | ||
| ipAddress: req.ip || req.connection.remoteAddress || "", | ||
| }); |
There was a problem hiding this comment.
Derive the submitter email from the email-typed field, not a hard-coded key.
The builder in reqruita-web-dashboard/src/app/(dashboard)/job-forms/page.tsx lets recruiters label an email field arbitrarily, but this only reads submittedData.email. A field like Work Email will be stored as unknown@example.com, which breaks the submissions tables.
🔧 Suggested fix
- const submitterEmail = submittedData.email || "unknown@example.com";
+ const emailField = form.fields.find((field) => field.type === "email");
+ const rawSubmitterEmail =
+ emailField && typeof submittedData[emailField.label] === "string"
+ ? submittedData[emailField.label]
+ : submittedData.email;
+ const submitterEmail =
+ typeof rawSubmitterEmail === "string" && rawSubmitterEmail.trim()
+ ? rawSubmitterEmail.toLowerCase().trim()
+ : "unknown@example.com";
const submission = new FormSubmission({
formId,
submittedData,
- submitterEmail: submitterEmail.toLowerCase().trim(),
+ submitterEmail,
ipAddress: req.ip || req.connection.remoteAddress || "",
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/dashboard/src/controllers/jobFormController.js` around lines 186 -
193, The submitterEmail is hard-coded to submittedData.email but field labels
are arbitrary; update the submitterEmail logic to derive the email by scanning
submittedData for an email-typed field or a value matching an email regex (e.g.,
check each entry: if field.type === 'email' use its value; otherwise if the
value string matches an email pattern use that), then normalize with
.toLowerCase().trim(); fall back to "unknown@example.com" only if no candidate
is found; apply this change where submitterEmail is constructed before creating
the FormSubmission.
| await submission.save(); | ||
|
|
||
| // Update submission count | ||
| form.submissionCount += 1; | ||
| await form.save(); |
There was a problem hiding this comment.
Increment submissionCount atomically.
form.submissionCount += 1; await form.save() loses updates when two submissions land at the same time. The counter can drift even though both FormSubmission documents were saved.
🔧 Suggested fix
- // Update submission count
- form.submissionCount += 1;
- await form.save();
+ await JobForm.findByIdAndUpdate(formId, {
+ $inc: { submissionCount: 1 },
+ });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/dashboard/src/controllers/jobFormController.js` around lines 195 -
199, The current code does submission.save() then mutates form.submissionCount
and calls form.save(), which races and can lose increments; instead perform an
atomic increment on the Form collection using the $inc operator (e.g., call the
Form model's updateOne/findByIdAndUpdate with {$inc: {submissionCount: 1}}
targeting form._id) rather than modifying form.submissionCount and calling
form.save(); leave submission.save() as-is but replace the form.save() block
with the atomic update to ensure concurrent submissions can't drop counts.
| }, | ||
| notes: { type: String, default: "" }, | ||
| rating: { type: Number, min: 0, max: 5, default: 0 }, | ||
| ipAddress: { type: String, default: "" }, |
There was a problem hiding this comment.
Privacy consideration: document IP address retention policy.
Storing ipAddress may have GDPR/CCPA implications. Ensure you have a documented retention policy and that users are informed via your privacy policy. Consider whether this field is necessary or if it should be anonymized/hashed after a certain period.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/dashboard/src/models/FormSubmission.js` at line 28, The
FormSubmission model currently stores raw IPs in the ipAddress field; update the
implementation and docs to meet privacy requirements by either removing the
ipAddress field from the schema or implementing anonymization/short-term
retention: add a clear retention policy in your privacy documentation and code
(e.g., a TTL cleanup or a post-save middleware that hashes/anonymizes ipAddress
after X days), ensure the FormSubmission schema and any create/update flows
reference the new behavior (ipAddress → anonymizedIp or removed), and update any
related tests and API docs to reflect that raw IPs are not retained
indefinitely.
| "socket.io": "^4.8.3", | ||
| "socket.io-client": "^4.8.3", | ||
| "sqlite3": "^5.1.7" | ||
| "sqlite3": "^6.0.1" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for sqlite3 imports/requires in the backend
rg -n "require\(['\"]sqlite3['\"]|from ['\"]sqlite3['\"]" backend/
# Search for any sqlite-related code patterns
rg -n -i "sqlite|\.db\b" backend/ --type jsRepository: Thenuja-Hansana/Reqruita-CS80
Length of output: 1803
🌐 Web query:
What are the breaking changes in sqlite3 npm package version 6.0.0?
💡 Result:
The sqlite3 npm package v6.0.0 (TryGhost/node-sqlite3) has these breaking changes:
- Node.js minimum supported version is now
>= 20.17.0(previously v10). This is enforced viaengines.node. [2] - 32-bit Windows (
win32-ia32) prebuilt binaries were dropped (Node 24 no longer ships 32-bit Windows), so installs on that platform will now require a different approach (or will fail). [2]
Notable major-version changes that may impact behavior (but aren’t necessarily “API breaks” in the JS surface):
- Bundled SQLite was upgraded from 3.45.0 to 3.52.0, which can change SQL engine behavior/semantics in edge cases (upstream changes). [2]
- The repo was also marked “unmaintained” in the release notes (not a runtime break, but an important lifecycle change). [1]
Sources: v6.0.0 release notes [1], and the v6.0.0 PR summary detailing the breaking Node/platform support changes [2].
References:
[1] (github.com)
[2] (github.com)
Citations:
- 1: https://github.com/TryGhost/node-sqlite3/releases
- 2: v6.0.0 — Bump all dependencies, SQLite, and modernise CI TryGhost/node-sqlite3#1857
Address breaking changes in sqlite3 v6.0.1 upgrade—package is now unmaintained.
The sqlite3 upgrade from ^5.1.7 to ^6.0.1 introduces breaking changes that affect the desktop app (which actively uses it):
- Node.js requirement changed to >= 20.17.0 (previously v10+)—verify the backend's Node.js version matches this constraint
- 32-bit Windows support dropped—binaries no longer available for
win32-ia32 - SQLite engine upgraded from 3.45.0 to 3.52.0—potential behavior changes in edge cases
- Critical: The sqlite3 npm package is marked "unmaintained"—consider migrating to an actively maintained SQLite driver (
better-sqlite3,sql.js, or similar) to avoid future security risks and missing updates
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/package.json` at line 22, The package.json dependency "sqlite3" was
bumped to "^6.0.1", which is marked unmaintained and introduces breaking changes
(Node >=20.17.0 requirement, dropped win32-ia32 binaries, SQLite engine bump);
either revert/pin "sqlite3" back to the last known working version (e.g.,
"^5.1.7") or migrate the backend/desktop DB code to a maintained driver (e.g.,
"better-sqlite3" or "sql.js"); if you choose to keep 6.x, update project Node
engine checks and CI to ensure Node >=20.17.0 and verify desktop Windows 32-bit
support is acceptable—search for "sqlite3" in package.json and DB initialization
code (DB open/prepare/serialize methods) to apply the change and update any API
differences accordingly.
| const fetchSubmissions = async (formId: string) => { | ||
| try { | ||
| setLoadingSubmissions(true); | ||
| const { submissions: data } = await getFormSubmissions(formId, { | ||
| sortBy, | ||
| status: filterStatus === "all" ? undefined : filterStatus, | ||
| }); | ||
| setSubmissions(data); |
There was a problem hiding this comment.
Pass the newly selected sort/filter into fetchSubmissions.
setSortBy and setFilterStatus are async, so the immediate fetchSubmissions(selectedFormId) call still reads the previous state. The submissions modal ends up one selection behind while the "All Submissions" table refreshes with the new values.
🔧 Suggested fix
- const fetchSubmissions = async (formId: string) => {
+ const fetchSubmissions = async (
+ formId: string,
+ nextOptions?: { sortBy?: "latest" | "oldest"; status?: string },
+ ) => {
+ const nextSortBy = nextOptions?.sortBy ?? sortBy;
+ const nextStatus = nextOptions?.status ?? filterStatus;
try {
setLoadingSubmissions(true);
const { submissions: data } = await getFormSubmissions(formId, {
- sortBy,
- status: filterStatus === "all" ? undefined : filterStatus,
+ sortBy: nextSortBy,
+ status: nextStatus === "all" ? undefined : nextStatus,
});
setSubmissions(data);
setSelectedFormId(formId);- onChange={(e) => {
- setFilterStatus(e.target.value);
- if (selectedFormId) {
- fetchSubmissions(selectedFormId);
- }
- }}
+ onChange={(e) => {
+ const nextStatus = e.target.value;
+ setFilterStatus(nextStatus);
+ if (selectedFormId) {
+ fetchSubmissions(selectedFormId, { status: nextStatus });
+ }
+ }}Also applies to: 778-808
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@reqruita-web-dashboard/src/app/`(dashboard)/job-forms/page.tsx around lines
142 - 149, fetchSubmissions currently reads sort/filter from state, so calls
immediately after setSortBy/setFilterStatus use stale values; update
fetchSubmissions(formId, sortByOverride?, filterStatusOverride?) to accept
optional overrides and use those instead of state when provided, then change the
call sites that setSortBy or setFilterStatus (and any other places that update
sort/filter and then call fetchSubmissions) to pass the newly selected values
(e.g., fetchSubmissions(selectedFormId, newSortBy, newFilterStatus)) so the
modal refreshes with the selected options rather than the previous state.
| const handleCreateForm = async () => { | ||
| if (!newFormTitle.trim()) { | ||
| alert("Form title is required"); | ||
| return; | ||
| } | ||
|
|
||
| const handleDragStartEditField = ( | ||
| event: DragEvent<HTMLButtonElement>, | ||
| sourceIndex: number, | ||
| ) => { | ||
| event.dataTransfer.setData("text/plain", sourceIndex.toString()); | ||
| event.dataTransfer.effectAllowed = "move"; | ||
| }; | ||
| const sanitizedFields = newFormFields | ||
| .map((f) => ({ ...f, label: f.label.trim() })) | ||
| .filter((f) => f.label.length > 0); | ||
|
|
||
| const handleDropEditField = ( | ||
| event: DragEvent<HTMLDivElement>, | ||
| targetIndex: number, | ||
| ) => { | ||
| event.preventDefault(); | ||
| const sourceIndex = Number(event.dataTransfer.getData("text/plain")); | ||
| try { | ||
| await createJobForm({ | ||
| title: newFormTitle, | ||
| description: newFormDescription, | ||
| jobRole: newFormJobRole, | ||
| fields: sanitizedFields, | ||
| }); | ||
|
|
||
| if ( | ||
| Number.isNaN(sourceIndex) || | ||
| sourceIndex < 0 || | ||
| sourceIndex >= editTemplateFields.length || | ||
| sourceIndex === targetIndex | ||
| ) { | ||
| return; | ||
| // Reset form and reload | ||
| setNewFormTitle(""); | ||
| setNewFormDescription(""); | ||
| setNewFormJobRole(""); | ||
| setNewFormFields([]); | ||
| setShowCreateModal(false); | ||
| await fetchAllForms(); | ||
| } catch (error) { | ||
| alert( | ||
| `Failed to create form: ${error instanceof Error ? error.message : "Unknown error"}`, | ||
| ); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Guard the form mutations against repeated clicks.
There is no in-flight guard around createJobForm / updateJobForm, and the primary modal action stays enabled while the request is pending. A double-click can create duplicate forms or send concurrent updates.
🔧 Suggested fix
+ const [savingForm, setSavingForm] = useState(false);
+
const handleCreateForm = async () => {
+ if (savingForm) return;
if (!newFormTitle.trim()) {
alert("Form title is required");
return;
}
const sanitizedFields = newFormFields
.map((f) => ({ ...f, label: f.label.trim() }))
.filter((f) => f.label.length > 0);
try {
+ setSavingForm(true);
await createJobForm({
title: newFormTitle,
description: newFormDescription,
jobRole: newFormJobRole,
fields: sanitizedFields,
@@
} catch (error) {
alert(
`Failed to create form: ${error instanceof Error ? error.message : "Unknown error"}`,
);
+ } finally {
+ setSavingForm(false);
}
};
const handleSaveEditForm = async () => {
+ if (savingForm) return;
if (!selectedTemplate || !editFormTitle.trim()) {
alert("Form title is required");
return;
}
@@
try {
+ setSavingForm(true);
await updateJobForm(selectedTemplate._id, {
title: editFormTitle,
description: editFormDescription,
jobRole: editFormJobRole,
fields: sanitizedFields,
@@
} catch (error) {
alert(
`Failed to update form: ${error instanceof Error ? error.message : "Unknown error"}`,
);
+ } finally {
+ setSavingForm(false);
}
};- <button
- onClick={isEdit ? handleSaveEditForm : handleCreateForm}
+ <button
+ onClick={isEdit ? handleSaveEditForm : handleCreateForm}
+ disabled={savingForm}
className="flex-1 bg-purple-600 hover:bg-purple-700 text-white py-3 rounded-xl font-semibold transition active:scale-95 shadow-lg hover:shadow-xl"
>Also applies to: 199-225, 549-553
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@reqruita-web-dashboard/src/app/`(dashboard)/job-forms/page.tsx around lines
158 - 188, Add an "in-flight" guard state (e.g., isSubmitting) around async form
mutations to prevent repeated clicks: in handleCreateForm (and the analogous
update handler for updateJobForm) return early if isSubmitting is true, set
isSubmitting = true before calling createJobForm/updateJobForm, and always reset
isSubmitting = false in a finally block; also tie the modal primary action
(submit button) to this state so it is disabled while submitting and show a
loading indicator to block double submissions and concurrent updates.
| const data = await res.json(); | ||
| if (!res.ok) { | ||
| throw new Error((data as ApiError).message || "Failed to create job form"); | ||
| } |
There was a problem hiding this comment.
Read both message and error from failed responses.
The new job-form controllers return { error: ... } on failure, but these helpers only look at data.message. Users will get generic fallbacks instead of actionable errors like "Form not found" or "This form is no longer accepting submissions".
🔧 Suggested fix
+function getApiErrorMessage(data: unknown, fallback: string): string {
+ if (data && typeof data === "object") {
+ const payload = data as { message?: string; error?: string };
+ return payload.message || payload.error || fallback;
+ }
+ return fallback;
+}
+
export async function submitJobFormResponse(
formId: string,
submittedData: FormSubmissionPayload,
): Promise<{ message: string; submissionId: string }> {
@@
const data = await res.json();
if (!res.ok) {
- throw new Error((data as ApiError).message || "Failed to submit form");
+ throw new Error(getApiErrorMessage(data, "Failed to submit form"));
}Also applies to: 355-358, 377-380, 403-406, 424-427, 451-454, 506-510, 540-544
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@reqruita-web-dashboard/src/lib/api.ts` around lines 331 - 334, The error
handling currently only reads (data as ApiError).message when responses are not
ok; update each failing-response branch (the blocks that call const data = await
res.json(); if (!res.ok) { throw new Error(...) }) to check both data.message
and data.error (e.g. (data as ApiError).message || (data as any).error) and use
that value in the thrown Error, falling back to the existing generic text like
"Failed to create job form"; apply the same change to the other similar blocks
noted (the other occurrences that follow the same pattern).
| export interface FormSubmissionPayload { | ||
| [key: string]: string | File | undefined; | ||
| } | ||
|
|
||
| export async function submitJobFormResponse( | ||
| formId: string, | ||
| submittedData: FormSubmissionPayload, | ||
| ): Promise<{ message: string; submissionId: string }> { | ||
| const res = await fetch( | ||
| `${AUTH_API_BASE}/api/public/forms/${formId}/submit`, | ||
| { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ submittedData }), | ||
| }, |
There was a problem hiding this comment.
File values are not actually supported by this transport.
This helper posts JSON, so any File in submittedData is serialized away. Combined with reqruita-web-dashboard/src/app/apply/[formId]/page.tsx Lines 298-348, the new upload UI submits only the filename string, not the document itself.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@reqruita-web-dashboard/src/lib/api.ts` around lines 432 - 448, The
submitJobFormResponse helper currently JSON-stringifies FormSubmissionPayload so
any File values are lost; change submitJobFormResponse to detect if
submittedData contains any File instances and, if so, build a FormData instead
of JSON: for each key in submittedData append File objects and append string
values normally, omit setting the "Content-Type" header so the browser adds the
correct multipart boundary, and send the FormData as the request body; otherwise
keep the existing JSON path. Update references to FormSubmissionPayload and
submitJobFormResponse accordingly.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
backend/dashboard/src/controllers/dashboardController.js (1)
151-154:⚠️ Potential issue | 🔴 CriticalCritical: Plaintext password storage persists.
This issue was previously flagged. Storing
newPasswordinvisiblePasswordcompletely undermines the bcrypt hashing and exposes credentials if the database is compromised. Remove line 153 entirely.,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/dashboard/src/controllers/dashboardController.js` around lines 151 - 154, The code currently stores plaintext credentials by assigning newPassword to user.visiblePassword inside the password update flow; remove the assignment to user.visiblePassword in the dashboardController password update logic (the line setting user.visiblePassword = newPassword) so only the bcrypt-hashed value (user.password) is saved; ensure no other code paths in the same controller or the saveUser/updatePassword function write plaintext passwords to any user fields and run tests to confirm only hashed passwords are persisted.
🧹 Nitpick comments (1)
backend/dashboard/src/controllers/dashboardController.js (1)
39-48: Consider consolidating withgetMe.This endpoint is functionally identical to
getMe. You could have one delegate to the other or extract the shared logic to reduce duplication.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/dashboard/src/controllers/dashboardController.js` around lines 39 - 48, getSettings duplicates the logic in getMe; consolidate by either having exports.getSettings call exports.getMe (forward req/res) or extract the shared user-fetching and response formatting into a helper (e.g., a new function fetchAndSendCurrentUser that uses User.findById and mapUserResponse) and have both getSettings and getMe call that helper to remove duplication; ensure error handling remains consistent and that both functions still return the same JSON shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/dashboard/src/controllers/dashboardController.js`:
- Line 170: getUsers currently queries all users (const users = await
User.find({}, "-password")) which breaks tenant isolation; update the query in
the getUsers controller to include companyId: req.user.companyId for standard
admins and only omit that filter when req.user.isMainAdmin is true (i.e., check
req.user.isMainAdmin and if false add { companyId: req.user.companyId } to
User.find), ensuring the projection still excludes "-password" and preserving
existing sort({ createdAt: -1 }).
---
Duplicate comments:
In `@backend/dashboard/src/controllers/dashboardController.js`:
- Around line 151-154: The code currently stores plaintext credentials by
assigning newPassword to user.visiblePassword inside the password update flow;
remove the assignment to user.visiblePassword in the dashboardController
password update logic (the line setting user.visiblePassword = newPassword) so
only the bcrypt-hashed value (user.password) is saved; ensure no other code
paths in the same controller or the saveUser/updatePassword function write
plaintext passwords to any user fields and run tests to confirm only hashed
passwords are persisted.
---
Nitpick comments:
In `@backend/dashboard/src/controllers/dashboardController.js`:
- Around line 39-48: getSettings duplicates the logic in getMe; consolidate by
either having exports.getSettings call exports.getMe (forward req/res) or
extract the shared user-fetching and response formatting into a helper (e.g., a
new function fetchAndSendCurrentUser that uses User.findById and
mapUserResponse) and have both getSettings and getMe call that helper to remove
duplication; ensure error handling remains consistent and that both functions
still return the same JSON shape.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: b59b4dd1-a4a1-46c7-ac8b-f017574ab004
📒 Files selected for processing (3)
backend/dashboard/src/controllers/dashboardController.jsbackend/dashboard/src/models/User.jsreqruita-web-dashboard/src/app/(dashboard)/layout.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- reqruita-web-dashboard/src/app/(dashboard)/layout.tsx
- backend/dashboard/src/models/User.js
| .status(403) | ||
| .json({ message: "Access Denied: Requires Admin Role" }); | ||
| } | ||
| const users = await User.find({}, "-password").sort({ createdAt: -1 }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if companyId filtering is used consistently across user management operations
echo "=== Checking companyId usage in dashboardController.js ==="
rg -n "companyId" backend/dashboard/src/controllers/dashboardController.js
echo ""
echo "=== Checking User model for companyId field ==="
ast-grep --pattern 'companyId: { $$$}'Repository: Thenuja-Hansana/Reqruita-CS80
Length of output: 535
🏁 Script executed:
# View the getUsers function context (around line 170)
sed -n '162,175p' backend/dashboard/src/controllers/dashboardController.jsRepository: Thenuja-Hansana/Reqruita-CS80
Length of output: 484
🏁 Script executed:
# Check the User model definition to confirm it has companyId field
find . -name "*.js" -type f | xargs grep -l "const userSchema\|new Schema" | grep -i user | head -5Repository: Thenuja-Hansana/Reqruita-CS80
Length of output: 111
🏁 Script executed:
# Check for any middleware or authorization on the getUsers endpoint
rg -B5 -A10 "router\.get.*users" backend/dashboard/src/routes/ | head -50Repository: Thenuja-Hansana/Reqruita-CS80
Length of output: 1203
🏁 Script executed:
# Look for documentation about the application's multi-tenancy approach
find . -name "*.md" -o -name "*.txt" | xargs grep -l "tenant\|multi-tenant\|company" 2>/dev/null | head -10Repository: Thenuja-Hansana/Reqruita-CS80
Length of output: 55
🏁 Script executed:
# View the User model schema
cat -n backend/dashboard/src/models/User.jsRepository: Thenuja-Hansana/Reqruita-CS80
Length of output: 2339
🏁 Script executed:
# View the addUser implementation to understand companyId context
sed -n '205,230p' backend/dashboard/src/controllers/dashboardController.jsRepository: Thenuja-Hansana/Reqruita-CS80
Length of output: 956
🏁 Script executed:
# Search for role definitions and authorization logic
rg -n "role.*admin\|admin.*role" backend/dashboard/src/ | head -20Repository: Thenuja-Hansana/Reqruita-CS80
Length of output: 55
🏁 Script executed:
# Check if there's any middleware for company-scoped access
cat -n backend/dashboard/src/middleware/authMiddleware.js 2>/dev/null || echo "File not found, searching for middleware files:"
find backend/dashboard/src -name "*middleware*" -o -name "*auth*" | head -10Repository: Thenuja-Hansana/Reqruita-CS80
Length of output: 301
🏁 Script executed:
# View the authentication middleware
cat -n backend/dashboard/src/middlewares/authMiddleware.jsRepository: Thenuja-Hansana/Reqruita-CS80
Length of output: 940
🏁 Script executed:
# Search for isMainAdmin usage throughout the codebase
rg -n "isMainAdmin" backend/dashboard/src/Repository: Thenuja-Hansana/Reqruita-CS80
Length of output: 606
🏁 Script executed:
# Check if getUsers or any other endpoint has special authorization for super-admins
rg -B3 -A10 "getUsers\|isMainAdmin" backend/dashboard/src/controllers/dashboardController.js | head -40Repository: Thenuja-Hansana/Reqruita-CS80
Length of output: 55
Tenant isolation removed—any admin can view all company users regardless of their company.
This query returns all users across all companies (User.find({}, "-password")), while addUser (line 213), updateUser (line 254), and deleteUser (line 281, 290) enforce companyId: req.user.companyId filtering. The User model requires a companyId field for all users, and an isMainAdmin field exists for super-admin functionality, but neither is checked in getUsers. This allows any admin to view users from all companies despite being restricted to managing only their own company's users—a data exposure issue.
Should either restrict to companyId: req.user.companyId for standard admins or add an isMainAdmin check if cross-company visibility is intentional for super-admins.
Suggested fix to restore tenant isolation
- const users = await User.find({}, "-password").sort({ createdAt: -1 });
+ const users = await User.find({ companyId: req.user.companyId }, "-password").sort({ createdAt: -1 });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/dashboard/src/controllers/dashboardController.js` at line 170,
getUsers currently queries all users (const users = await User.find({},
"-password")) which breaks tenant isolation; update the query in the getUsers
controller to include companyId: req.user.companyId for standard admins and only
omit that filter when req.user.isMainAdmin is true (i.e., check
req.user.isMainAdmin and if false add { companyId: req.user.companyId } to
User.find), ensuring the projection still excludes "-password" and preserving
existing sort({ createdAt: -1 }).
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
reqruita-web-dashboard/src/lib/api.ts (1)
43-74:⚠️ Potential issue | 🟠 MajorGuard
localStorageaccess for SSR safety.In Next.js, these helpers may be invoked during server-side rendering where
localStorageis undefined.saveToken,getToken, andgetStoredUseraccess it without guards, riskingReferenceError. The guards inremoveTokenandsaveUseronly protectdispatchEvent, notlocalStorage.🛡️ Proposed fix to add SSR guards
export function saveToken(token: string): void { + if (typeof window === "undefined") return; localStorage.setItem("reqruita_token", token); } export function getToken(): string | null { + if (typeof window === "undefined") return null; return localStorage.getItem("reqruita_token"); } export function removeToken(): void { - localStorage.removeItem("reqruita_token"); - localStorage.removeItem(USER_STORAGE_KEY); if (typeof window !== "undefined") { + localStorage.removeItem("reqruita_token"); + localStorage.removeItem(USER_STORAGE_KEY); window.dispatchEvent(new CustomEvent(USER_UPDATED_EVENT, { detail: null })); } } export function saveUser(user: AuthUser): void { - localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user)); if (typeof window !== "undefined") { + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user)); window.dispatchEvent(new CustomEvent(USER_UPDATED_EVENT, { detail: user })); } } export function getStoredUser(): AuthUser | null { + if (typeof window === "undefined") return null; const raw = localStorage.getItem(USER_STORAGE_KEY);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@reqruita-web-dashboard/src/lib/api.ts` around lines 43 - 74, Guard all direct localStorage access for SSR by checking typeof window !== "undefined" before using it: update saveToken(token), getToken(), and getStoredUser() to no-op (saveToken) or return null (getToken/getStoredUser) when window is undefined; keep existing dispatchEvent guards in removeToken() and saveUser() but also wrap their localStorage calls with the same typeof window check to avoid ReferenceError. Ensure you reference the USER_STORAGE_KEY and USER_UPDATED_EVENT behavior remains unchanged when running in the browser.
🧹 Nitpick comments (1)
reqruita-web-dashboard/src/lib/api.ts (1)
161-215: Add authentication guard for consistency.The settings functions (
fetchSettings,updateSettings,changePassword) don't validate that a token exists before making requests, unlike the job form functions which throw"Authentication required"early. A null token results in"Bearer null"in the header, producing a confusing 401 instead of a clear client-side error.♻️ Proposed fix to add consistent auth checks
export async function fetchSettings(): Promise<AuthUser> { const token = getToken(); + if (!token) { + throw new Error("Authentication required"); + } const res = await fetch(`${AUTH_API_BASE}/api/settings`, {Apply the same pattern to
updateSettingsandchangePassword.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@reqruita-web-dashboard/src/lib/api.ts` around lines 161 - 215, The three functions fetchSettings, updateSettings, and changePassword call getToken() but don’t guard for a missing token; update each to mirror the job form pattern by checking the token immediately after calling getToken() and throwing a clear client-side Error("Authentication required") if it's null/undefined, before making the fetch call (keep using the same Authorization header format when token exists).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@reqruita-web-dashboard/src/lib/api.ts`:
- Around line 43-74: Guard all direct localStorage access for SSR by checking
typeof window !== "undefined" before using it: update saveToken(token),
getToken(), and getStoredUser() to no-op (saveToken) or return null
(getToken/getStoredUser) when window is undefined; keep existing dispatchEvent
guards in removeToken() and saveUser() but also wrap their localStorage calls
with the same typeof window check to avoid ReferenceError. Ensure you reference
the USER_STORAGE_KEY and USER_UPDATED_EVENT behavior remains unchanged when
running in the browser.
---
Nitpick comments:
In `@reqruita-web-dashboard/src/lib/api.ts`:
- Around line 161-215: The three functions fetchSettings, updateSettings, and
changePassword call getToken() but don’t guard for a missing token; update each
to mirror the job form pattern by checking the token immediately after calling
getToken() and throwing a clear client-side Error("Authentication required") if
it's null/undefined, before making the fetch call (keep using the same
Authorization header format when token exists).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ea93ee8b-c395-437a-a3cb-7ecb53062ad8
📒 Files selected for processing (2)
backend/dashboard/src/models/User.jsreqruita-web-dashboard/src/lib/api.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/dashboard/src/models/User.js
Developed Settings Page and Job Forms Features.
Developed Settings Page and Job Forms Features.
Summary by CodeRabbit
New Features
Enhancements