Skip to content

feat: add secure account deletion functionality#571

Open
adityayadav176 wants to merge 2 commits into
UTKARSHH20:mainfrom
adityayadav176:feature/accountDelete
Open

feat: add secure account deletion functionality#571
adityayadav176 wants to merge 2 commits into
UTKARSHH20:mainfrom
adityayadav176:feature/accountDelete

Conversation

@adityayadav176

@adityayadav176 adityayadav176 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Description

Summary

Implemented a secure Account Deletion feature that allows authenticated users to permanently delete their accounts after password verification.

Changes Made

  • Added Delete Account option in the Danger Zone section of Settings.
  • Added a secure password confirmation modal before account deletion.
  • Implemented backend account deletion endpoint with authentication protection.
  • Verified user password using bcrypt before deletion.
  • Deleted all messages associated with the user.
  • Removed user account data from the database.
  • Deleted user profile image from Cloudinary (if available).
  • Cleared authentication and security cookies after successful deletion.
  • Redirected users to the login page after account removal.

Security Enhancements

  • Password verification required before deletion.
  • Protected route access using authentication middleware.
  • CSRF validation maintained for deletion requests.
  • Secure session termination after account deletion.

Testing

  • Verified successful account deletion with correct password.
  • Verified rejection on incorrect password.
  • Verified logout and cookie cleanup after deletion.
  • Verified removal of user data and associated messages.

step 1
image

step2
image

step3 Click the delete account btn and enter your password for confirmation and delete your account
image

Closes #515

Summary by CodeRabbit

  • New Features
    • Account deletion: Users can now permanently delete their account from Settings in the "Danger Zone" section. The deletion process requires password confirmation to prevent accidental removal and completely erases all user data and associated messages.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements secure account deletion functionality allowing users to permanently delete their accounts. It adds backend password verification, cascading deletion of user data (including messages), session invalidation, and a frontend modal-based UI with password confirmation and CSRF protection.

Changes

Account Deletion Feature

Layer / File(s) Summary
Backend configuration and dependencies
backend/.env.example, backend/package.json
Email configuration variables and compression package dependency are added to support the feature setup.
Backend account deletion endpoint
backend/src/controllers/user.controller.js, backend/src/routes/user.route.js
deleteProfile controller verifies password via bcrypt, deletes all messages associated with the user (as sender or receiver), removes the user document, clears authentication and CSRF cookies, and returns appropriate error codes for missing users or incorrect passwords. A protected DELETE /delete-account route wires the controller.
Frontend account deletion UI and handler
frontend/pages/SettingsPage.jsx
handleDeleteAccount extracts XSRF-TOKEN from cookies and sends an authenticated DELETE request with password and CSRF validation headers; displays success/error toasts, clears modal state, and redirects to login on success. A "Delete Account" card in the Danger Zone opens a password confirmation modal with Cancel and "Delete Permanently" controls.

Sequence Diagram

sequenceDiagram
  participant SettingsPage as Frontend:<br/>SettingsPage
  participant Router as Backend:<br/>Router
  participant Controller as Backend:<br/>deleteProfile
  participant Database as Database:<br/>User/Message
  SettingsPage->>Router: DELETE /delete-account<br/>(password, XSRF-TOKEN)
  Router->>Controller: protectRoute passes req.userId
  Controller->>Database: Find user by req.userId
  Controller->>Controller: bcrypt.compare password
  Controller->>Database: Delete messages (sender OR receiver)
  Controller->>Database: Delete user document
  Controller->>Router: Clear jwt & XSRF-TOKEN cookies
  Controller->>SettingsPage: 200 success JSON
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A bunny hops to delete with care,
Password checked, no trace left there,
Messages fade, the account's gone,
CSRF guards till logout's done!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add secure account deletion functionality' accurately summarizes the primary change of implementing secure account deletion across both backend and frontend.
Linked Issues check ✅ Passed All backend and frontend requirements from issue #515 are implemented: DELETE endpoint with password verification, session invalidation with cookie clearing, user and message data removal, frontend Delete Account UI with confirmation modal, and proper error handling.
Out of Scope Changes check ✅ Passed All changes are directly aligned with implementing the account deletion feature. The compression dependency addition and email configuration updates are minimal and supporting changes that do not introduce scope creep.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@adityayadav176

Copy link
Copy Markdown
Contributor Author

Hi @UTKARSHH20 ,

I have successfully implemented the secure Account Deletion functionality.

