import { useState, useEffect } from 'react'; import { useAuth } from '../components/AuthContext'; import toast from 'react-hot-toast'; import Profile from './Profile'; export const AlertSettingsPage = () => { const { token, user, reloadUser } = useAuth(); const [activeTab, setActiveTab] = useState(() => { return localStorage.getItem('settingsActiveTab') || 'profile'; }); // profile, notifications, apiKeys, team, billing, scheduler useEffect(() => { localStorage.setItem('settingsActiveTab', activeTab); }, [activeTab]); // Notification states (connected to backend) const [emailNotifications, setEmailNotifications] = useState(true); const [webhookUrl, setWebhookUrl] = useState(''); const [severityThreshold, setSeverityThreshold] = useState('Medium'); const [reportLogoUrl, setReportLogoUrl] = useState(''); const [teamUsers, setTeamUsers] = useState([]); const [loadingTeam, setLoadingTeam] = useState(false); const [newUserFirstName, setNewUserFirstName] = useState(''); const [newUserLastName, setNewUserLastName] = useState(''); const [newUserEmail, setNewUserEmail] = useState(''); const [newUserPassword, setNewUserPassword] = useState(''); const [newUserRole, setNewUserRole] = useState('soc_analyst'); const [invitingUser, setInvitingUser] = useState(false); const [showAddMember, setShowAddMember] = useState(false); const [showNewUserPassword, setShowNewUserPassword] = useState(false); const [editingUser, setEditingUser] = useState(null); const [updatingUser, setUpdatingUser] = useState(false); const [userToDelete, setUserToDelete] = useState(null); const [deletingUser, setDeletingUser] = useState(false); 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 [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [message, setMessage] = useState(''); const [error, setError] = useState(''); const [billingHistory, setBillingHistory] = useState([]); const [loadingBilling, setLoadingBilling] = useState(false); const [scanQuotas, setScanQuotas] = useState([]); const [loadingQuotas, setLoadingQuotas] = useState(false); // Demo Bookings State const [demoBookings, setDemoBookings] = useState([]); const [loadingDemoBookings, setLoadingDemoBookings] = useState(false); const [rescheduleModal, setRescheduleModal] = useState({ isOpen: false, bookingId: null, email: '', meetingDate: '', meetingTime: '', status: 'rescheduled' }); const handleUpdateDemoBookingStatus = async (bookingId, status) => { try { const res = await fetch(`/api/demo/bookings/${bookingId}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (res.ok) { toast.success(`Booking status updated to ${status}`); fetchDemoBookings(); } else { toast.error("Failed to update status"); } } catch (err) { toast.error("Error updating status"); } }; const handleOpenReschedule = (booking) => { setRescheduleModal({ isOpen: true, bookingId: booking.id, email: booking.email, meetingDate: booking.meeting_date || '', meetingTime: booking.meeting_time || '', status: 'rescheduled' }); }; const handleSaveReschedule = async (e) => { e.preventDefault(); try { const res = await fetch(`/api/demo/bookings/${rescheduleModal.bookingId}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ meeting_date: rescheduleModal.meetingDate, meeting_time: rescheduleModal.meetingTime, status: rescheduleModal.status }) }); if (res.ok) { toast.success("Demo booking rescheduled successfully!"); setRescheduleModal({ isOpen: false, bookingId: null, email: '', meetingDate: '', meetingTime: '', status: 'rescheduled' }); fetchDemoBookings(); } else { toast.error("Failed to reschedule demo booking"); } } catch (err) { toast.error("Error rescheduling booking"); } }; const handleDeleteDemoBooking = async (bookingId) => { if (!window.confirm("Are you sure you want to delete this booking lead?")) return; try { const res = await fetch(`/api/demo/bookings/${bookingId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { toast.success("Booking lead deleted successfully"); fetchDemoBookings(); } else { toast.error("Failed to delete booking lead"); } } catch (err) { toast.error("Error deleting lead"); } }; // Scheduled Scans const [scheduledScans, setScheduledScans] = useState([]); const [loadingScans, setLoadingScans] = useState(false); const [newSchedule, setNewSchedule] = useState({ target_url: '', scan_type: 'Full', schedule_time: '20:00' }); // Notification History const [notificationHistory, setNotificationHistory] = useState([]); const [loadingHistory, setLoadingHistory] = useState(false); const [sortTeamCol, setSortTeamCol] = useState('Name'); const [sortTeamDir, setSortTeamDir] = useState('asc'); const [sortBillCol, setSortBillCol] = useState('Date'); const [sortBillDir, setSortBillDir] = useState('desc'); const handleTeamSort = (column) => { if (column === 'Actions') return; if (sortTeamCol === column) { setSortTeamDir(sortTeamDir === 'asc' ? 'desc' : 'asc'); } else { setSortTeamCol(column); setSortTeamDir('asc'); } }; const getSortedTeam = () => { return [...teamUsers].sort((a, b) => { let aVal, bVal; switch (sortTeamCol) { case 'Name': aVal = `${a.first_name || ''} ${a.last_name || ''}`.trim(); bVal = `${b.first_name || ''} ${b.last_name || ''}`.trim(); break; case 'Email': aVal = a.email || ''; bVal = b.email || ''; break; case 'Assigned Role': aVal = a.role || ''; bVal = b.role || ''; break; case 'Status': aVal = a.status || ''; bVal = b.status || ''; break; default: return 0; } if (aVal < bVal) return sortTeamDir === 'asc' ? -1 : 1; if (aVal > bVal) return sortTeamDir === 'asc' ? 1 : -1; return 0; }); }; const handleBillSort = (column) => { if (sortBillCol === column) { setSortBillDir(sortBillDir === 'asc' ? 'desc' : 'asc'); } else { setSortBillCol(column); setSortBillDir('desc'); } }; const getSortedBilling = () => { return [...billingHistory].sort((a, b) => { let aVal, bVal; switch (sortBillCol) { case 'Date': aVal = new Date(a.created_at).getTime(); bVal = new Date(b.created_at).getTime(); break; case 'Plan': aVal = a.tier_id || ''; bVal = b.tier_id || ''; break; case 'Amount': aVal = a.amount || 0; bVal = b.amount || 0; break; case 'Status': aVal = a.status || ''; bVal = b.status || ''; break; default: return 0; } if (aVal < bVal) return sortBillDir === 'asc' ? -1 : 1; if (aVal > bVal) return sortBillDir === 'asc' ? 1 : -1; return 0; }); }; useEffect(() => { fetchSettings(); }, [token]); const fetchSettings = async () => { try { const res = await fetch('/api/vulnerabilities/settings', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setEmailNotifications(data.settings.email_notifications); setWebhookUrl(data.settings.webhook_url || ''); setSeverityThreshold(data.settings.severity_threshold); } } catch (err) { console.error("Error loading alert settings", err); } finally { setLoading(false); } }; const fetchNotificationHistory = async () => { setLoadingHistory(true); try { const res = await fetch('/api/auth/notifications', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setNotificationHistory(data.notifications || []); } } catch (err) { console.error("Error loading notification history", err); } finally { setLoadingHistory(false); } }; const fetchTeamUsers = async () => { setLoadingTeam(true); try { const endpoint = user?.org_id ? `/api/auth/organizations/${user.org_id}/users` : '/api/auth/users'; const res = await fetch(endpoint, { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); const filteredUsers = (data.users || []).filter(u => u.role !== 'super_admin'); setTeamUsers(filteredUsers); } } catch (err) { console.error('Failed to fetch org users', err); } finally { setLoadingTeam(false); } }; useEffect(() => { if (activeTab === 'team' && (user?.role === 'org_admin' || user?.role === 'super_admin')) { fetchTeamUsers(); } }, [activeTab, user]); const handleInviteUser = async (e) => { e.preventDefault(); if (!newUserEmail) return; setInvitingUser(true); try { const res = await fetch('/api/auth/users/invite', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ email: newUserEmail, role: newUserRole, first_name: newUserFirstName, last_name: newUserLastName, password: newUserPassword }) }); const data = await res.json(); if (res.ok) { toast.success(`User added successfully!`); setNewUserFirstName(''); setNewUserLastName(''); setNewUserEmail(''); setNewUserPassword(''); setShowAddMember(false); setShowNewUserPassword(false); fetchTeamUsers(); } else { toast.error(data.message || 'Failed to invite user'); } } catch (err) { toast.error("Error inviting user"); } finally { setInvitingUser(false); } }; const handleUpdateUser = async (e) => { e.preventDefault(); if (!editingUser) return; setUpdatingUser(true); try { const res = await fetch(`/api/auth/users/${editingUser.id}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ first_name: editingUser.first_name, last_name: editingUser.last_name, email: editingUser.email, password: editingUser.new_password, role: editingUser.role }) }); if (res.ok) { toast.success(`User updated successfully!`); setEditingUser(null); fetchTeamUsers(); } else { const data = await res.json(); toast.error(data.message || 'Failed to update user'); } } catch (err) { toast.error("Error updating user"); } finally { setUpdatingUser(false); } }; const executeDeleteUser = async () => { if (!userToDelete) return; setDeletingUser(true); try { const res = await fetch(`/api/auth/users/${userToDelete.id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { toast.success("User removed successfully!"); setUserToDelete(null); fetchTeamUsers(); } else { const data = await res.json(); toast.error(data.message || "Failed to remove user"); } } catch (err) { toast.error("Error removing user"); } finally { setDeletingUser(false); } }; 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.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"); } }; // Auto-refresh user data (e.g., subscription upgrades) when Billing tab is active useEffect(() => { let intervalId; if (activeTab === 'billing' && reloadUser) { intervalId = setInterval(() => { reloadUser(); fetchQuotas(true); fetchBillingHistory(true); }, 15000); // Poll every 15 seconds silently } return () => { if (intervalId) clearInterval(intervalId); }; }, [activeTab, reloadUser]); useEffect(() => { if (activeTab === 'billing') { fetchBillingHistory(); fetchQuotas(); } if (activeTab === 'notifications') { fetchNotificationHistory(); } }, [activeTab, user?.org_id, user?.id]); const fetchQuotas = async (isSilent = false) => { if (!isSilent && scanQuotas.length === 0) setLoadingQuotas(true); try { // If user has organization_id, fetch from it. Otherwise we might fetch from a general endpoint if it existed, or we just try to fetch the first organization. // Usually users belong to one organization. Let's try to get their organization ID first, or fetch from /api/auth/organizations let orgId = user?.org_id; if (!orgId) { const orgRes = await fetch('/api/auth/organizations', { headers: { 'Authorization': `Bearer ${token}` } }); if (orgRes.ok) { const orgsData = await orgRes.json(); if (orgsData.organizations && orgsData.organizations.length > 0) { orgId = orgsData.organizations[0].id; } } } if (orgId) { const res = await fetch(`/api/auth/organizations/${orgId}/quotas`, { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setScanQuotas(data.quotas || []); } } } catch (err) { console.error("Error loading quotas", err); } finally { setLoadingQuotas(false); } }; const fetchBillingHistory = async (isSilent = false) => { if (!isSilent && billingHistory.length === 0) setLoadingBilling(true); try { const res = await fetch('/api/billing/history', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setBillingHistory(data.history || []); } } catch (err) { console.error("Error loading billing history", err); } finally { setLoadingBilling(false); } }; useEffect(() => { if (activeTab === 'scheduler') { fetchScheduledScans(); } }, [activeTab, token]); const fetchScheduledScans = async () => { setLoadingScans(true); try { const res = await fetch('/api/scans/schedule', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setScheduledScans(data.schedules || []); } } catch (err) { console.error("Error loading scheduled scans", err); } finally { setLoadingScans(false); } }; const fetchDemoBookings = async () => { setLoadingDemoBookings(true); try { const res = await fetch('/api/demo/bookings', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setDemoBookings(data || []); } } catch (err) { console.error("Error loading demo bookings", err); } finally { setLoadingDemoBookings(false); } }; useEffect(() => { if (activeTab === 'demoBookings') { fetchDemoBookings(); } }, [activeTab, token]); const handleCreateSchedule = async (e) => { e.preventDefault(); setMessage(''); setError(''); if (user?.subscription_tier === 'Free') { setError('Scheduled scans require a premium subscription.'); return; } try { const res = await fetch('/api/scans/schedule', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ ...newSchedule, frequency: 'daily' }) }); const data = await res.json(); if (res.ok) { setMessage('Scan scheduled successfully!'); setNewSchedule({ target_url: '', scan_type: 'Full', schedule_time: '20:00' }); fetchScheduledScans(); } else { setError(data.message || 'Failed to schedule scan.'); } } catch (err) { setError('Connection error.'); } }; const handleDeleteSchedule = async (id) => { try { const res = await fetch(`/api/scans/schedule/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { setScheduledScans(prev => prev.filter(s => s.id !== id)); } } catch (err) { console.error('Failed to delete schedule', err); } }; const handleSave = async (e) => { e.preventDefault(); setSaving(true); setMessage(''); setError(''); try { const res = await fetch('/api/vulnerabilities/settings', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ email_notifications: emailNotifications, webhook_url: webhookUrl, severity_threshold: severityThreshold }) }); const data = await res.json(); if (res.ok) { setMessage('Alert preferences successfully updated!'); } else { setError(data.message || 'Failed to update alert configurations.'); } } catch (err) { setError('Could not establish connection to the security server API.'); console.error(err); } finally { setSaving(false); } }; const isSchedulerAllowed = ['super_admin', 'support_engineer', 'admin', 'org_admin', 'soc_analyst'].includes(user?.role); const isDemoBookingsAllowed = ['super_admin', 'support_engineer', 'admin', 'org_admin', 'soc_analyst'].includes(user?.role); const isNotificationsAllowed = ['super_admin', 'support_engineer', 'admin', 'org_admin'].includes(user?.role); const isApiKeysAllowed = ['super_admin', 'support_engineer', 'admin', 'org_admin'].includes(user?.role); const isTeamAllowed = ['super_admin', 'support_engineer', 'admin', 'org_admin'].includes(user?.role); const isBillingAllowed = ['super_admin', 'support_engineer', 'admin', 'org_admin'].includes(user?.role); const tabItems = [ { id: 'profile', label: 'My Profile', icon: 'person' } ]; if (isSchedulerAllowed) tabItems.push({ id: 'scheduler', label: 'Scheduler', icon: 'schedule' }); if (isDemoBookingsAllowed) tabItems.push({ id: 'demoBookings', label: 'Demo Bookings', icon: 'event_available' }); if (isNotificationsAllowed) tabItems.push({ id: 'notifications', label: 'Notifications', icon: 'notifications' }); if (isApiKeysAllowed) tabItems.push({ id: 'apiKeys', label: 'API Keys', icon: 'key' }); if (isTeamAllowed) tabItems.push({ id: 'team', label: 'Team', icon: 'group' }); if (isBillingAllowed) tabItems.push({ id: 'billing', label: 'Billing', icon: 'credit_card' }); useEffect(() => { const isAllowed = tabItems.some(t => t.id === activeTab); if (!isAllowed) { setActiveTab('profile'); } }, [user?.role, activeTab]); if (loading) { return (
Manage your account preferences, security protocols, and team access.
{/* Scrollable Tabs row */}Set up daily automated scans for your targets. The scanner will run automatically at your specified time.
{message && (Loading schedules...
) : scheduledScans.length === 0 ? (No automated scans scheduled yet.
) : (No demo bookings found.
No recent notifications.
Current Plan
| handleBillSort(h)}
className="p-sm font-label-sm text-on-surface-variant uppercase cursor-pointer hover:bg-surface-container-highest transition-colors group"
>
{h}
{sortBillCol === h && sortBillDir === 'desc' ? 'arrow_downward' : 'arrow_upward'}
|
))}
|||
|---|---|---|---|
| {new Date(tx.created_at).toLocaleDateString()} | {tx.tier_id} | ${(tx.amount / 100).toFixed(2)} | {tx.status} |
| handleTeamSort(h)}
className={`px-6 py-4 font-semibold ${i === 4 ? 'text-right' : ''} ${h !== 'Actions' ? 'cursor-pointer hover:bg-surface-container-highest transition-colors group' : ''}`}
>
{h}
{h !== 'Actions' && (
{sortTeamCol === h && sortTeamDir === 'desc' ? 'arrow_downward' : 'arrow_upward'}
)}
|
))}
||||
|---|---|---|---|---|
| {member.first_name || member.last_name ? `${member.first_name || ''} ${member.last_name || ''}`.trim() : 'N/A'} | {member.email} | {member.role.replace('_', ' ')} | {member.status} |
Are you sure you want to remove {userToDelete.email} from the organization? This action cannot be undone and they will lose all access immediately.
We are actively building a robust, high-performance API for LarShield. Soon, you will be able to programmatically manage scans, retrieve security reports, and integrate seamlessly with your CI/CD pipelines.