new use api handler which does autologin when session is revoked
All checks were successful
Deploy to Server / deploy (push) Successful in 2m6s

This commit is contained in:
rocord01 2025-09-28 21:48:01 -04:00
parent c9ecd95459
commit fcfccf084a
10 changed files with 153 additions and 92 deletions

View file

@ -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);
}

View file

@ -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();

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}
}
};

View file

@ -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}`);
}
}
};

View file

@ -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`;

View file

@ -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) {

View file

@ -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);
}

30
src/hooks/use-api.js Normal file
View file

@ -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;
};