import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertCircle, CheckCircle, Copy, Download, Eye, EyeOff, Key, Mail, Moon, QrCode, RefreshCw, Save, Settings, Shield, Smartphone, Sun, Trash2, User, } from "lucide-react"; import React, { useState } from "react"; import { useAuth } from "../contexts/AuthContext"; import { useTheme } from "../contexts/ThemeContext"; import { tfaAPI } from "../utils/api"; const Profile = () => { const usernameId = useId(); const emailId = useId(); const firstNameId = useId(); const lastNameId = useId(); const currentPasswordId = useId(); const newPasswordId = useId(); const confirmPasswordId = useId(); const { user, updateProfile, changePassword } = useAuth(); const { theme, toggleTheme, isDark } = useTheme(); const [activeTab, setActiveTab] = useState("profile"); const [isLoading, setIsLoading] = useState(false); const [message, setMessage] = useState({ type: "", text: "" }); const [profileData, setProfileData] = useState({ username: user?.username || "", email: user?.email || "", first_name: user?.first_name || "", last_name: user?.last_name || "", }); const [passwordData, setPasswordData] = useState({ currentPassword: "", newPassword: "", confirmPassword: "", }); const [showPasswords, setShowPasswords] = useState({ current: false, new: false, confirm: false, }); const handleProfileSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setMessage({ type: "", text: "" }); try { const result = await updateProfile(profileData); if (result.success) { setMessage({ type: "success", text: "Profile updated successfully!" }); } else { setMessage({ type: "error", text: result.error || "Failed to update profile", }); } } catch (error) { setMessage({ type: "error", text: "Network error occurred" }); } finally { setIsLoading(false); } }; const handlePasswordSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setMessage({ type: "", text: "" }); if (passwordData.newPassword !== passwordData.confirmPassword) { setMessage({ type: "error", text: "New passwords do not match" }); setIsLoading(false); return; } if (passwordData.newPassword.length < 6) { setMessage({ type: "error", text: "New password must be at least 6 characters", }); setIsLoading(false); return; } try { const result = await changePassword( passwordData.currentPassword, passwordData.newPassword, ); if (result.success) { setMessage({ type: "success", text: "Password changed successfully!" }); setPasswordData({ currentPassword: "", newPassword: "", confirmPassword: "", }); } else { setMessage({ type: "error", text: result.error || "Failed to change password", }); } } catch (error) { setMessage({ type: "error", text: "Network error occurred" }); } finally { setIsLoading(false); } }; const handleInputChange = (e) => { const { name, value } = e.target; if (activeTab === "profile") { setProfileData((prev) => ({ ...prev, [name]: value })); } else { setPasswordData((prev) => ({ ...prev, [name]: value })); } }; const togglePasswordVisibility = (field) => { setShowPasswords((prev) => ({ ...prev, [field]: !prev[field] })); }; const tabs = [ { id: "profile", name: "Profile Information", icon: User }, { id: "password", name: "Change Password", icon: Key }, { id: "tfa", name: "Multi-Factor Authentication", icon: Smartphone }, { id: "preferences", name: "Preferences", icon: Settings }, ]; return (
{/* Header */}

Manage your account information and security settings

{/* User Info Card */}

{user?.first_name && user?.last_name ? `${user.first_name} ${user.last_name}` : user?.first_name || user?.username}

{user?.email}

{user?.role?.charAt(0).toUpperCase() + user?.role?.slice(1).replace("_", " ")}
{/* Tabs */}
{/* Success/Error Message */} {message.text && (
{message.type === "success" ? ( ) : ( )}

{message.text}

)} {/* Profile Information Tab */} {activeTab === "profile" && (

Profile Information

)} {/* Change Password Tab */} {activeTab === "password" && (

Change Password

Must be at least 6 characters long

)} {/* Multi-Factor Authentication Tab */} {activeTab === "tfa" && } {/* Preferences Tab */} {activeTab === "preferences" && (

Preferences

{/* Theme Settings */}

Appearance

{isDark ? ( ) : ( )}

{isDark ? "Dark Mode" : "Light Mode"}

{isDark ? "Switch to light mode" : "Switch to dark mode"}

)}
); }; // TFA Tab Component const TfaTab = () => { const [setupStep, setSetupStep] = useState("status"); // 'status', 'setup', 'verify', 'backup-codes' const [verificationToken, setVerificationToken] = useState(""); const [password, setPassword] = useState(""); const [backupCodes, setBackupCodes] = useState([]); const [isLoading, setIsLoading] = useState(false); const [message, setMessage] = useState({ type: "", text: "" }); const queryClient = useQueryClient(); // Fetch TFA status const { data: tfaStatus, isLoading: statusLoading } = useQuery({ queryKey: ["tfaStatus"], queryFn: () => tfaAPI.status().then((res) => res.data), }); // Setup TFA mutation const setupMutation = useMutation({ mutationFn: () => tfaAPI.setup().then((res) => res.data), onSuccess: (data) => { setSetupStep("setup"); setMessage({ type: "info", text: "Scan the QR code with your authenticator app and enter the verification code below.", }); }, onError: (error) => { setMessage({ type: "error", text: error.response?.data?.error || "Failed to setup TFA", }); }, }); // Verify setup mutation const verifyMutation = useMutation({ mutationFn: (data) => tfaAPI.verifySetup(data).then((res) => res.data), onSuccess: (data) => { setBackupCodes(data.backupCodes); setSetupStep("backup-codes"); setMessage({ type: "success", text: "Two-factor authentication has been enabled successfully!", }); }, onError: (error) => { setMessage({ type: "error", text: error.response?.data?.error || "Failed to verify TFA setup", }); }, }); // Disable TFA mutation const disableMutation = useMutation({ mutationFn: (data) => tfaAPI.disable(data).then((res) => res.data), onSuccess: () => { queryClient.invalidateQueries(["tfaStatus"]); setSetupStep("status"); setMessage({ type: "success", text: "Two-factor authentication has been disabled successfully!", }); }, onError: (error) => { setMessage({ type: "error", text: error.response?.data?.error || "Failed to disable TFA", }); }, }); // Regenerate backup codes mutation const regenerateBackupCodesMutation = useMutation({ mutationFn: () => tfaAPI.regenerateBackupCodes().then((res) => res.data), onSuccess: (data) => { setBackupCodes(data.backupCodes); setMessage({ type: "success", text: "Backup codes have been regenerated successfully!", }); }, onError: (error) => { setMessage({ type: "error", text: error.response?.data?.error || "Failed to regenerate backup codes", }); }, }); const handleSetup = () => { setupMutation.mutate(); }; const handleVerify = (e) => { e.preventDefault(); if (verificationToken.length !== 6) { setMessage({ type: "error", text: "Please enter a 6-digit verification code", }); return; } verifyMutation.mutate({ token: verificationToken }); }; const handleDisable = (e) => { e.preventDefault(); if (!password) { setMessage({ type: "error", text: "Please enter your password to disable TFA", }); return; } disableMutation.mutate({ password }); }; const handleRegenerateBackupCodes = () => { regenerateBackupCodesMutation.mutate(); }; const copyToClipboard = async (text) => { try { // Try modern clipboard API first if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); setMessage({ type: "success", text: "Copied to clipboard!" }); return; } // Fallback for older browsers or non-secure contexts const textArea = document.createElement("textarea"); textArea.value = text; textArea.style.position = "fixed"; textArea.style.left = "-999999px"; textArea.style.top = "-999999px"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { const successful = document.execCommand("copy"); if (successful) { setMessage({ type: "success", text: "Copied to clipboard!" }); } else { throw new Error("Copy command failed"); } } catch (err) { // If all else fails, show the text in a prompt prompt("Copy this text:", text); setMessage({ type: "info", text: "Text shown in prompt for manual copying", }); } finally { document.body.removeChild(textArea); } } catch (err) { console.error("Failed to copy to clipboard:", err); // Show the text in a prompt as last resort prompt("Copy this text:", text); setMessage({ type: "info", text: "Text shown in prompt for manual copying", }); } }; const downloadBackupCodes = () => { const content = `PatchMon Backup Codes\n\n${backupCodes.map((code, index) => `${index + 1}. ${code}`).join("\n")}\n\nKeep these codes safe! Each code can only be used once.`; const blob = new Blob([content], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "patchmon-backup-codes.txt"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; if (statusLoading) { return (
); } return (

Multi-Factor Authentication

Add an extra layer of security to your account by enabling two-factor authentication.

{/* Status Message */} {message.text && (
{message.type === "success" ? ( ) : message.type === "error" ? ( ) : ( )}

{message.text}

)} {/* TFA Status */} {setupStep === "status" && (

{tfaStatus?.enabled ? "Two-Factor Authentication Enabled" : "Two-Factor Authentication Disabled"}

{tfaStatus?.enabled ? "Your account is protected with two-factor authentication." : "Add an extra layer of security to your account."}

{tfaStatus?.enabled ? ( ) : ( )}
{tfaStatus?.enabled && (

Backup Codes

Use these backup codes to access your account if you lose your authenticator device.

)}
)} {/* TFA Setup */} {setupStep === "setup" && setupMutation.data && (

Setup Two-Factor Authentication

QR Code

Scan this QR code with your authenticator app

Manual Entry Key:

{setupMutation.data.manualEntryKey}
)} {/* TFA Verification */} {setupStep === "verify" && (

Verify Setup

Enter the 6-digit code from your authenticator app to complete the setup.

setVerificationToken( e.target.value.replace(/\D/g, "").slice(0, 6), ) } placeholder="000000" className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white text-center text-lg font-mono tracking-widest" maxLength="6" required />
)} {/* Backup Codes */} {setupStep === "backup-codes" && backupCodes.length > 0 && (

Backup Codes

Save these backup codes in a safe place. Each code can only be used once.

{backupCodes.map((code, index) => (
{index + 1}. {code}
))}
)} {/* Disable TFA */} {setupStep === "disable" && (

Disable Two-Factor Authentication

Enter your password to disable two-factor authentication.

setPassword(e.target.value)} className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white" required />
)}
); }; export default Profile;