new use api handler which does autologin when session is revoked
All checks were successful
Deploy to Server / deploy (push) Successful in 2m6s
All checks were successful
Deploy to Server / deploy (push) Successful in 2m6s
This commit is contained in:
parent
c9ecd95459
commit
fcfccf084a
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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`;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
30
src/hooks/use-api.js
Normal 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;
|
||||
};
|
Loading…
Reference in a new issue