fix(frontend): logout state not shown after JWT expires (#40)
All checks were successful
CI / Maven unit tests (push) Successful in 14s
CI / Build and push images (push) Successful in 57s
CI / Deploy to server (push) Successful in 8s

Reviewed-on: #40
This commit was merged in pull request #40.
This commit is contained in:
2026-09-06 23:26:07 +01:00
parent 6fa02a34a3
commit 9758bb296b
12 changed files with 131 additions and 147 deletions

View File

@@ -19,6 +19,7 @@ import {config} from "./config.ts";
import NotFound from "./pages/NotFound.tsx"; import NotFound from "./pages/NotFound.tsx";
import AuthProvider from "./auth/AuthProvider.tsx"; import AuthProvider from "./auth/AuthProvider.tsx";
import ThemeProvider from "./theme/ThemeProvider.tsx"; import ThemeProvider from "./theme/ThemeProvider.tsx";
import RequireAuth from "./auth/RequireAuth.tsx";
function App() { function App() {
@@ -36,14 +37,18 @@ function App() {
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/create" element={<ClipUpload />} /> <Route path="/create" element={<ClipUpload />} />
<Route path="/create/:id" element={<ClipEdit />} /> <Route path="/create/:id" element={<ClipEdit />} />
<Route path="/my-clips" element={<MyClips />} />
<Route path="/my-vods" element={<MyVods />} /> <Route element={<RequireAuth />}>
<Route path="/video/:id" element={<MediaPlayer noun="Clip" getMediaUrl={getClipMediaUrl} fetchDetails={getClipById} />} /> <Route path="/my-clips" element={<MyClips />} />
<Route path="/vod/:id" element={<MediaPlayer noun="VoD" getMediaUrl={getVodMediaUrl} fetchDetails={getVodById} />} /> <Route path="/my-vods" element={<MyVods />} />
<Route path="/clips/:id/edit" element={<EditClip />} /> <Route path="/video/:id" element={<MediaPlayer noun="Clip" getMediaUrl={getClipMediaUrl} fetchDetails={getClipById} />} />
<Route path="/vods/:id/edit" element={<EditVod />} /> <Route path="/vod/:id" element={<MediaPlayer noun="VoD" getMediaUrl={getVodMediaUrl} fetchDetails={getVodById} />} />
<Route path="/profile" element={<Profile />} /> <Route path="/clips/:id/edit" element={<EditClip />} />
<Route path="/stream" element={<StreamControl />} /> <Route path="/vods/:id/edit" element={<EditVod />} />
<Route path="/profile" element={<Profile />} />
<Route path="/stream" element={<StreamControl />} />
</Route>
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
import type { User } from "../utils/types.ts"; import type { User } from "../utils/types.ts";
import { login as apiLogin, logout as apiLogout, getUser } from "../utils/api/users.ts"; import { login as apiLogin, logout as apiLogout, getUser } from "../utils/api/users.ts";
import { AuthContext } from "./AuthContext.ts"; import { AuthContext } from "./AuthContext.ts";
import {registerUnauthorizedHandler} from "../utils/api/client.ts";
/** /**
* Owns the single source of truth for the authenticated user. Everything that * Owns the single source of truth for the authenticated user. Everything that
@@ -33,7 +34,11 @@ const AuthProvider = ({ children }: { children: ReactNode }) => {
const logout = useCallback(async () => { const logout = useCallback(async () => {
await apiLogout(); await apiLogout();
setUser(null); await refresh();
}, [refresh]);
useEffect(() => {
registerUnauthorizedHandler(() => setUser(null));
}, []); }, []);
return ( return (

View File

@@ -0,0 +1,14 @@
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { useAuth } from "./useAuth";
const RequireAuth = () => {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) return null;
if (!user) return <Navigate to="/" replace state={{ from: location }} />;
return <Outlet />;
};
export default RequireAuth;

View File

@@ -6,13 +6,10 @@ import ClipRangeSlider from "./../components/video/ClipRangeSlider";
import ConfigBox from "../components/video/ConfigBox.tsx"; import ConfigBox from "../components/video/ConfigBox.tsx";
import ExportWidget from "../components/video/ExportWidget.tsx"; import ExportWidget from "../components/video/ExportWidget.tsx";
import { compress } from "../utils/api/media" import { compress } from "../utils/api/media"
import { getJob } from "../utils/api/jobs" import { getJob, downloadJob } from "../utils/api/jobs"
import type { VideoMetadata } from "../utils/types.ts"; import type { VideoMetadata } from "../utils/types.ts";
import Box from "../components/Box.tsx"; import Box from "../components/Box.tsx";
import MetadataBox from "../components/video/MetadataBox.tsx"; import MetadataBox from "../components/video/MetadataBox.tsx";
import {config} from "../config.ts";
const API_URL = config.apiUrl;
const ClipEdit = () => { const ClipEdit = () => {
const location = useLocation(); const location = useLocation();
@@ -111,14 +108,14 @@ const ClipEdit = () => {
const handleDownload = async () => { const handleDownload = async () => {
if (!uploadedId) return; if (!uploadedId) return;
const response = await fetch(API_URL + `/api/v1/jobs/${uploadedId}/download`, { credentials: 'include' }); let blob: Blob;
try {
if (!response.ok) { blob = await downloadJob(uploadedId);
console.error('Download failed'); } catch (err) {
console.error('Download failed', err);
return; return;
} }
const blob = await response.blob();
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');

View File

@@ -6,9 +6,43 @@ class AuthError extends Error {
constructor() { super("Not authenticated"); this.name = "AuthError"; } constructor() { super("Not authenticated"); this.name = "AuthError"; }
} }
const isThumbnailAvailable = async (thumbnailUrl: string): Promise<boolean> => { let onUnauthorized: (() => void) | null = null;
const response = await fetch(thumbnailUrl, { credentials: 'include' });
return response.ok;
};
export { API_URL, AuthError, isThumbnailAvailable }; export function registerUnauthorizedHandler(handler: () => void) {
onUnauthorized = handler;
}
/**
* Performs a request against the API, sending session cookies and funnelling
* expired/missing sessions through the registered unauthorized handler so the
* UI can drop back to a logged out state. Returns the raw response so callers
* that need something other than JSON (blobs, streams) can read it themselves.
*/
async function apiFetchRaw(url: string, options: RequestInit = {}): Promise<Response> {
const response = await fetch(url, {
...options,
credentials: 'include'
});
if (response.status == 401 || response.status == 403) {
onUnauthorized?.();
throw new AuthError();
}
if (!response.ok) {
const text = await response.text();
let message = text;
try { message = JSON.parse(text).message ?? text; } catch { /* not JSON, use raw text */ }
throw new Error(message || `Request failed: ${response.status}`);
}
return response;
}
async function apiFetch<T>(url: string, options: RequestInit = {}): Promise<T> {
const response = await apiFetchRaw(url, options);
const text = await response.text();
return text ? JSON.parse(text) : (undefined as T);
}
export { API_URL, AuthError, apiFetch, apiFetchRaw };

View File

@@ -1,27 +1,17 @@
import type { APIResponse, Clip } from "../types.ts"; import type { APIResponse, Clip } from "../types.ts";
import { API_URL, AuthError } from "./client.ts"; import { API_URL, apiFetch } from "./client.ts";
const getClips = async (): Promise<Clip[]> => { const getClips = async (): Promise<Clip[]> => {
const response = await fetch(API_URL + '/api/v1/clips', { credentials: 'include' }); const result = await apiFetch<APIResponse>(API_URL + '/api/v1/clips');
if (result.status === 'error') throw new Error(`Failed to fetch clips: ${result.message}`);
if (!response.ok) {
const errorResult: APIResponse = await response.json();
throw new Error(`Failed to fetch clips: ${errorResult.message}`);
}
const result: APIResponse = await response.json();
return result.data; return result.data;
}; };
const getClipById = async (id: string): Promise<Clip | null> => { const getClipById = async (id: string): Promise<Clip | null> => {
const response = await fetch(API_URL + `/api/v1/clips/${id}`, { credentials: 'include' }); const result = await apiFetch<APIResponse>(API_URL + `/api/v1/clips/${id}`);
if (result.status === 'error') throw new Error(`Failed to fetch clip: ${result.message}`);
if (!response.ok) {
if (response.status === 401 || response.status === 403) throw new AuthError();
throw new Error(`Failed to fetch clip: ${response.status}`);
}
const result: APIResponse = await response.json();
return result.data; return result.data;
}; };
@@ -31,31 +21,20 @@ const getClipById = async (id: string): Promise<Clip | null> => {
const getClipMediaUrl = (id: string): string => API_URL + `/api/v1/clips/${id}/media`; const getClipMediaUrl = (id: string): string => API_URL + `/api/v1/clips/${id}/media`;
const patchClip = async (id: number, data: { title?: string; description?: string }): Promise<Clip> => { const patchClip = async (id: number, data: { title?: string; description?: string }): Promise<Clip> => {
const response = await fetch(API_URL + `/api/v1/clips/${id}`, { const result = await apiFetch<APIResponse>(API_URL + `/api/v1/clips/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
if (!response.ok) throw new Error(`Failed to update clip: ${response.status}`);
const result: APIResponse = await response.json();
if (result.status === 'error') throw new Error(`Failed to update clip: ${result.message}`); if (result.status === 'error') throw new Error(`Failed to update clip: ${result.message}`);
return result.data; return result.data;
}; };
const deleteClip = async (id: number): Promise<void> => { const deleteClip = async (id: number): Promise<void> => {
const response = await fetch(API_URL + `/api/v1/clips/${id}`, { const result = await apiFetch<APIResponse>(API_URL + `/api/v1/clips/${id}`, { method: 'DELETE' });
method: 'DELETE', if (result?.status === 'error') throw new Error(`Failed to delete clip: ${result.message}`);
credentials: 'include',
});
if (!response.ok) throw new Error(`Failed to delete clip: ${response.status}`);
const result: APIResponse = await response.json();
if (result.status === 'error') throw new Error(`Failed to delete clip: ${result.message}`);
}; };
export { getClips, getClipById, getClipMediaUrl, patchClip, deleteClip }; export { getClips, getClipById, getClipMediaUrl, patchClip, deleteClip };

View File

@@ -1,15 +1,16 @@
import type { APIResponse, JobResponse } from "../types.ts"; import type { APIResponse, JobResponse } from "../types.ts";
import { API_URL } from "./client.ts"; import { API_URL, apiFetch, apiFetchRaw } from "./client.ts";
const getJob = async (uuid: string): Promise<JobResponse> => { const getJob = async (uuid: string): Promise<JobResponse> => {
const response = await fetch(API_URL + `/api/v1/jobs/${uuid}`, { credentials: 'include' }); const result = await apiFetch<APIResponse>(API_URL + `/api/v1/jobs/${uuid}`);
if (!response.ok) throw new Error(`Failed to fetch job: ${response.status}`);
const result: APIResponse = await response.json();
if (result.status === 'error') throw new Error(`Failed to fetch job: ${result.message}`); if (result.status === 'error') throw new Error(`Failed to fetch job: ${result.message}`);
return result.data; return result.data;
}; };
export { getJob }; const downloadJob = async (uuid: string): Promise<Blob> => {
const response = await apiFetchRaw(API_URL + `/api/v1/jobs/${uuid}/download`);
return response.blob();
};
export { getJob, downloadJob };

View File

@@ -1,40 +1,28 @@
import type { APIResponse, Marker } from "../types.ts"; import type { APIResponse, Marker } from "../types.ts";
import { API_URL } from "./client.ts"; import { API_URL, apiFetch } from "./client.ts";
const getMarkers = async (): Promise<Marker[]> => { const getMarkers = async (): Promise<Marker[]> => {
const response = await fetch(API_URL + '/api/v1/markers', { credentials: 'include' }); const result = await apiFetch<APIResponse>(API_URL + '/api/v1/markers');
if (!response.ok) throw new Error(`Failed to fetch markers: ${response.status}`);
const result: APIResponse = await response.json();
if (result.status === 'error') throw new Error(`Failed to fetch markers: ${result.message}`); if (result.status === 'error') throw new Error(`Failed to fetch markers: ${result.message}`);
return result.data; return result.data;
}; };
const createMarker = async (message: string): Promise<Marker> => { const createMarker = async (message: string): Promise<Marker> => {
const response = await fetch(API_URL + '/api/v1/markers', { const result = await apiFetch<APIResponse>(API_URL + '/api/v1/markers', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ message }), body: JSON.stringify({ message }),
}); });
const result: APIResponse = await response.json(); if (result.status === 'error') throw new Error(`Failed to add marker: ${result.message}`);
if (!response.ok || result.status === 'error') throw new Error(`Failed to add marker: ${result.message}`);
return result.data; return result.data;
}; };
const deleteMarker = async (id: number): Promise<void> => { const deleteMarker = async (id: number): Promise<void> => {
const response = await fetch(API_URL + `/api/v1/markers/${id}`, { const result = await apiFetch<APIResponse>(API_URL + `/api/v1/markers/${id}`, { method: 'DELETE' });
method: 'DELETE', if (result?.status === 'error') throw new Error(`Failed to delete marker: ${result.message}`);
credentials: 'include',
});
if (!response.ok) throw new Error(`Failed to delete marker: ${response.status}`);
const result: APIResponse = await response.json();
if (result.status === 'error') throw new Error(`Failed to delete marker: ${result.message}`);
}; };
export { getMarkers, createMarker, deleteMarker }; export { getMarkers, createMarker, deleteMarker };

View File

@@ -1,5 +1,5 @@
import type { APIResponse, VideoMetadata } from "../types.ts"; import type { APIResponse, VideoMetadata } from "../types.ts";
import { API_URL } from "./client.ts"; import { API_URL, apiFetch } from "./client.ts";
const compress = async (file: File, options: VideoMetadata): Promise<string> => { const compress = async (file: File, options: VideoMetadata): Promise<string> => {
const formData = new FormData(); const formData = new FormData();
@@ -13,58 +13,48 @@ const compress = async (file: File, options: VideoMetadata): Promise<string> =>
if (options.height) formData.append('height', options.height.toString()); if (options.height) formData.append('height', options.height.toString());
if (options.fileSize) formData.append('fileSize', options.fileSize.toString()); if (options.fileSize) formData.append('fileSize', options.fileSize.toString());
const response = await fetch(API_URL + '/api/v1/media/compress', { const result = await apiFetch<APIResponse>(API_URL + '/api/v1/media/compress', {
method: 'POST', method: 'POST',
body: formData, body: formData,
credentials: 'include',
}); });
if (!response.ok) throw new Error(`Compression failed: ${response.status}`);
const result: APIResponse = await response.json();
if (result.status === 'error') throw new Error(`Compression failed: ${result.message}`); if (result.status === 'error') throw new Error(`Compression failed: ${result.message}`);
return result.data.uuid; return result.data.uuid;
}; };
const saveSection = async (startTime: string, endTime: string, title?: string, description?: string): Promise<string> => { const saveSection = async (startTime: string, endTime: string, title?: string, description?: string): Promise<string> => {
const response = await fetch(API_URL + '/api/v1/media/save', { const result = await apiFetch<APIResponse>(API_URL + '/api/v1/media/save', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ startTime, endTime, title, description }), body: JSON.stringify({ startTime, endTime, title, description }),
}); });
const result: APIResponse = await response.json(); if (result.status === 'error') throw new Error(`Failed to save section: ${result.message}`);
if (!response.ok || result.status === 'error') throw new Error(`Failed to save section: ${result.message}`);
return result.data.uuid; return result.data.uuid;
}; };
const saveSectionByMarkers = async (startMarkerId: number, endMarkerId: number, title?: string, description?: string): Promise<string> => { const saveSectionByMarkers = async (startMarkerId: number, endMarkerId: number, title?: string, description?: string): Promise<string> => {
const response = await fetch(API_URL + '/api/v1/media/save/markers', { const result = await apiFetch<APIResponse>(API_URL + '/api/v1/media/save/markers', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ startMarkerId, endMarkerId, title, description }), body: JSON.stringify({ startMarkerId, endMarkerId, title, description }),
}); });
const result: APIResponse = await response.json(); if (result.status === 'error') throw new Error(`Failed to save section: ${result.message}`);
if (!response.ok || result.status === 'error') throw new Error(`Failed to save section: ${result.message}`);
return result.data.uuid; return result.data.uuid;
}; };
const clipSection = async (duration: number, title?: string, description?: string): Promise<string> => { const clipSection = async (duration: number, title?: string, description?: string): Promise<string> => {
const response = await fetch(API_URL + '/api/v1/media/clip', { const result = await apiFetch<APIResponse>(API_URL + '/api/v1/media/clip', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ duration, title, description }), body: JSON.stringify({ duration, title, description }),
}); });
const result: APIResponse = await response.json(); if (result.status === 'error') throw new Error(`Failed to clip section: ${result.message}`);
if (!response.ok || result.status === 'error') throw new Error(`Failed to clip section: ${result.message}`);
return result.data.uuid; return result.data.uuid;
}; };

View File

@@ -1,28 +1,24 @@
import type { APIResponse, StreamHistoryItem, StreamStatus } from "../types.ts"; import type { APIResponse, StreamHistoryItem, StreamStatus } from "../types.ts";
import { API_URL } from "./client.ts"; import { API_URL, apiFetch } from "./client.ts";
const getStreamStatus = async (): Promise<boolean> => { const getStreamStatus = async (): Promise<boolean> => {
const response = await fetch(API_URL + '/api/v1/stream/current', { credentials: 'include' }); try {
if (!response.ok) return false; const result = await apiFetch<APIResponse>(API_URL + '/api/v1/stream/current');
const result: APIResponse = await response.json(); return result.status === 'success' && result.data?.isStreaming === true;
return result.status === 'success' && result.data?.isStreaming === true; } catch {
return false;
}
}; };
const getCurrentStream = async (): Promise<StreamStatus> => { const getCurrentStream = async (): Promise<StreamStatus> => {
const response = await fetch(API_URL + '/api/v1/stream/current', { credentials: 'include' }); const result = await apiFetch<APIResponse>(API_URL + '/api/v1/stream/current');
if (!response.ok) throw new Error(`Failed to fetch stream status: ${response.status}`);
const result: APIResponse = await response.json();
if (result.status === 'error') throw new Error(`Failed to fetch stream status: ${result.message}`); if (result.status === 'error') throw new Error(`Failed to fetch stream status: ${result.message}`);
return result.data; return result.data;
}; };
const getStreamHistory = async (userId: number): Promise<StreamHistoryItem[]> => { const getStreamHistory = async (userId: number): Promise<StreamHistoryItem[]> => {
const response = await fetch(API_URL + `/api/v1/stream/history/${userId}`, { credentials: 'include' }); const result = await apiFetch<APIResponse>(API_URL + `/api/v1/stream/history/${userId}`);
if (!response.ok) throw new Error(`Failed to fetch stream history: ${response.status}`);
const result: APIResponse = await response.json();
if (result.status === 'error') throw new Error(`Failed to fetch stream history: ${result.message}`); if (result.status === 'error') throw new Error(`Failed to fetch stream history: ${result.message}`);
return result.data; return result.data;

View File

@@ -1,6 +1,9 @@
import type { APIResponse, User } from "../types.ts"; import type { APIResponse, User } from "../types.ts";
import { API_URL } from "./client.ts"; import { API_URL, apiFetch } from "./client.ts";
// login and getUser deliberately use fetch directly: they run when there may be
// no session at all, and getUser is what the unauthorized handler calls to
// refresh — routing them through apiFetch would loop on a 401.
const login = async (googleToken: string): Promise<string> => { const login = async (googleToken: string): Promise<string> => {
const response = await fetch(API_URL + '/api/v1/users/login', { const response = await fetch(API_URL + '/api/v1/users/login', {
method: 'POST', method: 'POST',
@@ -18,15 +21,8 @@ const login = async (googleToken: string): Promise<string> => {
}; };
const logout = async (): Promise<void> => { const logout = async (): Promise<void> => {
const response = await fetch(API_URL + '/api/v1/users/logout', { const result = await apiFetch<APIResponse>(API_URL + '/api/v1/users/logout', { method: 'POST' });
method: 'POST', if (result?.status === 'error') throw new Error(`Logout failed: ${result.message}`);
credentials: 'include',
});
if (!response.ok) throw new Error(`Logout failed: ${response.status}`);
const result: APIResponse = await response.json();
if (result.status === 'error') throw new Error(`Logout failed: ${result.message}`);
}; };
const getUser = async (): Promise<User | null> => { const getUser = async (): Promise<User | null> => {

View File

@@ -1,27 +1,17 @@
import type { APIResponse, Vod } from "../types.ts"; import type { APIResponse, Vod } from "../types.ts";
import { API_URL, AuthError } from "./client.ts"; import { API_URL, apiFetch } from "./client.ts";
const getVods = async (): Promise<Vod[]> => { const getVods = async (): Promise<Vod[]> => {
const response = await fetch(API_URL + '/api/v1/vods', { credentials: 'include' }); const result = await apiFetch<APIResponse>(API_URL + '/api/v1/vods');
if (result.status === 'error') throw new Error(`Failed to fetch vods: ${result.message}`);
if (!response.ok) {
const errorResult: APIResponse = await response.json();
throw new Error(`Failed to fetch vods: ${errorResult.message}`);
}
const result: APIResponse = await response.json();
return result.data; return result.data;
}; };
const getVodById = async (id: string): Promise<Vod | null> => { const getVodById = async (id: string): Promise<Vod | null> => {
const response = await fetch(API_URL + `/api/v1/vods/${id}`, { credentials: 'include' }); const result = await apiFetch<APIResponse>(API_URL + `/api/v1/vods/${id}`);
if (result.status === 'error') throw new Error(`Failed to fetch vod: ${result.message}`);
if (!response.ok) {
if (response.status === 401 || response.status === 403) throw new AuthError();
throw new Error(`Failed to fetch vod: ${response.status}`);
}
const result: APIResponse = await response.json();
return result.data; return result.data;
}; };
@@ -31,31 +21,20 @@ const getVodById = async (id: string): Promise<Vod | null> => {
const getVodMediaUrl = (id: string): string => API_URL + `/api/v1/vods/${id}/media`; const getVodMediaUrl = (id: string): string => API_URL + `/api/v1/vods/${id}/media`;
const patchVod = async (id: number, data: { title?: string; description?: string }): Promise<Vod> => { const patchVod = async (id: number, data: { title?: string; description?: string }): Promise<Vod> => {
const response = await fetch(API_URL + `/api/v1/vods/${id}`, { const result = await apiFetch<APIResponse>(API_URL + `/api/v1/vods/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
if (!response.ok) throw new Error(`Failed to update vod: ${response.status}`);
const result: APIResponse = await response.json();
if (result.status === 'error') throw new Error(`Failed to update vod: ${result.message}`); if (result.status === 'error') throw new Error(`Failed to update vod: ${result.message}`);
return result.data; return result.data;
}; };
const deleteVod = async (id: number): Promise<void> => { const deleteVod = async (id: number): Promise<void> => {
const response = await fetch(API_URL + `/api/v1/vods/${id}`, { const result = await apiFetch<APIResponse>(API_URL + `/api/v1/vods/${id}`, { method: 'DELETE' });
method: 'DELETE', if (result?.status === 'error') throw new Error(`Failed to delete vod: ${result.message}`);
credentials: 'include',
});
if (!response.ok) throw new Error(`Failed to delete vod: ${response.status}`);
const result: APIResponse = await response.json();
if (result.status === 'error') throw new Error(`Failed to delete vod: ${result.message}`);
}; };
export { getVods, getVodById, getVodMediaUrl, patchVod, deleteVod }; export { getVods, getVodById, getVodMediaUrl, patchVod, deleteVod };