import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertCircle, CheckCircle, Clock, Copy, Download, Eye, EyeOff, Key, LogOut, Mail, MapPin, Monitor, Moon, RefreshCw, Save, Shield, Smartphone, Sun, Trash2, User, } from "lucide-react"; import { useEffect, useId, useState } from "react"; import { useAuth } from "../contexts/AuthContext"; import { THEME_PRESETS, useColorTheme } from "../contexts/ColorThemeContext"; import { useTheme } from "../contexts/ThemeContext"; import { isCorsError, 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 { toggleTheme, isDark } = useTheme(); const { colorTheme, setColorTheme } = useColorTheme(); 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 || "", }); // Update profileData when user data changes useEffect(() => { if (user) { setProfileData({ username: user.username || "", email: user.email || "", first_name: user.first_name || "", last_name: user.last_name || "", }); } }, [user]); 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) { if (isCorsError(error)) { setMessage({ type: "error", text: "CORS_ORIGIN mismatch - please set your URL in your environment variable", }); } else { 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) { if (isCorsError(error)) { setMessage({ type: "error", text: "CORS_ORIGIN mismatch - please set your URL in your environment variable", }); } else { 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: "sessions", name: "Active Sessions", icon: Monitor }, ]; 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

{/* Theme Settings */}

Appearance

{isDark ? ( ) : ( )}

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

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

{/* Color Theme Settings */}

Color Theme

Choose your preferred color scheme for the application

{Object.entries(THEME_PRESETS).map(([themeKey, theme]) => { const isSelected = colorTheme === themeKey; const gradientColors = theme.login.xColors; return ( ); })}
)} {/* Change Password Tab */} {activeTab === "password" && (

Change Password

Must be at least 6 characters long

)} {/* Multi-Factor Authentication Tab */} {activeTab === "tfa" && } {/* Sessions Tab */} {activeTab === "sessions" && }
); }; // TFA Tab Component const TfaTab = () => { const verificationTokenId = useId(); const disablePasswordId = useId(); const [setupStep, setSetupStep] = useState("status"); // 'status', 'setup', 'verify', 'backup-codes' const [verificationToken, setVerificationToken] = useState(""); const [password, setPassword] = useState(""); const [backupCodes, setBackupCodes] = useState([]); 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: () => { 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 { // 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 />
)}
); }; // Sessions Tab Component const SessionsTab = () => { const _queryClient = useQueryClient(); const [_isLoading, _setIsLoading] = useState(false); const [message, setMessage] = useState({ type: "", text: "" }); // Fetch user sessions const { data: sessionsData, isLoading: sessionsLoading, refetch, } = useQuery({ queryKey: ["user-sessions"], queryFn: async () => { const response = await fetch("/api/v1/auth/sessions", { headers: { Authorization: `Bearer ${localStorage.getItem("token")}`, }, }); if (!response.ok) throw new Error("Failed to fetch sessions"); return response.json(); }, }); // Revoke individual session mutation const revokeSessionMutation = useMutation({ mutationFn: async (sessionId) => { const response = await fetch(`/api/v1/auth/sessions/${sessionId}`, { method: "DELETE", headers: { Authorization: `Bearer ${localStorage.getItem("token")}`, }, }); if (!response.ok) throw new Error("Failed to revoke session"); return response.json(); }, onSuccess: () => { setMessage({ type: "success", text: "Session revoked successfully" }); refetch(); }, onError: (error) => { setMessage({ type: "error", text: error.message }); }, }); // Revoke all sessions mutation const revokeAllSessionsMutation = useMutation({ mutationFn: async () => { const response = await fetch("/api/v1/auth/sessions", { method: "DELETE", headers: { Authorization: `Bearer ${localStorage.getItem("token")}`, }, }); if (!response.ok) throw new Error("Failed to revoke sessions"); return response.json(); }, onSuccess: () => { setMessage({ type: "success", text: "All other sessions revoked successfully", }); refetch(); }, onError: (error) => { setMessage({ type: "error", text: error.message }); }, }); const formatDate = (dateString) => { return new Date(dateString).toLocaleString(); }; const formatRelativeTime = (dateString) => { const now = new Date(); const date = new Date(dateString); const diff = now - date; const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (days > 0) return `${days} day${days > 1 ? "s" : ""} ago`; if (hours > 0) return `${hours} hour${hours > 1 ? "s" : ""} ago`; if (minutes > 0) return `${minutes} minute${minutes > 1 ? "s" : ""} ago`; return "Just now"; }; const handleRevokeSession = (sessionId) => { if (window.confirm("Are you sure you want to revoke this session?")) { revokeSessionMutation.mutate(sessionId); } }; const handleRevokeAllSessions = () => { if ( window.confirm( "Are you sure you want to revoke all other sessions? This will log you out of all other devices.", ) ) { revokeAllSessionsMutation.mutate(); } }; return (
{/* Header */}

Active Sessions

Manage your active sessions and devices. You can see where you're logged in and revoke access for any device.

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

{message.text}

)} {/* Sessions List */} {sessionsLoading ? (
) : sessionsData?.sessions?.length > 0 ? (
{/* Revoke All Button */} {sessionsData.sessions.filter((s) => !s.is_current_session).length > 0 && (
)} {/* Sessions */} {sessionsData.sessions.map((session) => (

{session.device_info?.browser} on{" "} {session.device_info?.os}

{session.is_current_session && ( Current Session )} {session.tfa_remember_me && ( Remembered )}

{session.device_info?.device} • {session.ip_address}

{session.location_info?.city},{" "} {session.location_info?.country}
Last active: {formatRelativeTime(session.last_activity)}
Created: {formatDate(session.created_at)}
Login count: {session.login_count}
{!session.is_current_session && ( )}
))}
) : (

No active sessions

You don't have any active sessions at the moment.

)}
); }; export default Profile;