import { useState, useEffect, useRef } from 'react'; import { useAuth } from '../components/AuthContext'; import { getInitials } from '../components/Layout'; import { toast } from 'react-hot-toast'; export const Profile = () => { const { token, logout } = useAuth(); const fileInputRef = useRef(null); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [reportLogoUrl, setReportLogoUrl] = useState(''); useEffect(() => { const fetchProfile = async () => { try { const res = await fetch('/api/auth/profile', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setProfile(data.user); } else { setError("Failed to fetch profile data. Please try again."); } } catch (err) { setError("Network error while fetching profile data."); console.error(err); } finally { setLoading(false); } }; const fetchBranding = async () => { try { const res = await fetch('/api/auth/organizations/webhook', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setReportLogoUrl(data.report_logo_url || ''); } } catch (err) { console.error("Error loading branding info", err); } }; fetchProfile(); fetchBranding(); }, [token]); const fetchBrandingManual = async () => { try { const res = await fetch('/api/auth/organizations/webhook', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setReportLogoUrl(data.report_logo_url || ''); } } catch (err) { console.error("Error loading branding info", err); } }; useEffect(() => { const preventWindowDrop = (e) => { e.preventDefault(); }; window.addEventListener('dragover', preventWindowDrop); window.addEventListener('drop', preventWindowDrop); return () => { window.removeEventListener('dragover', preventWindowDrop); window.removeEventListener('drop', preventWindowDrop); }; }, []); const [isDragging, setIsDragging] = useState(false); const [uploadingLogo, setUploadingLogo] = useState(false); const handleUploadLogoUrl = async (imageUrl) => { setUploadingLogo(true); try { const res = await fetch('/api/auth/organizations/logo', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ logo_url: imageUrl }) }); if (res.ok) { toast.success("Report branding updated! Future PDF reports will include your logo."); fetchBrandingManual(); } else { const data = await res.json(); toast.error(data.message || "Failed to download and process web image."); } } catch (err) { toast.error("Error uploading logo URL."); } finally { setUploadingLogo(false); } }; const handleUploadLogoFile = async (file) => { if (!file) return; if (file.size > 5 * 1024 * 1024) { toast.error("File size exceeds 5MB limit."); return; } setUploadingLogo(true); const formData = new FormData(); formData.append('logo', file); try { const res = await fetch('/api/auth/organizations/logo', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData }); const data = await res.json(); if (res.ok) { if (data.report_logo_url) { setReportLogoUrl(data.report_logo_url); } toast.success("Report branding updated! Future PDF reports will include your logo."); fetchBrandingManual(); } else { toast.error(data.message || "Failed to update report branding."); } } catch (err) { toast.error("Error uploading logo."); } finally { setUploadingLogo(false); } }; const handleRemoveLogo = async () => { try { const res = await fetch('/api/auth/organizations/logo', { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { setReportLogoUrl(''); toast.success("Logo removed successfully."); fetchBrandingManual(); } else { toast.error("Failed to remove logo."); } } catch (err) { toast.error("Error removing logo."); } }; const handleDragOver = (e) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer) { e.dataTransfer.dropEffect = 'copy'; } if (!isDragging) setIsDragging(true); }; const handleDragLeave = (e) => { e.preventDefault(); e.stopPropagation(); // Only set false if leaving the main drop container if (e.currentTarget && e.relatedTarget && e.currentTarget.contains(e.relatedTarget)) { return; } setIsDragging(false); }; const handleDrop = async (e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); // 1. Direct File Drop (from File Explorer or Desktop) if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { const file = e.dataTransfer.files[0]; if (file && (file.type.startsWith('image/') || file.type === '' || file.name.match(/\.(png|jpe?g|webp|svg|gif|bmp)$/i))) { await handleUploadLogoFile(file); return; } } // 2. DataTransfer Items (Dragging image element or file item from browser window) if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { for (let i = 0; i < e.dataTransfer.items.length; i++) { const item = e.dataTransfer.items[i]; if (item.kind === 'file') { const file = item.getAsFile(); if (file && (file.type.startsWith('image/') || file.name.match(/\.(png|jpe?g|webp|svg|gif|bmp)$/i))) { await handleUploadLogoFile(file); return; } } } } // 3. Chrome / Web Image / HTML / URL Drag const htmlData = e.dataTransfer.getData('text/html'); const uriData = e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('URL') || e.dataTransfer.getData('text/plain'); let imageUrl = ''; if (htmlData) { try { const parser = new DOMParser(); const doc = parser.parseFromString(htmlData, 'text/html'); const img = doc.querySelector('img'); if (img && img.src) { imageUrl = img.src; } } catch (err) { console.warn("Could not parse dragged HTML", err); } } if (!imageUrl && uriData && uriData.trim().match(/^https?:\/\/.+/i)) { imageUrl = uriData.trim(); } if (imageUrl) { // Base64 Data URL handling if (imageUrl.startsWith('data:image/')) { try { const arr = imageUrl.split(','); const mime = arr[0].match(/:(.*?);/)[1]; const bstr = atob(arr[1]); let n = bstr.length; const u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } const file = new File([u8arr], 'dragged_logo.png', { type: mime }); await handleUploadLogoFile(file); } catch (err) { toast.error("Invalid base64 image data."); } return; } // Web HTTP/HTTPS URL handling - First try frontend fetch, fallback to backend fetch setUploadingLogo(true); try { const res = await fetch(imageUrl, { mode: 'cors' }); if (!res.ok) throw new Error("CORS or HTTP error"); const blob = await res.blob(); const contentType = blob.type || 'image/png'; const fileExt = contentType.split('/')[1] || 'png'; const file = new File([blob], `dragged_logo.${fileExt}`, { type: contentType }); await handleUploadLogoFile(file); } catch (frontendErr) { // Fallback: send web image URL to backend to download server-side (bypasses CORS!) await handleUploadLogoUrl(imageUrl); } finally { setUploadingLogo(false); } return; } toast.error("Please drop a valid image file (PNG, JPG, WebP, SVG)."); }; const [passwordData, setPasswordData] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' }); const [passwordStatus, setPasswordStatus] = useState({ loading: false, error: null, success: false }); const [showPassword, setShowPassword] = useState({ current: false, new: false, confirm: false }); const [showEditModal, setShowEditModal] = useState(false); const [editData, setEditData] = useState({ first_name: '', last_name: '', email: '', contact_no: '', org_name: '' }); const [editStatus, setEditStatus] = useState({ loading: false, error: null }); const handleEditOpen = () => { setEditData({ first_name: profile.first_name || '', last_name: profile.last_name || '', email: profile.email || '', contact_no: profile.contact_no || '', org_name: profile.org_name || 'LarShield Organization' }); setShowEditModal(true); }; const handleEditSubmit = async (e) => { e.preventDefault(); setEditStatus({ loading: true, error: null }); try { // Update User Profile const userRes = await fetch(`/api/auth/users/${profile.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ ...profile, first_name: editData.first_name, last_name: editData.last_name, email: editData.email, contact_no: editData.contact_no }) }); const userData = await userRes.json(); if (!userRes.ok) { setEditStatus({ loading: false, error: userData.message || "Failed to update profile" }); toast.error(userData.message || "Failed to update profile"); return; } // Update Organization Name if changed and user has permission if (editData.org_name !== profile.org_name && (profile.role === 'org_admin' || profile.role === 'super_admin')) { const orgRes = await fetch(`/api/auth/organizations/${profile.org_id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ name: editData.org_name }) }); if (!orgRes.ok) { const orgData = await orgRes.json(); toast.error(orgData.message || "Failed to update organization name"); } } toast.success("Profile updated successfully!"); setProfile({ ...profile, first_name: editData.first_name, last_name: editData.last_name, email: editData.email, contact_no: editData.contact_no, org_name: editData.org_name }); setShowEditModal(false); setEditStatus({ loading: false, error: null }); } catch (err) { setEditStatus({ loading: false, error: "Network error" }); toast.error("Network error"); } }; const handlePasswordChange = async (e) => { e.preventDefault(); setPasswordStatus({ loading: true, error: null, success: false }); if (passwordData.newPassword !== passwordData.confirmPassword) { setPasswordStatus({ loading: false, error: "New passwords do not match", success: false }); return; } if (passwordData.newPassword.length < 6) { setPasswordStatus({ loading: false, error: "New password must be at least 6 characters", success: false }); return; } try { const res = await fetch('/api/auth/password', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ currentPassword: passwordData.currentPassword, newPassword: passwordData.newPassword }) }); let data = {}; try { data = await res.json(); } catch (e) { if (res.status === 429) { data = { message: "Too many attempts. Please try again later." }; } else { data = { message: "Unexpected server error occurred." }; } } if (res.ok) { setPasswordStatus({ loading: false, error: null, success: true }); setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' }); toast.success("Password updated successfully!"); setTimeout(() => setPasswordStatus(prev => ({ ...prev, success: false })), 3000); } else { const errorMsg = data.message || "Failed to update password"; setPasswordStatus({ loading: false, error: errorMsg, success: false }); toast.error(errorMsg); } } catch (err) { setPasswordStatus({ loading: false, error: "Network error occurred", success: false }); toast.error("Network error occurred"); } }; if (loading) { return (
sync Loading Profile Data...
); } if (error || !profile) { return (
error

Unable to load profile

{error || "Profile data not found."}

); } return (
{/* Main Grid Layout - 3 Equal Columns */}
{/* Card 1: Identity Card */}

person Organization Profile

{getInitials(profile)}

{profile.first_name || profile.last_name ? `${profile.first_name || ''} ${profile.last_name || ''}`.trim() : (profile.name || profile.email.split('@')[0])}

{(profile.role || 'user').replace(/_/g, ' ')}
mail Email
{profile.email}
calendar_today Joined
{profile.created_at ? new Date(profile.created_at).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }).replace(/ /g, '-') : 'N/A'}
{/* Card 2: Security Configuration */}

