fix(frontend): logout state not shown after JWT expires (#40)
Reviewed-on: #40
This commit was merged in pull request #40.
This commit is contained in:
@@ -19,6 +19,7 @@ import {config} from "./config.ts";
|
||||
import NotFound from "./pages/NotFound.tsx";
|
||||
import AuthProvider from "./auth/AuthProvider.tsx";
|
||||
import ThemeProvider from "./theme/ThemeProvider.tsx";
|
||||
import RequireAuth from "./auth/RequireAuth.tsx";
|
||||
|
||||
|
||||
function App() {
|
||||
@@ -36,14 +37,18 @@ function App() {
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/create" element={<ClipUpload />} />
|
||||
<Route path="/create/:id" element={<ClipEdit />} />
|
||||
<Route path="/my-clips" element={<MyClips />} />
|
||||
<Route path="/my-vods" element={<MyVods />} />
|
||||
<Route path="/video/:id" element={<MediaPlayer noun="Clip" getMediaUrl={getClipMediaUrl} fetchDetails={getClipById} />} />
|
||||
<Route path="/vod/:id" element={<MediaPlayer noun="VoD" getMediaUrl={getVodMediaUrl} fetchDetails={getVodById} />} />
|
||||
<Route path="/clips/:id/edit" element={<EditClip />} />
|
||||
<Route path="/vods/:id/edit" element={<EditVod />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/stream" element={<StreamControl />} />
|
||||
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route path="/my-clips" element={<MyClips />} />
|
||||
<Route path="/my-vods" element={<MyVods />} />
|
||||
<Route path="/video/:id" element={<MediaPlayer noun="Clip" getMediaUrl={getClipMediaUrl} fetchDetails={getClipById} />} />
|
||||
<Route path="/vod/:id" element={<MediaPlayer noun="VoD" getMediaUrl={getVodMediaUrl} fetchDetails={getVodById} />} />
|
||||
<Route path="/clips/:id/edit" element={<EditClip />} />
|
||||
<Route path="/vods/:id/edit" element={<EditVod />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/stream" element={<StreamControl />} />
|
||||
</Route>
|
||||
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
|
||||
import type { User } from "../utils/types.ts";
|
||||
import { login as apiLogin, logout as apiLogout, getUser } from "../utils/api/users.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
|
||||
@@ -33,7 +34,11 @@ const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
await apiLogout();
|
||||
setUser(null);
|
||||
await refresh();
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
registerUnauthorizedHandler(() => setUser(null));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
14
frontend/src/auth/RequireAuth.tsx
Normal file
14
frontend/src/auth/RequireAuth.tsx
Normal 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;
|
||||
@@ -6,13 +6,10 @@ import ClipRangeSlider from "./../components/video/ClipRangeSlider";
|
||||
import ConfigBox from "../components/video/ConfigBox.tsx";
|
||||
import ExportWidget from "../components/video/ExportWidget.tsx";
|
||||
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 Box from "../components/Box.tsx";
|
||||
import MetadataBox from "../components/video/MetadataBox.tsx";
|
||||
import {config} from "../config.ts";
|
||||
|
||||
const API_URL = config.apiUrl;
|
||||
|
||||
const ClipEdit = () => {
|
||||
const location = useLocation();
|
||||
@@ -111,14 +108,14 @@ const ClipEdit = () => {
|
||||
const handleDownload = async () => {
|
||||
if (!uploadedId) return;
|
||||
|
||||
const response = await fetch(API_URL + `/api/v1/jobs/${uploadedId}/download`, { credentials: 'include' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Download failed');
|
||||
let blob: Blob;
|
||||
try {
|
||||
blob = await downloadJob(uploadedId);
|
||||
} catch (err) {
|
||||
console.error('Download failed', err);
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
|
||||
|
||||
@@ -6,9 +6,43 @@ class AuthError extends Error {
|
||||
constructor() { super("Not authenticated"); this.name = "AuthError"; }
|
||||
}
|
||||
|
||||
const isThumbnailAvailable = async (thumbnailUrl: string): Promise<boolean> => {
|
||||
const response = await fetch(thumbnailUrl, { credentials: 'include' });
|
||||
return response.ok;
|
||||
};
|
||||
let onUnauthorized: (() => void) | null = null;
|
||||
|
||||
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 };
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
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 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;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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 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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
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}`);
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const deleteClip = async (id: number): Promise<void> => {
|
||||
const response = await fetch(API_URL + `/api/v1/clips/${id}`, {
|
||||
method: 'DELETE',
|
||||
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}`);
|
||||
const result = await apiFetch<APIResponse>(API_URL + `/api/v1/clips/${id}`, { method: 'DELETE' });
|
||||
if (result?.status === 'error') throw new Error(`Failed to delete clip: ${result.message}`);
|
||||
};
|
||||
|
||||
export { getClips, getClipById, getClipMediaUrl, patchClip, deleteClip };
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
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 response = await fetch(API_URL + `/api/v1/jobs/${uuid}`, { credentials: 'include' });
|
||||
|
||||
if (!response.ok) throw new Error(`Failed to fetch job: ${response.status}`);
|
||||
|
||||
const result: APIResponse = await response.json();
|
||||
const result = await apiFetch<APIResponse>(API_URL + `/api/v1/jobs/${uuid}`);
|
||||
if (result.status === 'error') throw new Error(`Failed to fetch job: ${result.message}`);
|
||||
|
||||
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 };
|
||||
|
||||
@@ -1,40 +1,28 @@
|
||||
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 response = await fetch(API_URL + '/api/v1/markers', { credentials: 'include' });
|
||||
if (!response.ok) throw new Error(`Failed to fetch markers: ${response.status}`);
|
||||
|
||||
const result: APIResponse = await response.json();
|
||||
const result = await apiFetch<APIResponse>(API_URL + '/api/v1/markers');
|
||||
if (result.status === 'error') throw new Error(`Failed to fetch markers: ${result.message}`);
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ message }),
|
||||
});
|
||||
|
||||
const result: APIResponse = await response.json();
|
||||
if (!response.ok || result.status === 'error') throw new Error(`Failed to add marker: ${result.message}`);
|
||||
if (result.status === 'error') throw new Error(`Failed to add marker: ${result.message}`);
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const deleteMarker = async (id: number): Promise<void> => {
|
||||
const response = await fetch(API_URL + `/api/v1/markers/${id}`, {
|
||||
method: 'DELETE',
|
||||
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}`);
|
||||
const result = await apiFetch<APIResponse>(API_URL + `/api/v1/markers/${id}`, { method: 'DELETE' });
|
||||
if (result?.status === 'error') throw new Error(`Failed to delete marker: ${result.message}`);
|
||||
};
|
||||
|
||||
export { getMarkers, createMarker, deleteMarker };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 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.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',
|
||||
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}`);
|
||||
|
||||
return result.data.uuid;
|
||||
};
|
||||
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ startTime, endTime, title, description }),
|
||||
});
|
||||
|
||||
const result: APIResponse = await response.json();
|
||||
if (!response.ok || result.status === 'error') throw new Error(`Failed to save section: ${result.message}`);
|
||||
if (result.status === 'error') throw new Error(`Failed to save section: ${result.message}`);
|
||||
|
||||
return result.data.uuid;
|
||||
};
|
||||
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ startMarkerId, endMarkerId, title, description }),
|
||||
});
|
||||
|
||||
const result: APIResponse = await response.json();
|
||||
if (!response.ok || result.status === 'error') throw new Error(`Failed to save section: ${result.message}`);
|
||||
if (result.status === 'error') throw new Error(`Failed to save section: ${result.message}`);
|
||||
|
||||
return result.data.uuid;
|
||||
};
|
||||
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ duration, title, description }),
|
||||
});
|
||||
|
||||
const result: APIResponse = await response.json();
|
||||
if (!response.ok || result.status === 'error') throw new Error(`Failed to clip section: ${result.message}`);
|
||||
if (result.status === 'error') throw new Error(`Failed to clip section: ${result.message}`);
|
||||
|
||||
return result.data.uuid;
|
||||
};
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
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 response = await fetch(API_URL + '/api/v1/stream/current', { credentials: 'include' });
|
||||
if (!response.ok) return false;
|
||||
const result: APIResponse = await response.json();
|
||||
return result.status === 'success' && result.data?.isStreaming === true;
|
||||
try {
|
||||
const result = await apiFetch<APIResponse>(API_URL + '/api/v1/stream/current');
|
||||
return result.status === 'success' && result.data?.isStreaming === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentStream = async (): Promise<StreamStatus> => {
|
||||
const response = await fetch(API_URL + '/api/v1/stream/current', { credentials: 'include' });
|
||||
if (!response.ok) throw new Error(`Failed to fetch stream status: ${response.status}`);
|
||||
|
||||
const result: APIResponse = await response.json();
|
||||
const result = await apiFetch<APIResponse>(API_URL + '/api/v1/stream/current');
|
||||
if (result.status === 'error') throw new Error(`Failed to fetch stream status: ${result.message}`);
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const getStreamHistory = async (userId: number): Promise<StreamHistoryItem[]> => {
|
||||
const response = await fetch(API_URL + `/api/v1/stream/history/${userId}`, { credentials: 'include' });
|
||||
if (!response.ok) throw new Error(`Failed to fetch stream history: ${response.status}`);
|
||||
|
||||
const result: APIResponse = await response.json();
|
||||
const result = await apiFetch<APIResponse>(API_URL + `/api/v1/stream/history/${userId}`);
|
||||
if (result.status === 'error') throw new Error(`Failed to fetch stream history: ${result.message}`);
|
||||
|
||||
return result.data;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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 response = await fetch(API_URL + '/api/v1/users/login', {
|
||||
method: 'POST',
|
||||
@@ -18,15 +21,8 @@ const login = async (googleToken: string): Promise<string> => {
|
||||
};
|
||||
|
||||
const logout = async (): Promise<void> => {
|
||||
const response = await fetch(API_URL + '/api/v1/users/logout', {
|
||||
method: 'POST',
|
||||
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 result = await apiFetch<APIResponse>(API_URL + '/api/v1/users/logout', { method: 'POST' });
|
||||
if (result?.status === 'error') throw new Error(`Logout failed: ${result.message}`);
|
||||
};
|
||||
|
||||
const getUser = async (): Promise<User | null> => {
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
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 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;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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 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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
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}`);
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const deleteVod = async (id: number): Promise<void> => {
|
||||
const response = await fetch(API_URL + `/api/v1/vods/${id}`, {
|
||||
method: 'DELETE',
|
||||
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}`);
|
||||
const result = await apiFetch<APIResponse>(API_URL + `/api/v1/vods/${id}`, { method: 'DELETE' });
|
||||
if (result?.status === 'error') throw new Error(`Failed to delete vod: ${result.message}`);
|
||||
};
|
||||
|
||||
export { getVods, getVodById, getVodMediaUrl, patchVod, deleteVod };
|
||||
|
||||
Reference in New Issue
Block a user