-
Notifications
You must be signed in to change notification settings - Fork 30
feat: add secure account deletion functionality #571
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| import User from "../models/user.model.js"; | ||
| import { broadcastStatusMoodUpdate } from "../lib/socket.js"; | ||
| import { catchAsync } from "../lib/utils.js"; | ||
| import Message from "../models/message.model.js"; | ||
| import bcrypt from "bcryptjs" | ||
|
|
||
| const ALLOWED_STATUS_MOODS = new Set([ | ||
| "coding", | ||
|
|
@@ -42,3 +44,47 @@ export const updateStatusMood = catchAsync(async (req, res) => { | |
|
|
||
| res.status(200).json(user); | ||
| }); | ||
|
|
||
|
|
||
| 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", | ||
| }); | ||
| } | ||
|
|
||
| const isMatch = await bcrypt.compare(password, user.password); | ||
|
Comment on lines
+51
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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/srcRepository: 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 -SRepository: 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 -SRepository: 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 -SRepository: 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 -SRepository: 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 -nRepository: UTKARSHH20/fullstack-chat-app Length of output: 11395 Guard password confirmation input and passwordless accounts before
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 |
||
|
|
||
| if (!isMatch) { | ||
| return res.status(400).json({ | ||
| success: false, | ||
| message: "Incorrect Password", | ||
| }); | ||
| } | ||
|
|
||
| // Delete user's messages | ||
| await Message.deleteMany({ | ||
| $or: [ | ||
| { senderId: userId }, | ||
| { receiverId: userId }, | ||
| ], | ||
| }); | ||
|
|
||
| // Delete user | ||
| await User.findByIdAndDelete(userId); | ||
|
Comment on lines
+71
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Comment on lines
+79
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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/srcRepository: 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 200Repository: 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.jsRepository: 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 || trueRepository: 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.jsRepository: UTKARSHH20/fullstack-chat-app Length of output: 4660 Add Cloudinary profile-image cleanup before deleting the user document The 🤖 Prompt for AI Agents |
||
|
|
||
| // Logout | ||
| res.clearCookie("jwt"); | ||
| res.clearCookie("XSRF-TOKEN"); | ||
|
|
||
| return res.status(200).json({ | ||
| success: true, | ||
| message: "Account deleted successfully", | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
| import express from "express"; | ||
| import protectRoute from "../middleware/auth.middleware.js"; | ||
| import { updateStatusMood } from "../controllers/user.controller.js"; | ||
| import { deleteProfile, updateStatusMood } from "../controllers/user.controller.js"; | ||
|
|
||
| const router = express.Router(); | ||
|
|
||
| router.patch("/status-mood", protectRoute, updateStatusMood); | ||
| router.delete("/delete-account", protectRoute, deleteProfile); | ||
|
|
||
| export default router; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace the example password with a placeholder.
vzktivbgeaqcdczrlooks like a real app password, not a dummy value. Keeping secret-looking credentials in.env.examplenormalizes committing secrets and risks accidental reuse or leakage.Suggested change
📝 Committable suggestion
🧰 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