Changes Implemented

  • Added Delete Account option in the Danger Zone section.
  • Added password verification before account deletion.
  • Permanently deletes the user account from the database.
  • Deletes associated messages.
  • Removes profile image from Cloudinary (if available).
  • Clears authentication cookies and logs the user out.
  • Added a confirmation modal for secure account deletion.

This introduces a significant security-focused feature and improves user account management.

Kindly review the implementation and, if it meets the project requirements, please consider marking it as Critical and adding the appropriate GSSoC'26 labels.

Closes #515

Thank you!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/.env.example`:
- Line 13: The EMAIL_PASSWORD value in the .env example is a real-looking
secret; replace the current value for the EMAIL_PASSWORD entry with a
non-sensitive placeholder (e.g., EMAIL_PASSWORD=your_email_password_here or
EMAIL_PASSWORD=CHANGE_ME) so the example no longer contains secret-looking
credentials and clearly signals that users must supply their own secret.

In `@backend/src/controllers/user.controller.js`:
- Around line 71-80: Wrap the two destructive operations (Message.deleteMany and
User.findByIdAndDelete) in a single transactional boundary or convert to a
two-phase delete; for a transaction use Mongoose sessions: obtain a session via
mongoose.startSession(), call session.withTransaction(async () => { await
Message.deleteMany({ $or: [{ senderId: userId }, { receiverId: userId }] }, {
session }); await User.findByIdAndDelete(userId, { session }); }); and ensure
you end the session and propagate errors; alternatively implement a two-phase
soft-delete by marking User as "deleting" (e.g., userController sets
user.deleting=true) and have a background job perform the actual
Message.deleteMany and final User removal to avoid partial application.
- Around line 79-80: The delete flow currently calls
User.findByIdAndDelete(userId) and never removes the Cloudinary asset; update
the deleteProfile flow to first fetch the user (e.g., via User.findById or
findByIdAndUpdate with return) read the profilePicture field (upload.secure_url
or ideally upload.public_id), call cloudinary.uploader.destroy(public_id) (or
parse the public_id from secure_url if public_id is not stored), await
successful destroy (handle errors/no-op if no image), then proceed to remove the
user with User.findByIdAndDelete(userId); ensure you reference
User.findByIdAndDelete, profilePicture (or upload.public_id), and
cloudinary.uploader.destroy in the change.
- Around line 51-62: In deleteProfile, guard against missing password input and
passwordless (OAuth) accounts before calling bcrypt.compare by validating
req.body.password is a non-empty string and that user.password exists; return a
400 with a clear message if either check fails. Wrap the deletes in a MongoDB
session/transaction (use startSession and session.withTransaction) and perform
Message.deleteMany(...) and User.findByIdAndDelete(...) inside that transaction
to avoid partial deletion. Also, if user.profilePicture (or its publicId)
exists, call the Cloudinary deletion (e.g.,
cloudinary.uploader.destroy(publicId)) as part of the same transactional flow or
immediately after successful user deletion to remove the stored profile picture.
Ensure to end/abort the session on errors and surface appropriate error
responses.

In `@frontend/pages/SettingsPage.jsx`:
- Line 12: Replace the raw axios import and hardcoded URL usage in
SettingsPage.jsx with the shared axios client and the relative endpoint path;
import the shared client (e.g., apiClient from the existing axios wrapper)
instead of `import axios from "axios"`, and update the account-deletion calls
that use `http://localhost:5001/...` (including the logic around the delete
account handler at the block around lines ~107-125) to call
apiClient.post('/users/delete-account') so XSRF and credentials logic defined in
the shared client are reused. Ensure you update all occurrences in this file to
use the shared client and the `/users/delete-account` path.
- Around line 90-91: Add an in-flight isDeleting boolean state (e.g., const
[isDeleting, setIsDeleting] = useState(false)) in SettingsPage.jsx and use it to
prevent duplicate requests: in the delete-confirm handler (the function that
sends the DELETE request from the account delete modal) early-return if
isDeleting is true, setIsDeleting(true) before the async fetch/axios call, and
setIsDeleting(false) in a finally block so state is cleared on both success and
error. Disable the modal action buttons and the "Delete Permanently" button when
isDeleting is true and change the button label to a progress state like
"Deleting…" while in-flight; apply the same guard/disable behavior to the other
delete-related handlers/components referenced in the file (the modal confirm
handler and the other delete button locations).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8039d030-2311-4c38-a3eb-11215b32250a

📥 Commits

Reviewing files that changed from the base of the PR and between b6ebd50 and f408598.

