fix(admin): use absolute API URL for cross-origin admin fetches
CI / test (push) Successful in 1m39s
CI / docker (push) Successful in 33s

This commit is contained in:
zwitschi committed 2026-08-16 10:44:01 +02:00
1 parent c2f9f9ed62
commit 9a0511b36b
1 file changed
+21 -17
+21 -17
View File
@@ -31,6 +31,7 @@ const adminConfig = {
const MAX_UPLOAD_BYTES = 2 * 1024 * 1024 * 1024; const MAX_UPLOAD_BYTES = 2 * 1024 * 1024 * 1024;
const UPLOAD_CHUNK_BYTES = 8 * 1024 * 1024; const UPLOAD_CHUNK_BYTES = 8 * 1024 * 1024;
const UPLOAD_RESUME_KEY_PREFIX = "linearvoid-upload-resume:"; const UPLOAD_RESUME_KEY_PREFIX = "linearvoid-upload-resume:";
const API_URL = import.meta.env.VITE_PUBLIC_APP_URL?.trim() || window.location.origin;
const state = { const state = {
mediaUploading: false, mediaUploading: false,
@@ -131,7 +132,7 @@ const pageCache: Record<string, string> = {};
async function loadPageContent(tabName: string): Promise<string> { async function loadPageContent(tabName: string): Promise<string> {
if (pageCache[tabName]) return pageCache[tabName]; if (pageCache[tabName]) return pageCache[tabName];
const response = await fetch(`/admin/${tabName}.html`); const response = await fetch(`${API_URL}/admin/${tabName}.html`);
if (!response.ok) throw new Error(`Failed to load page: ${tabName}`); if (!response.ok) throw new Error(`Failed to load page: ${tabName}`);
const html = await response.text(); const html = await response.text();
pageCache[tabName] = html; pageCache[tabName] = html;
@@ -175,7 +176,7 @@ async function switchTab(tabName: string) {
export async function refreshOverview() { export async function refreshOverview() {
try { try {
const response = await fetch("/api/admin/overview"); const response = await fetch(`${API_URL}/api/admin/overview`);
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json(); const payload = await response.json();
@@ -202,7 +203,7 @@ export async function refreshOverview() {
export async function refreshAnalytics() { export async function refreshAnalytics() {
setStatus("analytics-status", "Loading analytics..."); setStatus("analytics-status", "Loading analytics...");
try { try {
const response = await fetch("/api/admin/analytics"); const response = await fetch(`${API_URL}/api/admin/analytics`);
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json(); const payload = await response.json();
@@ -240,7 +241,7 @@ export async function refreshAnalytics() {
async function refreshPlaylist() { async function refreshPlaylist() {
setStatus("playlist-status", "Loading playlist..."); setStatus("playlist-status", "Loading playlist...");
try { try {
const response = await fetch("/api/admin/playlist"); const response = await fetch(`${API_URL}/api/admin/playlist`);
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json(); const payload = await response.json();
@@ -522,7 +523,7 @@ async function submitOverride(e: Event) {
const endTime = (document.getElementById("override-end") as HTMLInputElement).value; const endTime = (document.getElementById("override-end") as HTMLInputElement).value;
const reason = (document.getElementById("override-reason") as HTMLInputElement).value; const reason = (document.getElementById("override-reason") as HTMLInputElement).value;
try { try {
const resp = await fetch("/api/admin/schedule/overrides", { const resp = await fetch(`${API_URL}/api/admin/schedule/overrides`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
@@ -551,7 +552,7 @@ async function submitOverride(e: Event) {
async function deleteOverride(id: number) { async function deleteOverride(id: number) {
try { try {
await fetch(`/api/admin/schedule/overrides?id=${id}`, { method: "DELETE" }); await fetch(`${API_URL}/api/admin/schedule/overrides?id=${id}`, { method: "DELETE" });
refreshOverrides(); refreshOverrides();
} catch { } catch {
/* ignore */ /* ignore */
@@ -749,9 +750,12 @@ async function uploadVideo(file: File) {
setStatus("media-status", `Uploading ${file.name} (${chunkIndex + 1}/${effectiveTotalChunks} chunks)...`); setStatus("media-status", `Uploading ${file.name} (${chunkIndex + 1}/${effectiveTotalChunks} chunks)...`);
} }
const completeResponse = await fetch(`/api/admin/media/uploads/${encodeURIComponent(uploadId)}/complete`, { const completeResponse = await fetch(
method: "POST", `${API_URL}/api/admin/media/uploads/${encodeURIComponent(uploadId)}/complete`,
}); {
method: "POST",
},
);
if (!completeResponse.ok) { if (!completeResponse.ok) {
throw new Error(await readErrorMessage(completeResponse)); throw new Error(await readErrorMessage(completeResponse));
@@ -791,7 +795,7 @@ async function editMedia(file: MediaEntry) {
setStatus("media-status", `Saving metadata for ${file.name}...`); setStatus("media-status", `Saving metadata for ${file.name}...`);
try { try {
const response = await fetch(`/api/admin/media/${file.id}`, { const response = await fetch(`${API_URL}/api/admin/media/${file.id}`, {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, status, tags }), body: JSON.stringify({ title, status, tags }),
@@ -816,7 +820,7 @@ async function normalizeMedia(file: MediaEntry) {
setStatus("media-status", `Normalizing ${file.name}...`); setStatus("media-status", `Normalizing ${file.name}...`);
try { try {
const response = await fetch(`/api/admin/media/${file.id}/normalize`, { method: "POST" }); const response = await fetch(`${API_URL}/api/admin/media/${file.id}/normalize`, { method: "POST" });
if (!response.ok) throw new Error(await readErrorMessage(response)); if (!response.ok) throw new Error(await readErrorMessage(response));
setStatus("media-status", `Normalization started for ${file.name}.`); setStatus("media-status", `Normalization started for ${file.name}.`);
@@ -836,7 +840,7 @@ function pollNormalizationStatus(id: number, attempt: number) {
} }
window.setTimeout(async () => { window.setTimeout(async () => {
try { try {
const response = await fetch(`/api/admin/media/${id}`); const response = await fetch(`${API_URL}/api/admin/media/${id}`);
if (!response.ok) return; if (!response.ok) return;
const file = (await response.json()) as MediaEntry; const file = (await response.json()) as MediaEntry;
if (file.status === "normalizing" || file.status === "needs_normalization") { if (file.status === "normalizing" || file.status === "needs_normalization") {
@@ -869,7 +873,7 @@ async function deleteMedia(file: MediaEntry) {
setStatus("media-status", `Deleting ${file.name}...`); setStatus("media-status", `Deleting ${file.name}...`);
try { try {
const response = await fetch(`/api/admin/media/${file.id}`, { const response = await fetch(`${API_URL}/api/admin/media/${file.id}`, {
method: "DELETE", method: "DELETE",
}); });
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
@@ -887,7 +891,7 @@ async function deleteMedia(file: MediaEntry) {
async function refreshModerationLogs() { async function refreshModerationLogs() {
setStatus("moderation-status", "Loading moderation logs..."); setStatus("moderation-status", "Loading moderation logs...");
try { try {
const response = await fetch(`/api/admin/moderation/logs?limit=${adminConfig.moderationLogLimit}`); const response = await fetch(`${API_URL}/api/admin/moderation/logs?limit=${adminConfig.moderationLogLimit}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json(); const payload = await response.json();
@@ -988,7 +992,7 @@ async function refreshChatLog() {
if (chatLogState.username) params.set("username", chatLogState.username); if (chatLogState.username) params.set("username", chatLogState.username);
if (chatLogState.text) params.set("text", chatLogState.text); if (chatLogState.text) params.set("text", chatLogState.text);
const response = await fetch(`/api/admin/chat-log?${params}`); const response = await fetch(`${API_URL}/api/admin/chat-log?${params}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json(); const payload = await response.json();
@@ -1118,7 +1122,7 @@ async function deleteChatEntry(id: number) {
setStatus("chat-log-status", `Deleting message #${id}...`); setStatus("chat-log-status", `Deleting message #${id}...`);
try { try {
const response = await fetch(`/api/admin/chat-log/${id}`, { const response = await fetch(`${API_URL}/api/admin/chat-log/${id}`, {
method: "DELETE", method: "DELETE",
}); });
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
@@ -1268,7 +1272,7 @@ async function restartService(service: string) {
const status = document.getElementById("restart-status"); const status = document.getElementById("restart-status");
if (status) status.textContent = `Requesting restart of ${service}...`; if (status) status.textContent = `Requesting restart of ${service}...`;
try { try {
const resp = await fetch(`/api/admin/restart?service=${service}`, { method: "POST" }); const resp = await fetch(`${API_URL}/api/admin/restart?service=${service}`, { method: "POST" });
const data = await resp.json(); const data = await resp.json();
if (status) status.textContent = data.message; if (status) status.textContent = data.message;
} catch (err) { } catch (err) {