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 (
{error || "Profile data not found."}
Update your account password securely.
Your account type is not permitted to change its own password. Please contact your administrator.
Need higher API limits, custom proxy integrations, or VIP enterprise support? Contact your organization admin or LarShield sales to upgrade your tier.
Customize generated PDF security reports with your organization's logo.
Or drag and drop a new logo file above (PNG, JPG, WebP, SVG up to 5MB)
> ) : ( <>Upload your custom logo to brand all PDF security reports
Supported formats: PNG, JPG, WebP, SVG (Max 5MB)
> )}