⛔ Files ignored due to path filters (2)
  • backend/package-lock.json is excluded by !**/package-lock.json
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • backend/.env.example
  • backend/package.json
  • backend/src/controllers/user.controller.js
  • backend/src/routes/user.route.js
  • frontend/pages/SettingsPage.jsx

Comment thread backend/.env.example
VAPID_PRIVATE_KEY=your_vapid_private_key
VAPID_SUBJECT=mailto:your@email.com
EMAIL=mailto:your@email.com
EMAIL_PASSWORD=vzktivbgeaqcdczr

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace the example password with a placeholder.

vzktivbgeaqcdczr looks like a real app password, not a dummy value. Keeping secret-looking credentials in .env.example normalizes committing secrets and risks accidental reuse or leakage.

Suggested change
-EMAIL_PASSWORD=vzktivbgeaqcdczr
+EMAIL_PASSWORD=your_email_app_password
📝 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.

Suggested change
EMAIL_PASSWORD=vzktivbgeaqcdczr
EMAIL_PASSWORD=your_email_app_password
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 13-13: [UnorderedKey] The EMAIL_PASSWORD key should go before the GOOGLE_CLIENT_ID key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/.env.example` at line 13, The EMAIL_PASSWORD value in the .env
example is a real-looking secret; replace the current value for the
EMAIL_PASSWORD entry with a non-sensitive placeholder (e.g.,
EMAIL_PASSWORD=your_email_password_here or EMAIL_PASSWORD=CHANGE_ME) so the
example no longer contains secret-looking credentials and clearly signals that
users must supply their own secret.

Comment on lines +51 to +62
const { password } = req.body;

const user = await User.findById(userId);

if (!user) {
return res.status(404).json({
success: false,
message: "User not found",
});
}

const isMatch = await bcrypt.compare(password, user.password);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "User schema fields related to local-vs-OAuth auth:"
sed -n '1,80p' backend/src/models/user.model.js

echo
echo "All bcrypt.compare call sites:"
rg -n -C2 'bcrypt\.compare\s*\(' backend/src

echo
echo "Any Google/OAuth-related auth paths:"
rg -n -C2 'googleId|google' backend/src

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 7661


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate deleteProfile in user.controller.js:"
rg -n "deleteProfile" backend/src/controllers/user.controller.js

echo
echo "Show deleteProfile implementation (around the reported lines 51-62):"
sed -n '1,140p' backend/src/controllers/user.controller.js | nl -ba | sed -n '35,120p'

echo
echo "Search for profile image / Cloudinary cleanup across backend/src:"
rg -n "cloudinary|profilePicture|XSRF-TOKEN|jwt|delete.*profile|remove.*image" backend/src -S

echo
echo "Search for message deletion logic (senderId/receiverId) specifically:"
rg -n "senderId|receiverId|deleteMany" backend/src/controllers backend/src/models -S

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 313


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate deleteProfile in user.controller.js:"
rg -n "export const deleteProfile" backend/src/controllers/user.controller.js

echo
echo "Show deleteProfile implementation (user.controller.js, lines 1-220):"
sed -n '1,220p' backend/src/controllers/user.controller.js | cat -n | sed -n '35,190p'

echo
echo "Search for profile image / Cloudinary cleanup across backend/src:"
rg -n "cloudinary|profilePicture|XSRF-TOKEN|jwt|delete.*profile|remove.*image|destroy.*image" backend/src -S

echo
echo "Search for message deletion logic (senderId/receiverId) specifically:"
rg -n "senderId|receiverId|deleteMany" backend/src/controllers backend/src/models -S

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 14302


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect full user.model.js for any hooks (up to 240 lines):"
sed -n '1,240p' backend/src/models/user.model.js | cat -n

echo
echo "Search for Cloudinary destroy usage related to profilePicture/user deletion:"
rg -n "profilePicture.*destroy|destroy.*profilePicture|destroy.*user|on\\(\"delete|pre\\(\"delete|post\\(\"delete" backend/src -S

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 1871


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Search for profile-picture update/delete and Cloudinary destroy in auth/user-related controllers:"
rg -n "updateProfilePicture|profilePicture.*(upload|destroy|delete)|destroy\\(" backend/src/controllers backend/src/services backend/src/lib -S

echo
echo "Search for any 'delete user' helpers or routes besides deleteProfile:"
rg -n "deleteProfile|findByIdAndDelete\\(|remove\\(\\)|deleteOne\\(|deleteMany\\(\\)" backend/src -S

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 1425


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Show delete-account route middleware stack:"
sed -n '1,120p' backend/src/routes/user.route.js | cat -n

echo
echo "Show validateLogin / validateSignup behavior (if present):"
rg -n "export const validateLogin|validateLogin|export const validateSignup|validateSignup" backend/src/middleware/validate.js backend/src/middleware -S
sed -n '1,220p' backend/src/middleware/validate.js | cat -n

echo
echo "Inspect auth.controller.js login section for how bcrypt.compare inputs are handled:"
rg -n "export const login|bcrypt\\.compare" backend/src/controllers/auth.controller.js
sed -n '40,140p' backend/src/controllers/auth.controller.js | cat -n

echo
echo "Inspect updateProfilePicture to see what Cloudinary fields are stored (secure_url vs public_id):"
sed -n '160,260p' backend/src/controllers/auth.controller.js | cat -n

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 11395


Guard password confirmation input and passwordless accounts before bcrypt.compare in deleteProfile.

  • deleteProfile calls bcrypt.compare(password, user.password) without validating that password is a string or that user.password is present; User.password defaults to null for OAuth accounts, and this path is currently missing the same guard used by login.
  • deleteProfile performs Message.deleteMany(...) and User.findByIdAndDelete(...) as separate operations; use a MongoDB transaction/session to avoid partial deletion.
  • No Cloudinary/profile-picture deletion runs in deleteProfile (message attachment cleanup exists elsewhere, but user profilePicture is not destroyed here).
Suggested change
 export const deleteProfile = catchAsync(async (req, res) => {
     const userId = req.userId;
     const { password } = req.body;

     const user = await User.findById(userId);

     if (!user) {
         return res.status(404).json({
             success: false,
             message: "User not found",
         });
     }
+
+    if (typeof password !== "string" || password.trim() === "") {
+        return res.status(400).json({
+            success: false,
+            message: "Password is required",
+        });
+    }
+
+    if (!user.password) {
+        return res.status(400).json({
+            success: false,
+            message: "This account does not support password confirmation",
+        });
+    }

     const isMatch = await bcrypt.compare(password, user.password);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/controllers/user.controller.js` around lines 51 - 62, In
