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. +

+
+ +
+ + {/* Delete Account */} +
+
+

Delete Account

+

+ Permanently remove your account and all associated data. +

+
+ +
+
+ + {/* 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. +

+
+ +
+ + {/* Delete Account */} +
+
+

Delete Account

+

+ Permanently remove your account and all associated data. +

+
+ +
+
+ + {/* 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' && ( +
+ + 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 */} +
+ + +
+
+
+
+ ); +}; + 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 ( - + ); -} +}); + +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) => (