diff --git a/backend/.env.example b/backend/.env.example index 7b62781..a560881 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -9,3 +9,5 @@ GOOGLE_CLIENT_ID=your_google_client_id VAPID_PUBLIC_KEY=your_vapid_public_key VAPID_PRIVATE_KEY=your_vapid_private_key VAPID_SUBJECT=mailto:your@email.com +EMAIL=mailto:your@email.com +EMAIL_PASSWORD=vzktivbgeaqcdczr diff --git a/backend/package-lock.json b/backend/package-lock.json index 1ff3988..59cfbf9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "bcryptjs": "^3.0.3", "cloudinary": "^2.10.0", + "compression": "^1.8.1", "cookie-parser": "^1.4.7", "cors": "^2.8.6", "dotenv": "^17.4.2", @@ -355,6 +356,60 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -1542,6 +1597,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/backend/package.json b/backend/package.json index 4996ced..d8ac45f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "dependencies": { "bcryptjs": "^3.0.3", "cloudinary": "^2.10.0", + "compression": "^1.8.1", "cookie-parser": "^1.4.7", "cors": "^2.8.6", "dotenv": "^17.4.2", diff --git a/backend/src/controllers/user.controller.js b/backend/src/controllers/user.controller.js index 31d4e5c..9df5668 100644 --- a/backend/src/controllers/user.controller.js +++ b/backend/src/controllers/user.controller.js @@ -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); + + 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); + + // Logout + res.clearCookie("jwt"); + res.clearCookie("XSRF-TOKEN"); + + return res.status(200).json({ + success: true, + message: "Account deleted successfully", + }); +}); \ No newline at end of file diff --git a/backend/src/routes/user.route.js b/backend/src/routes/user.route.js index 3a7ba1f..d49d655 100644 --- a/backend/src/routes/user.route.js +++ b/backend/src/routes/user.route.js @@ -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; diff --git a/frontend/components/chat/MessageBubble.jsx b/frontend/components/chat/MessageBubble.jsx index 32203be..bab31e6 100644 --- a/frontend/components/chat/MessageBubble.jsx +++ b/frontend/components/chat/MessageBubble.jsx @@ -3,8 +3,37 @@ import Avatar from "./Avatar" import { useState } from "react" import ReplyPreview from "./ReplyPreview" -const formatTime = (d) => - new Date(d).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) +const formatTime = (dateString) => { + const date = new Date(dateString); + const now = new Date(); + + const diffMs = now - date; + const diffMinutes = Math.floor(diffMs / (1000 * 60)); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffMinutes < 1) return "Just now"; + + if (diffMinutes < 60) + return `${diffMinutes} minute${diffMinutes > 1 ? "s" : ""} ago`; + + if (diffHours < 24) + return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`; + + if (diffDays === 1) return "Yesterday"; + + if (diffDays < 7) { + return date.toLocaleDateString([], { + weekday: "long", + }); + } + + return date.toLocaleDateString([], { + month: "short", + day: "numeric", + year: "numeric", + }); +}; // Single message bubble with avatar, media, reactions, and read receipts export default function MessageBubble({ msg, isMine, showTime, selectedUser, isOnline, authUser, onContextMenu, onTouchStart, onTouchEnd, onReact }) { @@ -33,10 +62,10 @@ export default function MessageBubble({ msg, isMine, showTime, selectedUser, isO return (
- {formatTime(msg.createdAt)} -
- )} ++ {formatTime(msg.createdAt)} +
+)}Delete Account
+Permanently delete your account and all associated data
++ This action cannot be undone. + All messages, profile data and account information + will be permanently deleted. +
+ + setDeletePassword(e.target.value)} + /> + +