diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js
index db40e01..5dbfe96 100644
--- a/backend/middleware/auth.js
+++ b/backend/middleware/auth.js
@@ -1,25 +1,37 @@
const jwt = require('jsonwebtoken');
+const User = require('../models/User');
require('dotenv').config();
// Use a fallback JWT secret if env variable is missing
const JWT_SECRET = process.env.JWT_SECRET || 'devsync_secure_jwt_secret_key_for_authentication';
-module.exports = function(req, res, next) {
- // Get token from header
- const token = req.header('x-auth-token');
-
- // Check if no token
- if (!token) {
- return res.status(401).json({ errors: [{ msg: 'No token, authorization denied' }] });
- }
-
- // Verify token
+module.exports = async function(req, res, next) {
try {
+ const token = req.header('Authorization')?.replace('Bearer ', '');
+
+ if (!token) {
+ return res.status(401).json({ message: 'No token provided' });
+ }
+
const decoded = jwt.verify(token, JWT_SECRET);
- req.user = decoded.user;
+ const user = await User.findById(decoded.id);
+
+ if (!user) {
+ return res.status(401).json({ message: 'User not found' });
+ }
+
+ // Check if account is active
+ if (!user.isActive) {
+ return res.status(403).json({
+ message: 'Account is deactivated. Please contact support to reactivate.',
+ accountStatus: 'deactivated'
+ });
+ }
+
+ req.user = user;
next();
- } catch (err) {
- console.error('Token verification error:', err.message);
- res.status(401).json({ errors: [{ msg: 'Token is not valid' }] });
+ } catch (error) {
+ console.error('Token verification error:', error.message);
+ res.status(401).json({ message: 'Invalid token' });
}
};
\ No newline at end of file
diff --git a/backend/models/User.js b/backend/models/User.js
index 1a23c04..037d743 100644
--- a/backend/models/User.js
+++ b/backend/models/User.js
@@ -97,7 +97,43 @@ const UserSchema = new Schema({
date: {
type: Date,
default: Date.now
+ },
+ isActive: {
+ type: Boolean,
+ default: true,
+ index: true
+ },
+ deletedAt: {
+ type: Date,
+ default: null
+ },
+ deactivatedAt: {
+ type: Date,
+ default: null
}
+}, {
+ timestamps: true
});
+// Add method to safely delete user data
+UserSchema.methods.softDelete = function() {
+ this.isActive = false;
+ this.deletedAt = new Date();
+ return this.save();
+};
+
+// Add method to deactivate account
+UserSchema.methods.deactivate = function() {
+ this.isActive = false;
+ this.deactivatedAt = new Date();
+ return this.save();
+};
+
+// Add method to reactivate account
+UserSchema.methods.reactivate = function() {
+ this.isActive = true;
+ this.deactivatedAt = null;
+ return this.save();
+};
+
module.exports = mongoose.model('User', UserSchema);
diff --git a/backend/routes/users.js b/backend/routes/users.js
new file mode 100644
index 0000000..317f536
--- /dev/null
+++ b/backend/routes/users.js
@@ -0,0 +1,123 @@
+const express = require('express');
+const router = express.Router();
+const User = require('../models/User');
+const auth = require('../middleware/auth');
+
+// DELETE /api/users/:id - Permanently delete user account
+router.delete('/:id', auth, async (req, res) => {
+ try {
+ const userId = req.params.id;
+ const requestingUserId = req.user.id;
+
+ // Users can only delete their own account
+ if (userId !== requestingUserId) {
+ return res.status(403).json({
+ message: 'You can only delete your own account'
+ });
+ }
+
+ const user = await User.findById(userId);
+ if (!user) {
+ return res.status(404).json({ message: 'User not found' });
+ }
+
+ // Perform hard delete - removes all user data (GDPR compliant)
+ await User.findByIdAndDelete(userId);
+
+ // TODO: Also delete related data (projects, tasks, etc.)
+ // This should be implemented based on your data relationships
+
+ res.status(200).json({
+ message: 'Account permanently deleted',
+ timestamp: new Date().toISOString()
+ });
+ } catch (error) {
+ res.status(500).json({
+ message: 'Error deleting account',
+ error: error.message
+ });
+ }
+});
+
+// PATCH /api/users/:id/deactivate - Deactivate user account
+router.patch('/:id/deactivate', auth, async (req, res) => {
+ try {
+ const userId = req.params.id;
+ const requestingUserId = req.user.id;
+
+ // Users can only deactivate their own account
+ if (userId !== requestingUserId) {
+ return res.status(403).json({
+ message: 'You can only deactivate your own account'
+ });
+ }
+
+ const user = await User.findById(userId);
+ if (!user) {
+ return res.status(404).json({ message: 'User not found' });
+ }
+
+ if (!user.isActive) {
+ return res.status(400).json({ message: 'Account is already deactivated' });
+ }
+
+ await user.deactivate();
+
+ res.status(200).json({
+ message: 'Account deactivated successfully',
+ user: {
+ id: user._id,
+ email: user.email,
+ isActive: user.isActive,
+ deactivatedAt: user.deactivatedAt
+ }
+ });
+ } catch (error) {
+ res.status(500).json({
+ message: 'Error deactivating account',
+ error: error.message
+ });
+ }
+});
+
+// PATCH /api/users/:id/reactivate - Reactivate user account
+router.patch('/:id/reactivate', auth, async (req, res) => {
+ try {
+ const userId = req.params.id;
+ const requestingUserId = req.user.id;
+
+ // Users can only reactivate their own account
+ if (userId !== requestingUserId) {
+ return res.status(403).json({
+ message: 'You can only reactivate your own account'
+ });
+ }
+
+ const user = await User.findById(userId);
+ if (!user) {
+ return res.status(404).json({ message: 'User not found' });
+ }
+
+ if (user.isActive) {
+ return res.status(400).json({ message: 'Account is already active' });
+ }
+
+ await user.reactivate();
+
+ res.status(200).json({
+ message: 'Account reactivated successfully',
+ user: {
+ id: user._id,
+ email: user.email,
+ isActive: user.isActive
+ }
+ });
+ } catch (error) {
+ res.status(500).json({
+ message: 'Error reactivating account',
+ error: error.message
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b83744d..71f0bce 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,7 +13,9 @@
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2",
+ "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.14",
@@ -37,6 +39,7 @@
"react-intersection-observer": "^9.16.0",
"react-router-dom": "^7.7.0",
"shadcn": "^2.9.2",
+ "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"zod": "^3.25.76"
@@ -1629,6 +1632,57 @@
}
}
},
+ "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
+ "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
+ "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
@@ -1796,20 +1850,20 @@
}
},
"node_modules/@radix-ui/react-dialog": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
- "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.10",
- "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-portal": "1.1.9",
- "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
@@ -1831,6 +1885,63 @@
}
}
},
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
@@ -1874,9 +1985,9 @@
}
},
"node_modules/@radix-ui/react-focus-guards": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
- "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
@@ -1940,6 +2051,29 @@
}
}
},
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
+ "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-popper": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
@@ -7340,6 +7474,16 @@
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT"
},
+ "node_modules/sonner": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
+ "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 1175a0a..a32da85 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -15,7 +15,9 @@
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2",
+ "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.14",
@@ -39,6 +41,7 @@
"react-intersection-observer": "^9.16.0",
"react-router-dom": "^7.7.0",
"shadcn": "^2.9.2",
+ "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"zod": "^3.25.76"
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index e2ab820..0bae2bb 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -27,6 +27,7 @@ import { ArrowUp } from "lucide-react";
import GitHubProfile from "./Components/GitHubProfile";
import LeetCode from "./Components/DashBoard/LeetCode";
import FloatingSupportButton from "./Components/ui/Support";
+import Settings from "./Components/Settings";
function Home() {
const [showTop, setShowTop] = useState(false);
@@ -134,6 +135,7 @@ function App() {
} />
} />
} />
+ } />
);
diff --git a/frontend/src/Components/Navbar.jsx b/frontend/src/Components/Navbar.jsx
new file mode 100644
index 0000000..d6049bc
--- /dev/null
+++ b/frontend/src/Components/Navbar.jsx
@@ -0,0 +1,140 @@
+import React, { useState } from 'react';
+import { Trash2, UserX, AlertTriangle } from 'lucide-react';
+import { Button } from '@/Components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/Card';
+import { ConfirmationModal } from './ConfirmationModal';
+import { useAuth } from '@/hooks/useAuth';
+import { toast } from 'sonner';
+
+export const AccountDangerZone = () => {
+ const [showDeactivateModal, setShowDeactivateModal] = useState(false);
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const { user, logout } = useAuth();
+
+ const handleDeactivateAccount = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/users/${user.id}/deactivate`, {
+ method: 'PATCH',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (response.ok) {
+ toast.success('Account deactivated successfully');
+ logout();
+ } else {
+ const data = await response.json();
+ toast.error(data.message || 'Failed to deactivate account');
+ }
+ } catch {
+ toast.error('Network error. Please try again.');
+ } finally {
+ setIsLoading(false);
+ setShowDeactivateModal(false);
+ }
+ };
+
+ const handleDeleteAccount = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/users/${user.id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
+ },
+ });
+
+ if (response.ok) {
+ toast.success('Account deleted permanently');
+ logout();
+ } else {
+ const data = await response.json();
+ toast.error(data.message || 'Failed to delete account');
+ }
+ } catch {
+ toast.error('Network error. Please try again.');
+ } finally {
+ setIsLoading(false);
+ setShowDeleteModal(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Danger Zone
+
+
+ These actions are irreversible. Please proceed with caution.
+
+
+
+ {/* Deactivate Account */}
+
+
+
Deactivate Account
+
+ Temporarily disable your account. You can reactivate it later.
+
+
+
setShowDeactivateModal(true)}
+ disabled={isLoading}
+ >
+
+ Deactivate
+
+
+
+ {/* Delete Account */}
+
+
+
Delete Account
+
+ Permanently remove your account and all associated data.
+
+
+
setShowDeleteModal(true)}
+ disabled={isLoading}
+ >
+
+ Delete
+
+
+
+
+ {/* Confirmation Modals */}
+ setShowDeactivateModal(false)}
+ onConfirm={handleDeactivateAccount}
+ title="Deactivate Account"
+ description="Your account will be disabled until reactivated. You won't be able to log in or access DevSync, but your data will be preserved."
+ confirmText="Deactivate Account"
+ isLoading={isLoading}
+ variant="warning"
+ />
+
+ setShowDeleteModal(false)}
+ onConfirm={handleDeleteAccount}
+ title="Delete Account"
+ description="Your account and all associated data will be permanently removed. This action cannot be undone."
+ confirmText="Delete Account Forever"
+ isLoading={isLoading}
+ variant="destructive"
+ />
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/Components/Settings.jsx b/frontend/src/Components/Settings.jsx
new file mode 100644
index 0000000..59c3ca3
--- /dev/null
+++ b/frontend/src/Components/Settings.jsx
@@ -0,0 +1,95 @@
+import React from 'react';
+import { AccountDangerZone } from '@/Components/settings/AccountDangerZone';
+import { Bell, Palette, Shield, User } from 'lucide-react';
+
+export const Settings = () => {
+ return (
+
+
+
+
Settings
+
+ Manage your account settings and preferences.
+
+
+
+ {/* Profile Settings */}
+
+
+
+
+ Profile
+
+
+ Update your personal information and profile settings.
+
+
+
+
+ Profile settings section - to be implemented
+
+
+
+
+ {/* Notification Settings */}
+
+
+
+
+ Notifications
+
+
+ Configure your notification preferences.
+
+
+
+
+ Notification settings section - to be implemented
+
+
+
+
+ {/* Privacy & Security */}
+
+
+
+
+ Privacy & Security
+
+
+ Manage your privacy and security settings.
+
+
+
+
+ Privacy and security settings section - to be implemented
+
+
+
+
+ {/* Appearance */}
+
+
+
+
+ Appearance
+
+
+ Customize the look and feel of DevSync.
+
+
+
+
+ Theme and appearance settings section - to be implemented
+
+
+
+
+ {/* Account Management */}
+
+
+
+ );
+};
+
+export default Settings;
diff --git a/frontend/src/Components/settings/AccountDangerZone.jsx b/frontend/src/Components/settings/AccountDangerZone.jsx
new file mode 100644
index 0000000..ab1c132
--- /dev/null
+++ b/frontend/src/Components/settings/AccountDangerZone.jsx
@@ -0,0 +1,140 @@
+import React, { useState } from 'react';
+import { Trash2, UserX, AlertTriangle } from 'lucide-react';
+import { ConfirmationModal } from './ConfirmationModal';
+import { useAuth } from '@/hooks/useAuth';
+import { toast } from 'sonner';
+
+export const AccountDangerZone = () => {
+ const [showDeactivateModal, setShowDeactivateModal] = useState(false);
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const { user, logout } = useAuth();
+
+ const handleDeactivateAccount = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/users/${user.id}/deactivate`, {
+ method: 'PATCH',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (response.ok) {
+ toast.success('Account deactivated successfully');
+ logout();
+ } else {
+ const data = await response.json();
+ toast.error(data.message || 'Failed to deactivate account');
+ }
+ } catch {
+ toast.error('Network error. Please try again.');
+ } finally {
+ setIsLoading(false);
+ setShowDeactivateModal(false);
+ }
+ };
+
+ const handleDeleteAccount = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/users/${user.id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
+ },
+ });
+
+ if (response.ok) {
+ toast.success('Account deleted permanently');
+ logout();
+ } else {
+ const data = await response.json();
+ toast.error(data.message || 'Failed to delete account');
+ }
+ } catch {
+ toast.error('Network error. Please try again.');
+ } finally {
+ setIsLoading(false);
+ setShowDeleteModal(false);
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+ Danger Zone
+
+
+ These actions are irreversible. Please proceed with caution.
+
+
+
+ {/* Content */}
+
+ {/* Deactivate Account */}
+
+
+
Deactivate Account
+
+ Temporarily disable your account. You can reactivate it later.
+
+
+
setShowDeactivateModal(true)}
+ disabled={isLoading}
+ className="inline-flex items-center justify-center rounded-md text-sm font-medium border border-orange-300 text-orange-700 hover:bg-orange-100 h-10 py-2 px-4 disabled:opacity-50 disabled:pointer-events-none"
+ >
+
+ Deactivate
+
+
+
+ {/* Delete Account */}
+
+
+
Delete Account
+
+ Permanently remove your account and all associated data.
+
+
+
setShowDeleteModal(true)}
+ disabled={isLoading}
+ className="inline-flex items-center justify-center rounded-md text-sm font-medium bg-red-600 text-white hover:bg-red-700 h-10 py-2 px-4 disabled:opacity-50 disabled:pointer-events-none"
+ >
+
+ Delete
+
+
+
+
+ {/* Confirmation Modals */}
+
setShowDeactivateModal(false)}
+ onConfirm={handleDeactivateAccount}
+ title="Deactivate Account"
+ description="Your account will be disabled until reactivated. You won't be able to log in or access DevSync, but your data will be preserved."
+ confirmText="Deactivate Account"
+ isLoading={isLoading}
+ variant="warning"
+ />
+
+ setShowDeleteModal(false)}
+ onConfirm={handleDeleteAccount}
+ title="Delete Account"
+ description="Your account and all associated data will be permanently removed. This action cannot be undone."
+ confirmText="Delete Account Forever"
+ isLoading={isLoading}
+ variant="destructive"
+ />
+
+ );
+};
diff --git a/frontend/src/Components/settings/ConfirmationModal.jsx b/frontend/src/Components/settings/ConfirmationModal.jsx
new file mode 100644
index 0000000..f79af82
--- /dev/null
+++ b/frontend/src/Components/settings/ConfirmationModal.jsx
@@ -0,0 +1,109 @@
+import React, { useState, useEffect } from 'react';
+import { AlertTriangle, Loader2 } from 'lucide-react';
+
+export const ConfirmationModal = ({
+ isOpen,
+ onClose,
+ onConfirm,
+ title,
+ description,
+ confirmText,
+ isLoading,
+ variant = 'destructive',
+ requiresTypeConfirmation = true,
+}) => {
+ const [confirmationText, setConfirmationText] = useState('');
+ const expectedText = 'DELETE';
+
+ const isConfirmationValid = !requiresTypeConfirmation ||
+ confirmationText.toUpperCase() === expectedText;
+
+ useEffect(() => {
+ if (isOpen) {
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = 'unset';
+ }
+ return () => {
+ document.body.style.overflow = 'unset';
+ };
+ }, [isOpen]);
+
+ const handleConfirm = () => {
+ if (isConfirmationValid) {
+ onConfirm();
+ }
+ };
+
+ const handleClose = () => {
+ setConfirmationText('');
+ onClose();
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+
+
+
+ {/* Header */}
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+ {requiresTypeConfirmation && variant === 'destructive' && (
+
+
+ Type {expectedText} to confirm:
+
+ setConfirmationText(e.target.value)}
+ placeholder={expectedText}
+ className="flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ />
+
+ )}
+
+ {/* Footer */}
+
+
+ Cancel
+
+
+ {isLoading && }
+ {confirmText}
+
+
+
+
+
+ );
+};
+
diff --git a/frontend/src/Components/ui/Card.jsx b/frontend/src/Components/ui/Card.jsx
index 3689ea8..04d5c66 100644
--- a/frontend/src/Components/ui/Card.jsx
+++ b/frontend/src/Components/ui/Card.jsx
@@ -1,26 +1,61 @@
// src/Components/ui/Card.jsx
-export function Card({ children, className = "" }) {
- return (
-
- {children}
-
- );
-}
+import React from 'react';
+import { cn } from '@/lib/utils';
-export function CardHeader({ children, className = "" }) {
- return (
- {children}
- );
-}
+const Card = React.forwardRef(({ className, ...props }, ref) => (
+
+));
+Card.displayName = "Card";
-export function CardTitle({ children, className = "" }) {
- return {children} ;
-}
+const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = "CardHeader";
-export function CardContent({ children, className = "" }) {
- return {children}
;
-}
+const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = "CardTitle";
-export function CardFooter({ children, className = "" }) {
- return {children}
;
-}
+const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = "CardDescription";
+
+const CardContent = React.forwardRef(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = "CardContent";
+
+const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = "CardFooter";
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
diff --git a/frontend/src/Components/ui/button.jsx b/frontend/src/Components/ui/button.jsx
index 69ad71f..3360288 100644
--- a/frontend/src/Components/ui/button.jsx
+++ b/frontend/src/Components/ui/button.jsx
@@ -1,55 +1,48 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva } from "class-variance-authority";
+import React from 'react';
+import { cn } from '@/lib/utils';
-import { cn } from "@/lib/utils"
+const Button = React.forwardRef(({
+ className,
+ variant = 'default',
+ size = 'default',
+ disabled,
+ children,
+ ...props
+}, ref) => {
+ const baseStyles = "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background";
+
+ const variants = {
+ default: "bg-blue-600 text-white hover:bg-blue-700",
+ destructive: "bg-red-600 text-white hover:bg-red-700",
+ outline: "border border-input hover:bg-accent hover:text-accent-foreground",
+ secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "underline-offset-4 hover:underline text-blue-600"
+ };
-const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
- {
- variants: {
- variant: {
- default:
- "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
- destructive:
- "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
- outline:
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
- secondary:
- "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
- ghost:
- "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
- icon: "size-9",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
-)
-
-function Button({
- className,
- variant,
- size,
- asChild = false,
- ...props
-}) {
- const Comp = asChild ? Slot : "button"
+ const sizes = {
+ default: "h-10 py-2 px-4",
+ sm: "h-9 px-3 rounded-md",
+ lg: "h-11 px-8 rounded-md"
+ };
return (
-
+
+ {children}
+
);
-}
+});
+
+Button.displayName = "Button";
-export { Button, buttonVariants }
+export { Button };
diff --git a/frontend/src/Components/ui/dialog.jsx b/frontend/src/Components/ui/dialog.jsx
new file mode 100644
index 0000000..bd93ede
--- /dev/null
+++ b/frontend/src/Components/ui/dialog.jsx
@@ -0,0 +1,93 @@
+import React, { useEffect } from 'react';
+import { X } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+const Dialog = ({ open, onOpenChange, children }) => {
+ useEffect(() => {
+ if (open) {
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = 'unset';
+ }
+ return () => {
+ document.body.style.overflow = 'unset';
+ };
+ }, [open]);
+
+ if (!open) return null;
+
+ return (
+
+
onOpenChange(false)}
+ />
+
+ {children}
+
+
+ );
+};
+
+const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
+
+ {children}
+
+));
+DialogContent.displayName = "DialogContent";
+
+const DialogHeader = ({ className, children, ...props }) => (
+
+ {children}
+
+);
+
+const DialogTitle = React.forwardRef(({ className, children, ...props }, ref) => (
+
+ {children}
+
+));
+DialogTitle.displayName = "DialogTitle";
+
+const DialogDescription = React.forwardRef(({ className, children, ...props }, ref) => (
+
+ {children}
+
+));
+DialogDescription.displayName = "DialogDescription";
+
+const DialogFooter = ({ className, children, ...props }) => (
+
+ {children}
+
+);
+
+export {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+};
diff --git a/frontend/src/Components/ui/input.jsx b/frontend/src/Components/ui/input.jsx
index e267074..90c5bbd 100644
--- a/frontend/src/Components/ui/input.jsx
+++ b/frontend/src/Components/ui/input.jsx
@@ -1,33 +1,20 @@
-import * as React from "react"
-import { cva } from "class-variance-authority"
+import React from "react"
import { cn } from "@/lib/utils"
-const inputVariants = cva(
- "flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all",
- {
- variants: {
- size: {
- default: "h-10",
- sm: "h-9",
- lg: "h-11",
- },
- },
- defaultVariants: {
- size: "default",
- },
- }
-)
-
-const Input = React.forwardRef(({ className, type, size, ...props }, ref) => {
+const Input = React.forwardRef(({ className, type = "text", ...props }, ref) => {
return (
)
})
+
Input.displayName = "Input"
-export { Input, inputVariants }
+export { Input }
diff --git a/frontend/src/Components/ui/label.jsx b/frontend/src/Components/ui/label.jsx
index 3fc5c1a..9c5177c 100644
--- a/frontend/src/Components/ui/label.jsx
+++ b/frontend/src/Components/ui/label.jsx
@@ -1,5 +1,5 @@
-import * as React from "react"
-import { cn } from "@/lib/utils"
+import React from 'react';
+import { cn } from '@/lib/utils';
const Label = React.forwardRef(({ className, ...props }, ref) => (
(
)}
{...props}
/>
-))
-Label.displayName = "Label"
+));
-export { Label }
+Label.displayName = "Label";
+
+export { Label };
diff --git a/frontend/src/hooks/useAccountManagement.js b/frontend/src/hooks/useAccountManagement.js
new file mode 100644
index 0000000..ec99119
--- /dev/null
+++ b/frontend/src/hooks/useAccountManagement.js
@@ -0,0 +1,80 @@
+import { useState } from 'react';
+import { useAuth } from './useAuth';
+import { toast } from 'sonner';
+
+export const useAccountManagement = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const { user, logout } = useAuth();
+
+ const deactivateAccount = async () => {
+ if (!user?.id) {
+ toast.error('User not found');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/users/${user.id}/deactivate`, {
+ method: 'PATCH',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ toast.success('Account deactivated successfully');
+ logout();
+ return { success: true, data };
+ } else {
+ toast.error(data.message || 'Failed to deactivate account');
+ return { success: false, error: data.message };
+ }
+ } catch (error) {
+ toast.error('Network error. Please try again.');
+ return { success: false, error: error.message };
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const deleteAccount = async () => {
+ if (!user?.id) {
+ toast.error('User not found');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/users/${user.id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
+ },
+ });
+
+ if (response.ok) {
+ toast.success('Account deleted permanently');
+ logout();
+ return { success: true };
+ } else {
+ const data = await response.json();
+ toast.error(data.message || 'Failed to delete account');
+ return { success: false, error: data.message };
+ }
+ } catch (error) {
+ toast.error('Network error. Please try again.');
+ return { success: false, error: error.message };
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return {
+ deactivateAccount,
+ deleteAccount,
+ isLoading,
+ };
+};
diff --git a/frontend/src/hooks/useAuth.js b/frontend/src/hooks/useAuth.js
new file mode 100644
index 0000000..09e6cac
--- /dev/null
+++ b/frontend/src/hooks/useAuth.js
@@ -0,0 +1,69 @@
+import { useState, useEffect, createContext, useContext, createElement } from 'react';
+
+const AuthContext = createContext();
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+};
+
+export const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ // Check for existing token and validate user
+ const token = localStorage.getItem('token');
+ if (token) {
+ // Validate token and get user info
+ fetchUser(token);
+ } else {
+ setLoading(false);
+ }
+ }, []);
+
+ const fetchUser = async (token) => {
+ try {
+ const response = await fetch('/api/users/profile', {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ if (response.ok) {
+ const userData = await response.json();
+ setUser(userData);
+ } else {
+ localStorage.removeItem('token');
+ }
+ } catch (error) {
+ console.error('Error fetching user:', error);
+ localStorage.removeItem('token');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const logout = () => {
+ localStorage.removeItem('token');
+ setUser(null);
+ window.location.href = '/login';
+ };
+
+ const login = (token, userData) => {
+ localStorage.setItem('token', token);
+ setUser(userData);
+ };
+
+ const value = {
+ user,
+ logout,
+ login,
+ loading,
+ };
+
+ return createElement(AuthContext.Provider, { value }, children);
+};
diff --git a/frontend/src/hooks/useAuth.jsx b/frontend/src/hooks/useAuth.jsx
new file mode 100644
index 0000000..60e38d5
--- /dev/null
+++ b/frontend/src/hooks/useAuth.jsx
@@ -0,0 +1,74 @@
+import React, { useState, useEffect, createContext, useContext } from 'react';
+
+const AuthContext = createContext();
+
+// eslint-disable-next-line react-refresh/only-export-components
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+};
+
+export const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ // Check for existing token and validate user
+ const token = localStorage.getItem('token');
+ if (token) {
+ // Validate token and get user info
+ fetchUser(token);
+ } else {
+ setLoading(false);
+ }
+ }, []);
+
+ const fetchUser = async (token) => {
+ try {
+ const response = await fetch('/api/users/profile', {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ if (response.ok) {
+ const userData = await response.json();
+ setUser(userData);
+ } else {
+ localStorage.removeItem('token');
+ }
+ } catch (error) {
+ console.error('Error fetching user:', error);
+ localStorage.removeItem('token');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const logout = () => {
+ localStorage.removeItem('token');
+ setUser(null);
+ window.location.href = '/login';
+ };
+
+ const login = (token, userData) => {
+ localStorage.setItem('token', token);
+ setUser(userData);
+ };
+
+ const value = {
+ user,
+ logout,
+ login,
+ loading,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/frontend/src/lib/utils.js b/frontend/src/lib/utils.js
index 20aa603..0ae73e1 100644
--- a/frontend/src/lib/utils.js
+++ b/frontend/src/lib/utils.js
@@ -1,6 +1,6 @@
-import { clsx } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
export function cn(...inputs) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
}
diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx
new file mode 100644
index 0000000..49e16af
--- /dev/null
+++ b/frontend/src/pages/Settings.jsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import { AccountDangerZone } from '@/Components/settings/AccountDangerZone';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/Components/ui/Card';
+import { Bell, Palette, Shield, User } from 'lucide-react';
+
+export const Settings = () => {
+ return (
+
+
+
+
Settings
+
+ Manage your account settings and preferences.
+
+
+
+ {/* Profile Settings */}
+
+
+
+
+ Profile
+
+
+ Update your personal information and profile settings.
+
+
+
+
+ Profile settings section - to be implemented
+
+
+
+
+ {/* Notification Settings */}
+
+
+
+
+ Notifications
+
+
+ Configure your notification preferences.
+
+
+
+
+ Notification settings section - to be implemented
+
+
+
+
+ {/* Privacy & Security */}
+
+
+
+
+ Privacy & Security
+
+
+ Manage your privacy and security settings.
+
+
+
+
+ Privacy and security settings section - to be implemented
+
+
+
+
+ {/* Appearance */}
+
+
+
+
+ Appearance
+
+
+ Customize the look and feel of DevSync.
+
+
+
+
+ Theme and appearance settings section - to be implemented
+
+
+
+
+ {/* Account Management */}
+
+
+
+ );
+};
+
+export default Settings;
\ No newline at end of file