diff --git a/src/components/conferences/conference-details.jsx b/src/components/conferences/conference-details.jsx index 0db5819..a737ab6 100644 --- a/src/components/conferences/conference-details.jsx +++ b/src/components/conferences/conference-details.jsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useAuth } from '@/contexts/AuthContext'; +import { useApi } from '@/hooks/use-api'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { Button } from '@/components/ui/button'; @@ -16,6 +17,7 @@ export default function ConferenceDetails({ conferenceId }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const { token, isLoggedIn } = useAuth(); + const apiFetch = useApi(); const [isConnecting, setIsConnecting] = useState(false); const [currentUserExtension, setCurrentUserExtension] = useState(null); @@ -23,20 +25,20 @@ export default function ConferenceDetails({ conferenceId }) { if (isLoggedIn && token) { const fetchUserDetails = async () => { try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/me`, { - headers: { 'Authorization': `Bearer ${token}` } - }); + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/me`); if (res.ok) { const data = await res.json(); setCurrentUserExtension(data.extensionId); } } catch (err) { - console.error("Failed to fetch user details", err); + if (err.message !== 'Unauthorized') { + console.error("Failed to fetch user details", err); + } } }; fetchUserDetails(); } - }, [isLoggedIn, token]); + }, [isLoggedIn, token, apiFetch]); const fetchDetails = useCallback(async () => { setLoading(true); @@ -44,8 +46,8 @@ export default function ConferenceDetails({ conferenceId }) { try { // Fetch both participants and conference room info const [participantsRes, roomsRes] = await Promise.all([ - fetch(`${process.env.NEXT_PUBLIC_API_URL}/conferences/${conferenceId}`), - fetch(`${process.env.NEXT_PUBLIC_API_URL}/conferences`) + apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/conferences/${conferenceId}`), + apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/conferences`) ]); if (!participantsRes.ok) { @@ -66,12 +68,14 @@ export default function ConferenceDetails({ conferenceId }) { } } catch (err) { - console.error(`Failed to fetch details for ${conferenceId}`, err); + if (err.message !== 'Unauthorized') { + console.error(`Failed to fetch details for ${conferenceId}`, err); + } setError(err.message); } finally { setLoading(false); } - }, [conferenceId]); + }, [conferenceId, apiFetch]); useEffect(() => { fetchDetails(); @@ -83,9 +87,8 @@ export default function ConferenceDetails({ conferenceId }) { if (!token) return; setIsConnecting(true); try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/conferences/${conferenceId}/connectme`, { + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/conferences/${conferenceId}/connectme`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}` } }); if (!res.ok) { const errorData = await res.json(); @@ -93,8 +96,10 @@ export default function ConferenceDetails({ conferenceId }) { } // Optionally show a success message } catch (err) { - console.error("Failed to connect", err); - alert(err.message); + if (err.message !== 'Unauthorized') { + console.error("Failed to connect", err); + alert(err.message); + } } finally { setIsConnecting(false); } diff --git a/src/components/conferences/conference-list.jsx b/src/components/conferences/conference-list.jsx index ce2176c..ec71750 100644 --- a/src/components/conferences/conference-list.jsx +++ b/src/components/conferences/conference-list.jsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useCallback } from 'react'; +import { useApi } from '@/hooks/use-api'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { Button } from '@/components/ui/button'; @@ -12,6 +13,7 @@ export default function ConferenceList() { const [conferences, setConferences] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const apiFetch = useApi(); const fetchConferences = useCallback(async () => { setLoading(true); @@ -19,7 +21,7 @@ export default function ConferenceList() { try { // Acting as an unauthenticated user, so no auth token is sent. // This assumes the endpoint is public. - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/conferences`); + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/conferences`); if (!res.ok) { const errorData = await res.json(); if (res.status === 401) { @@ -30,12 +32,14 @@ export default function ConferenceList() { const data = await res.json(); setConferences(Array.isArray(data) ? data : []); } catch (err) { - console.error("Failed to fetch conferences", err); + if (err.message !== 'Unauthorized') { + console.error("Failed to fetch conferences", err); + } setError(err.message); } finally { setLoading(false); } - }, []); + }, [apiFetch]); useEffect(() => { fetchConferences(); diff --git a/src/components/dashboard/SipCredentialsCard.jsx b/src/components/dashboard/SipCredentialsCard.jsx index 71b1f6a..8dbe542 100644 --- a/src/components/dashboard/SipCredentialsCard.jsx +++ b/src/components/dashboard/SipCredentialsCard.jsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useAuth } from '@/contexts/AuthContext'; +import { useApi } from '@/hooks/use-api'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; @@ -38,6 +39,7 @@ function CredentialRow({ icon, label, value, onCopy, isCopied }) { export function SipCredentialsCard({ details, loading, refetchDetails }) { const { token } = useAuth(); + const apiFetch = useApi(); const [newSecret, setNewSecret] = useState(null); const [isResetting, setIsResetting] = useState(false); const [copiedState, setCopiedState] = useState({}); @@ -46,9 +48,8 @@ export function SipCredentialsCard({ details, loading, refetchDetails }) { if (!token) return; setIsResetting(true); try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me/resetsecret`, { + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me/resetsecret`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); setNewSecret(data.newSecret); @@ -56,8 +57,10 @@ export function SipCredentialsCard({ details, loading, refetchDetails }) { refetchDetails(); } } catch (error) { - console.error("Failed to reset secret", error); - alert('Failed to reset secret.'); + if (error.message !== 'Unauthorized') { + console.error("Failed to reset secret", error); + alert('Failed to reset secret.'); + } } finally { setIsResetting(false); } diff --git a/src/components/dashboard/actions-card.jsx b/src/components/dashboard/actions-card.jsx index d39d118..edeebbb 100644 --- a/src/components/dashboard/actions-card.jsx +++ b/src/components/dashboard/actions-card.jsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useAuth } from '@/contexts/AuthContext'; +import { useApi } from '@/hooks/use-api'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; @@ -23,6 +24,7 @@ import { export function ActionsCard({ details }) { const { token } = useAuth(); + const apiFetch = useApi(); const [isCalling, setIsCalling] = useState(false); const [callMode, setCallMode] = useState('hold'); const [callerId, setCallerId] = useState(''); @@ -38,14 +40,15 @@ export function ActionsCard({ details }) { if (callerId) { url += `&callerId=${encodeURIComponent(callerId)}`; } - await fetch(url, { + await apiFetch(url, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}` } }); setIsCallMeDialogOpen(false); } catch (error) { - console.error("Failed to initiate call", error); - alert('Failed to initiate call.'); + if (error.message !== 'Unauthorized') { + console.error("Failed to initiate call", error); + alert('Failed to initiate call.'); + } } finally { setIsCalling(false); } @@ -55,15 +58,16 @@ export function ActionsCard({ details }) { if (!token) return; setIsResetting(true); try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me/resetsecret`, { + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me/resetsecret`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); setNewSecret(data.newSecret); } catch (error) { - console.error("Failed to reset secret", error); - alert('Failed to reset secret.'); + if (error.message !== 'Unauthorized') { + console.error("Failed to reset secret", error); + alert('Failed to reset secret.'); + } } finally { setIsResetting(false); } diff --git a/src/components/dashboard/active-calls.jsx b/src/components/dashboard/active-calls.jsx index b7fe833..172b376 100644 --- a/src/components/dashboard/active-calls.jsx +++ b/src/components/dashboard/active-calls.jsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useAuth } from '@/contexts/AuthContext'; +import { useApi } from '@/hooks/use-api'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; @@ -34,6 +35,7 @@ function CallCard({ call }) { export function ActiveCalls() { const { token } = useAuth(); + const apiFetch = useApi(); const [calls, setCalls] = useState([]); const [loading, setLoading] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true); @@ -44,13 +46,13 @@ export function ActiveCalls() { setLoading(true); } try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me/calls`, { - headers: { 'Authorization': `Bearer ${token}` } - }); + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me/calls`); const data = await res.json(); setCalls(Array.isArray(data) ? data : []); } catch (error) { - console.error("Failed to fetch active calls", error); + if (error.message !== 'Unauthorized') { + console.error("Failed to fetch active calls", error); + } setCalls([]); } finally { if (isInitialLoad || isManualRefresh) { @@ -60,7 +62,7 @@ export function ActiveCalls() { setIsInitialLoad(false); } } - }, [token, isInitialLoad]); + }, [token, isInitialLoad, apiFetch]); useEffect(() => { if (token) { @@ -75,13 +77,14 @@ export function ActiveCalls() { const handleHangup = async () => { if (!token) return; try { - await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me/calls`, { + await apiFetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me/calls`, { method: 'DELETE', - headers: { 'Authorization': `Bearer ${token}` } }); fetchCalls(true); // Refresh immediately and show loading state } catch (error) { - console.error("Failed to hangup calls", error); + if (error.message !== 'Unauthorized') { + console.error("Failed to hangup calls", error); + } } }; diff --git a/src/components/dashboard/api-keys.jsx b/src/components/dashboard/api-keys.jsx index 5813c53..c3ab849 100644 --- a/src/components/dashboard/api-keys.jsx +++ b/src/components/dashboard/api-keys.jsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useAuth } from '@/contexts/AuthContext'; +import { useApi } from '@/hooks/use-api'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; @@ -25,6 +26,7 @@ import { Badge } from "@/components/ui/badge" export default function ApiKeys() { const { token } = useAuth(); + const apiFetch = useApi(); const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); const [newKey, setNewKey] = useState(null); @@ -40,18 +42,18 @@ export default function ApiKeys() { if (!token) return; setLoading(true); try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/keys`, { - headers: { 'Authorization': `Bearer ${token}` } - }); + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/keys`); const data = await res.json(); setKeys(Array.isArray(data) ? data : []); } catch (error) { - console.error("Failed to fetch API keys", error); + if (error.message !== 'Unauthorized') { + console.error("Failed to fetch API keys", error); + } setKeys([]); } finally { setLoading(false); } - }, [token]); + }, [token, apiFetch]); useEffect(() => { fetchKeys(); @@ -61,10 +63,9 @@ export default function ApiKeys() { if (!token) return; setIsCreating(true); try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/keys`, { + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/keys`, { method: 'POST', headers: { - 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ expiresInDays }) @@ -87,8 +88,10 @@ export default function ApiKeys() { setKeys(prevKeys => [keyForState, ...prevKeys]); setIsCreateDialogOpen(false); // Close the creation dialog } catch (error) { - console.error("Failed to create key", error); - alert(`Failed to create key: ${error.message}`); + if (error.message !== 'Unauthorized') { + console.error("Failed to create key", error); + alert(`Failed to create key: ${error.message}`); + } } finally { setIsCreating(false); } @@ -97,9 +100,8 @@ export default function ApiKeys() { const handleDeleteKey = async (keyToDelete) => { if (!token) return; try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/keys/${keyToDelete}`, { + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/keys/${keyToDelete}`, { method: 'DELETE', - headers: { 'Authorization': `Bearer ${token}` } }); if (!res.ok) { const errorData = await res.json().catch(() => ({ error: 'Failed to delete key.' })); @@ -107,8 +109,10 @@ export default function ApiKeys() { } setKeys(prevKeys => prevKeys.filter(key => key.key !== keyToDelete)); } catch (error) { - console.error("Failed to delete key", error); - alert(`Failed to delete key: ${error.message}`); + if (error.message !== 'Unauthorized') { + console.error("Failed to delete key", error); + alert(`Failed to delete key: ${error.message}`); + } } }; diff --git a/src/components/dashboard/dashboard-client.jsx b/src/components/dashboard/dashboard-client.jsx index 3a570dc..ac29517 100644 --- a/src/components/dashboard/dashboard-client.jsx +++ b/src/components/dashboard/dashboard-client.jsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { useAuth } from '@/contexts/AuthContext'; +import { useApi } from '@/hooks/use-api'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import ExtensionDetails from './extension-details'; @@ -43,6 +44,7 @@ export default function DashboardClient() { const [detailsLoading, setDetailsLoading] = useState(true); const [deviceStatus, setDeviceStatus] = useState(null); const [deviceStatusLoading, setDeviceStatusLoading] = useState(true); + const apiFetch = useApi(); useEffect(() => { if (!token) { @@ -53,9 +55,7 @@ export default function DashboardClient() { const fetchDetails = async () => { setDetailsLoading(true); try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me`, { - headers: { 'Authorization': `Bearer ${token}` } - }); + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me`); if (res.ok) { const data = await res.json(); console.log("Fetched details:", data); @@ -64,7 +64,9 @@ export default function DashboardClient() { setDetails(null); } } catch (err) { - console.error("Failed to fetch details", err); + if (err.message !== 'Unauthorized') { + console.error("Failed to fetch details", err); + } setDetails(null); } finally { setDetailsLoading(false); @@ -72,7 +74,7 @@ export default function DashboardClient() { }; fetchDetails(); - }, [token]); + }, [token, apiFetch]); useEffect(() => { if (!token) { @@ -83,9 +85,7 @@ export default function DashboardClient() { const fetchDeviceStatus = async () => { // No need to set loading to true every time for a silent refresh try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me/devicestatus`, { - headers: { 'Authorization': `Bearer ${token}` } - }); + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me/devicestatus`); if (res.ok) { const data = await res.json(); setDeviceStatus(data); @@ -93,7 +93,9 @@ export default function DashboardClient() { setDeviceStatus(null); } } catch (err) { - console.error("Failed to fetch device status", err); + if (err.message !== 'Unauthorized') { + console.error("Failed to fetch device status", err); + } setDeviceStatus(null); } finally { if (deviceStatusLoading) setDeviceStatusLoading(false); @@ -103,7 +105,7 @@ export default function DashboardClient() { fetchDeviceStatus(); const interval = setInterval(fetchDeviceStatus, 5000); // Poll every 5 seconds return () => clearInterval(interval); - }, [token, deviceStatusLoading]); + }, [token, deviceStatusLoading, apiFetch]); const handleLogin = () => { window.location.href = `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/auth/discord`; diff --git a/src/components/dashboard/registered-devices.jsx b/src/components/dashboard/registered-devices.jsx index 3739bbc..783f067 100644 --- a/src/components/dashboard/registered-devices.jsx +++ b/src/components/dashboard/registered-devices.jsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useAuth } from '@/contexts/AuthContext'; +import { useApi } from '@/hooks/use-api'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; @@ -68,6 +69,7 @@ export function RegisteredDevices() { const [devices, setDevices] = useState([]); const [loading, setLoading] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true); + const apiFetch = useApi(); const fetchDevices = useCallback(async (isManualRefresh = false) => { if (!token) return; @@ -75,9 +77,7 @@ export function RegisteredDevices() { setLoading(true); } try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me/endpoint`, { - headers: { 'Authorization': `Bearer ${token}` } - }); + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/extensions/me/endpoint`); if (res.ok) { const data = await res.json(); setDevices(Array.isArray(data) ? data : []); @@ -85,7 +85,9 @@ export function RegisteredDevices() { setDevices([]); } } catch (error) { - console.error("Failed to fetch registered devices", error); + if (error.message !== 'Unauthorized') { + console.error("Failed to fetch registered devices", error); + } setDevices([]); } finally { if (isInitialLoad || isManualRefresh) { @@ -95,7 +97,7 @@ export function RegisteredDevices() { setIsInitialLoad(false); } } - }, [token, isInitialLoad]); + }, [token, isInitialLoad, apiFetch]); useEffect(() => { if (token) { diff --git a/src/components/dashboard/voicemail.jsx b/src/components/dashboard/voicemail.jsx index de4565b..fb812ee 100644 --- a/src/components/dashboard/voicemail.jsx +++ b/src/components/dashboard/voicemail.jsx @@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useMemo } from "react"; import { useAuth } from "@/contexts/AuthContext"; +import { useApi } from "@/hooks/use-api"; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -48,6 +49,7 @@ function formatVoicemailDate(dateStr) { export function VoicemailPanel({ extensionId: extProp }) { const { token } = useAuth(); + const apiFetch = useApi(); const [extensionId, setExtensionId] = useState(extProp || null); const [loadingExt, setLoadingExt] = useState(!extProp); const [loading, setLoading] = useState(true); @@ -65,30 +67,28 @@ export function VoicemailPanel({ extensionId: extProp }) { const load = async () => { setLoadingExt(true); try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/me`, { - headers: { Authorization: `Bearer ${token}` } - }); + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/me`); if (res.ok) { const data = await res.json(); setExtensionId(data.extensionId); } } catch (e) { - // silent + if (e.message !== 'Unauthorized') { + // silent + } } finally { setLoadingExt(false); } }; load(); - }, [token, extProp]); + }, [token, extProp, apiFetch]); const fetchVoicemails = useCallback(async () => { if (!token || !extensionId) return; setRefreshing(true); if (voicemails.length === 0) setLoading(true); try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails`, { - headers: { Authorization: `Bearer ${token}` } - }); + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails`); if (res.ok) { const data = await res.json(); setVoicemails(Array.isArray(data) ? data : []); @@ -96,12 +96,14 @@ export function VoicemailPanel({ extensionId: extProp }) { setVoicemails([]); } } catch (e) { - setVoicemails([]); + if (e.message !== 'Unauthorized') { + setVoicemails([]); + } } finally { setLoading(false); setRefreshing(false); } - }, [token, extensionId, voicemails.length]); + }, [token, extensionId, voicemails.length, apiFetch]); useEffect(() => { if (extensionId) fetchVoicemails(); @@ -128,15 +130,15 @@ export function VoicemailPanel({ extensionId: extProp }) { if (isOpening && vm.hasAudio && !audioMap[vm.messageId]?.url) { setAudioMap(m => ({ ...m, [vm.messageId]: { url: null, loading: true } })); try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vm.messageId}/download`, { - headers: { Authorization: `Bearer ${token}` } - }); + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vm.messageId}/download`); if (!res.ok) throw new Error('Download failed'); const blob = await res.blob(); const url = URL.createObjectURL(blob); setAudioMap(m => ({ ...m, [vm.messageId]: { url, loading: false } })); - } catch { - setAudioMap(m => ({ ...m, [vm.messageId]: { url: null, loading: false, error: true } })); + } catch(e) { + if (e.message !== 'Unauthorized') { + setAudioMap(m => ({ ...m, [vm.messageId]: { url: null, loading: false, error: true } })); + } } } }; @@ -145,10 +147,9 @@ export function VoicemailPanel({ extensionId: extProp }) { if (target === vm.folder) return; setMovingId(vm.messageId); try { - await fetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vm.messageId}/move`, { + await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vm.messageId}/move`, { method: 'PATCH', headers: { - Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ targetFolder: target }) @@ -157,8 +158,10 @@ export function VoicemailPanel({ extensionId: extProp }) { if (target !== activeFolder) { setExpandedId(null); } - } catch { - // silent fail + } catch(e) { + if (e.message !== 'Unauthorized') { + // silent fail + } } finally { setMovingId(null); } @@ -167,9 +170,7 @@ export function VoicemailPanel({ extensionId: extProp }) { const downloadVoicemail = async (vm) => { if (!vm.hasAudio) return; try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vm.messageId}/download`, { - headers: { Authorization: `Bearer ${token}` } - }); + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vm.messageId}/download`); if (!res.ok) return; const blob = await res.blob(); const url = URL.createObjectURL(blob); @@ -180,24 +181,27 @@ export function VoicemailPanel({ extensionId: extProp }) { a.click(); a.remove(); URL.revokeObjectURL(url); - } catch { - // ignore + } catch(e) { + if (e.message !== 'Unauthorized') { + // ignore + } } }; const handleDelete = async () => { if (!vmToDelete || !token || !extensionId) return; try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vmToDelete.messageId}`, { + const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vmToDelete.messageId}`, { method: 'DELETE', - headers: { Authorization: `Bearer ${token}` } }); if (res.ok) { setVoicemails(list => list.filter(v => v.messageId !== vmToDelete.messageId)); if (expandedId === vmToDelete.messageId) setExpandedId(null); } - } catch { - // silent + } catch(e) { + if (e.message !== 'Unauthorized') { + // silent + } } finally { setVmToDelete(null); } diff --git a/src/hooks/use-api.js b/src/hooks/use-api.js new file mode 100644 index 0000000..e8cc596 --- /dev/null +++ b/src/hooks/use-api.js @@ -0,0 +1,30 @@ +"use client"; + +import { useCallback } from 'react'; +import { useAuth } from '@/contexts/AuthContext'; + +export const useApi = () => { + const { token, logout } = useAuth(); + + const apiFetch = useCallback(async (url, options = {}) => { + const headers = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const response = await fetch(url, { ...options, headers }); + + if (response.status === 401) { + await logout(); + throw new Error('Unauthorized'); + } + + return response; + }, [token, logout]); + + return apiFetch; +};