security Security Config

Password Management

Update your account password securely.

{(profile.role === 'soc_analyst' || profile.role === 'executive_user') ? (
lock

Your account type is not permitted to change its own password. Please contact your administrator.

) : (
{passwordStatus.error && (
error {passwordStatus.error}
)} {passwordStatus.success && (
check_circle Password updated successfully!
)}
setPasswordData({ ...passwordData, currentPassword: e.target.value })} className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors pr-10" />
setPasswordData({ ...passwordData, newPassword: e.target.value })} className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors pr-10" />
setPasswordData({ ...passwordData, confirmPassword: e.target.value })} className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors pr-10" />
)}
{/* Card 3: Subscription Card */}

workspace_premium Subscription Status

inventory_2 Current Plan
{profile.role === 'super_admin' ? 'Enterprise' : profile.subscription_tier || 'Free'}
speed Status
{profile.role === 'super_admin' ? 'Active' : profile.subscription_status || 'Inactive'}
info

Need higher API limits, custom proxy integrations, or VIP enterprise support? Contact your organization admin or LarShield sales to upgrade your tier.

{/* Second Row for Report Branding */} {(profile.role === 'super_admin' || profile.role === 'org_admin') && (
palette

Report Branding

Customize generated PDF security reports with your organization's logo.

{reportLogoUrl && ( )}
fileInputRef.current?.click()} className={`relative border-2 border-dashed rounded-xl py-10 px-6 flex flex-col items-center justify-center text-center transition-all duration-200 cursor-pointer ${ isDragging ? 'border-[#2563eb] bg-[#eff6ff] scale-[1.005]' : 'border-[#d1d5db] bg-[#f9fafb] hover:border-[#9ca3af] hover:bg-[#f3f4f6]' }`} > { if (e.target.files && e.target.files[0]) { handleUploadLogoFile(e.target.files[0]); } }} className="hidden" /> {reportLogoUrl ? ( <>
Organization Logo { if (reportLogoUrl && !e.target.dataset.retried) { e.target.dataset.retried = '1'; if (reportLogoUrl.startsWith('/uploads/')) { e.target.src = `/api/auth${reportLogoUrl}`; } else if (reportLogoUrl.startsWith('/api/auth/uploads/')) { e.target.src = reportLogoUrl.replace('/api/auth', ''); } else if (!reportLogoUrl.startsWith('http')) { e.target.src = `/uploads/logos/${reportLogoUrl.split('/').pop()}`; } } }} />

Or drag and drop a new logo file above (PNG, JPG, WebP, SVG up to 5MB)

) : ( <>
cloud_upload

Upload Organization Logo

Upload your custom logo to brand all PDF security reports

Supported formats: PNG, JPG, WebP, SVG (Max 5MB)

)}
)} {/* Edit Profile Modal */} {showEditModal && (

edit Edit Profile

{editStatus.error && (
error {editStatus.error}
)}
setEditData({ ...editData, first_name: e.target.value })} className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors" placeholder="Enter first name" />
setEditData({ ...editData, last_name: e.target.value })} className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors" placeholder="Enter last name" />
setEditData({ ...editData, email: e.target.value })} className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors" placeholder="Enter email address" required />
setEditData({ ...editData, contact_no: e.target.value })} className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors" placeholder="+1 (555) 000-0000" />
{profile.role !== 'org_admin' && profile.role !== 'super_admin' && ( Admin Only )}
setEditData({ ...editData, org_name: e.target.value })} disabled={profile.role !== 'org_admin' && profile.role !== 'super_admin'} className={`w-full rounded-lg px-md py-sm outline-none transition-colors ${profile.role === 'org_admin' || profile.role === 'super_admin' ? 'bg-surface-container border border-outline-variant text-on-surface focus:border-primary focus:ring-1 focus:ring-primary' : 'bg-surface-variant/50 border border-outline-variant text-on-surface-variant cursor-not-allowed'}`} />
)}
); }; export default Profile;