deleteProfile, guard against missing password input and passwordless (OAuth)
accounts before calling bcrypt.compare by validating req.body.password is a
non-empty string and that user.password exists; return a 400 with a clear
message if either check fails. Wrap the deletes in a MongoDB session/transaction
(use startSession and session.withTransaction) and perform
Message.deleteMany(...) and User.findByIdAndDelete(...) inside that transaction
to avoid partial deletion. Also, if user.profilePicture (or its publicId)
exists, call the Cloudinary deletion (e.g.,
cloudinary.uploader.destroy(publicId)) as part of the same transactional flow or
immediately after successful user deletion to remove the stored profile picture.
Ensure to end/abort the session on errors and surface appropriate error
responses.

Comment on lines +71 to +80
// Delete user's messages
await Message.deleteMany({
$or: [
{ senderId: userId },
{ receiverId: userId },
],
});

// Delete user
await User.findByIdAndDelete(userId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make the destructive deletes atomic.

If message deletion succeeds and user deletion fails, this endpoint leaves the account alive with its history already gone. Account removal needs a single transactional boundary, or a two-phase delete state, so it cannot partially apply.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/controllers/user.controller.js` around lines 71 - 80, Wrap the
two destructive operations (Message.deleteMany and User.findByIdAndDelete) in a
single transactional boundary or convert to a two-phase delete; for a
transaction use Mongoose sessions: obtain a session via mongoose.startSession(),
call session.withTransaction(async () => { await Message.deleteMany({ $or: [{
senderId: userId }, { receiverId: userId }] }, { session }); await
User.findByIdAndDelete(userId, { session }); }); and ensure you end the session
and propagate errors; alternatively implement a two-phase soft-delete by marking
User as "deleting" (e.g., userController sets user.deleting=true) and have a
background job perform the actual Message.deleteMany and final User removal to
avoid partial application.

Comment on lines +79 to +80
// Delete user
await User.findByIdAndDelete(userId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "Image/profile fields on the user model:"
rg -n -C2 'profile|avatar|image|publicId|cloudinary' backend/src/models/user.model.js

echo
echo "Cloudinary delete/destroy usage:"
rg -n -C2 'cloudinary|uploader\.destroy|destroy\s*\(' backend/src

echo
echo "User delete hooks or cleanup middleware:"
rg -n -C2 'pre\(|post\(|findOneAndDelete|findByIdAndDelete|deleteOne' backend/src/models backend/src

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 8064


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the user model + delete controller code
echo "=== backend/src/models/user.model.js ==="
ls -la backend/src/models/user.model.js
nl -ba backend/src/models/user.model.js | sed -n '1,220p'

echo
echo "=== backend/src/controllers/user.controller.js (around deletion) ==="
nl -ba backend/src/controllers/user.controller.js | sed -n '1,220p'

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for Cloudinary deletion/destroy logic that might be used during user deletion
echo "=== Cloudinary destroy usage anywhere ==="
rg -n --glob='backend/src/**/*.js' "cloudinary\.uploader\.destroy|destroy\s*\(" backend/src | head -n 200

echo
echo "=== Any helper that extracts Cloudinary public IDs ==="
rg -n --glob='backend/src/**/*.js' "extractPublicId" backend/src | head -n 200

echo
echo "=== User deletion flow entry points ==="
rg -n --glob='backend/src/**/*.js' "findByIdAndDelete\(userId\)|deleteUser|/delete|delete.*user" backend/src/controllers backend/src/routes | head -n 200

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 1210


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== User controller: find deleteProfile + updateProfilePicture ==="
rg -n "function\s+deleteProfile|const\s+deleteProfile|deleteProfile\s*=" backend/src/controllers/user.controller.js
rg -n "function\s+updateProfilePicture|const\s+updateProfilePicture|updateProfilePicture\s*=" backend/src/controllers/user.controller.js
rg -n "cloudinary|uploader\.destroy|destroy\s*\(" backend/src/controllers/user.controller.js

echo
echo "=== Print user.controller.js (top + around deleteProfile) ==="
wc -l backend/src/controllers/user.controller.js
cat -n backend/src/controllers/user.controller.js | head -n 220
echo
# best-effort: print around lines ~60-120 (where the delete snippet was)
cat -n backend/src/controllers/user.controller.js | sed -n '50,130p'

echo
echo "=== Print user.model.js ==="
wc -l backend/src/models/user.model.js
cat -n backend/src/models/user.model.js | sed -n '1,260p'

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 205


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for extractPublicId usage besides message.controller.js ==="
rg -n --glob='backend/src/**/*.js' "extractPublicId\s*\(" backend/src | head -n 200

echo
echo "=== Search for profilePicture update/delete logic (cloudinary destroy) ==="
rg -n --glob='backend/src/**/*.js' "profilePicture" backend/src/controllers backend/src/models | head -n 200

echo
echo "=== Search for Mongoose hooks in user.model.js ==="
rg -n "pre\s*\(|post\s*\(|schema\.(pre|post)|hooks" backend/src/models/user.model.js

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 2370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== backend/src/controllers/user.controller.js (full, with line numbers via cat -n) ==="
wc -l backend/src/controllers/user.controller.js
cat -n backend/src/controllers/user.controller.js | sed -n '1,220p'

echo
echo "=== backend/src/models/user.model.js (with line numbers) ==="
wc -l backend/src/models/user.model.js
cat -n backend/src/models/user.model.js | sed -n '1,260p'

echo
echo "=== Cloudinary usage specifically in user.controller.js ==="
rg -n --glob='backend/src/controllers/user.controller.js' "cloudinary\.uploader|uploader\.destroy|uploader\.|destroy\s*\(" backend/src/controllers/user.controller.js || true

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 4946


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "updateProfilePicture" backend/src/controllers/auth.controller.js
rg -n "profilePicture" backend/src/controllers/auth.controller.js | head -n 50
sed -n '150,260p' backend/src/controllers/auth.controller.js

Repository: UTKARSHH20/fullstack-chat-app

Length of output: 4660


Add Cloudinary profile-image cleanup before deleting the user document

The deleteProfile flow deletes the user via await User.findByIdAndDelete(userId) without any Cloudinary uploader.destroy call. The user model has no Mongoose delete hooks, and profilePicture is stored as upload.secure_url, so the current delete path can’t clean up the underlying Cloudinary asset (it will orphan it).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/controllers/user.controller.js` around lines 79 - 80, The delete
flow currently calls User.findByIdAndDelete(userId) and never removes the
Cloudinary asset; update the deleteProfile flow to first fetch the user (e.g.,
via User.findById or findByIdAndUpdate with return) read the profilePicture
field (upload.secure_url or ideally upload.public_id), call
cloudinary.uploader.destroy(public_id) (or parse the public_id from secure_url
if public_id is not stored), await successful destroy (handle errors/no-op if no
image), then proceed to remove the user with User.findByIdAndDelete(userId);
ensure you reference User.findByIdAndDelete, profilePicture (or
upload.public_id), and cloudinary.uploader.destroy in the change.

import useAuthStore from "../src/store/useAuthStore"
import StatusMoodSelector from "../components/StatusMoodSelector"
import toast from "react-hot-toast"
import axios from "axios"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the shared axios client for this flow.

Hardcoding http://localhost:5001/... makes account deletion fail outside local development, and this duplicates XSRF/credentials logic that frontend/lib/axios.js already centralizes. Call the shared client with /users/delete-account instead.

Suggested change
-import axios from "axios"
+import axiosInstance from "../lib/axios"
...
 const handleDeleteAccount = async () => {
     try {
-        const csrfToken = document.cookie
-            .split("; ")
-            .find((row) => row.startsWith("XSRF-TOKEN="))
-            ?.split("=")[1];
-
-        const res = await axios.delete(
-            "http://localhost:5001/api/users/delete-account",
-            {
-                data: {
-                    password: deletePassword,
-                },
-                withCredentials: true,
-                headers: {
-                    "x-xsrf-token": csrfToken,
-                },
-            }
-        );
+        const res = await axiosInstance.delete("/users/delete-account", {
+            data: { password: deletePassword },
+        });

Also applies to: 107-125

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/pages/SettingsPage.jsx` at line 12, Replace the raw axios import and
hardcoded URL usage in SettingsPage.jsx with the shared axios client and the
relative endpoint path; import the shared client (e.g., apiClient from the
existing axios wrapper) instead of `import axios from "axios"`, and update the
account-deletion calls that use `http://localhost:5001/...` (including the logic
around the delete account handler at the block around lines ~107-125) to call
apiClient.post('/users/delete-account') so XSRF and credentials logic defined in
the shared client are reused. Ensure you update all occurrences in this file to
use the shared client and the `/users/delete-account` path.

Comment on lines +90 to +91
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deletePassword, setDeletePassword] = useState("");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent duplicate delete requests and expose an in-flight state.

Delete Permanently stays clickable while the DELETE is running, so double-clicks can fire concurrent destructive requests and turn a success path into a later error toast. Track isDeleting, disable the modal actions, and switch the button label to a progress state.

Suggested change
 const [showDeleteModal, setShowDeleteModal] = useState(false);
 const [deletePassword, setDeletePassword] = useState("");
+const [isDeleting, setIsDeleting] = useState(false);

 const handleDeleteAccount = async () => {
+    if (isDeleting) return;
+    setIsDeleting(true);
     try {
         const res = await axiosInstance.delete("/users/delete-account", {
             data: {
                 password: deletePassword,
             },
         });

         toast.success(res.data.message);
         setShowDeleteModal(false);
         setDeletePassword("");
         window.location.href = "/login";
     } catch (error) {
         toast.error(
             error.response?.data?.message ||
             "Failed to delete account"
         );
+    } finally {
+        setIsDeleting(false);
     }
 };
...
                 <button
                     className="btn btn-outline"
+                    disabled={isDeleting}
                     onClick={() => {
                         setShowDeleteModal(false);
                         setDeletePassword("");
                     }}
                 >
                     Cancel
                 </button>

                 <button
                     className="btn btn-error"
+                    disabled={isDeleting || !deletePassword.trim()}
                     onClick={handleDeleteAccount}
                 >
-                    Delete Permanently
+                    {isDeleting ? "Deleting..." : "Delete Permanently"}
                 </button>

Also applies to: 107-139, 457-462

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/pages/SettingsPage.jsx` around lines 90 - 91, Add an in-flight
isDeleting boolean state (e.g., const [isDeleting, setIsDeleting] =
useState(false)) in SettingsPage.jsx and use it to prevent duplicate requests:
in the delete-confirm handler (the function that sends the DELETE request from
the account delete modal) early-return if isDeleting is true,
setIsDeleting(true) before the async fetch/axios call, and setIsDeleting(false)
in a finally block so state is cleared on both success and error. Disable the
modal action buttons and the "Delete Permanently" button when isDeleting is true
and change the button label to a progress state like "Deleting…" while
in-flight; apply the same guard/disable behavior to the other delete-related
handlers/components referenced in the file (the modal confirm handler and the
other delete button locations).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

# Feature: Add Secure Account Deletion Functionality

1 participant