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" ? (
) : (
)}
)}
{/* Profile Information Tab */}
{activeTab === "profile" && (
)}
{/* Change Password Tab */}
{activeTab === "password" && (
)}
{/* 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" ? (
) : (
)}
)}
{/* 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
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.
)}
{/* 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.
)}
);
};
// 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" ? (
) : (
)}
)}
{/* 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;