update references to codey.lol -> codey.horse [new domain]
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# codey.lol
|
||||
codey.lol is a web app built with Astro, TypeScript, Tailwind CSS, React, and Vite.
|
||||
# codey.horse
|
||||
codey.horse is a web app built with Astro, TypeScript, Tailwind CSS, React, and Vite.
|
||||
|
||||
## Pages Overview
|
||||
|
||||
|
||||
@@ -665,7 +665,7 @@ const ARCHIVE_USERS = {
|
||||
id: '456226577798135808',
|
||||
username: 'slip',
|
||||
displayName: 'poopboy',
|
||||
avatar: 'https://codey.lol/images/456226577798135808.png',
|
||||
avatar: 'https://codey.horse/images/456226577798135808.png',
|
||||
color: null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ function clearCookie(name: string): void {
|
||||
}
|
||||
|
||||
// Trusted domains for redirect validation
|
||||
const TRUSTED_DOMAINS = ['codey.lol', 'boatson.boats'];
|
||||
const TRUSTED_DOMAINS = ['codey.horse', 'boatson.boats'];
|
||||
|
||||
/**
|
||||
* Parse and decode the redirect URL from query params
|
||||
|
||||
@@ -8,7 +8,7 @@ import { toast } from 'react-toastify';
|
||||
import { API_URL } from '../config';
|
||||
|
||||
const MEME_API_URL = `${API_URL}/memes/list_memes`;
|
||||
const BASE_IMAGE_URL = "https://codey.lol/meme";
|
||||
const BASE_IMAGE_URL = "https://codey.horse/meme";
|
||||
|
||||
interface MemeImage {
|
||||
id: string;
|
||||
|
||||
@@ -203,7 +203,7 @@ export default function Player({ user }: PlayerProps) {
|
||||
setIsStreamReady(false);
|
||||
};
|
||||
|
||||
const streamUrl = `https://stream.codey.lol/hls/${station}/${station}.m3u8`;
|
||||
const streamUrl = `https://stream.codey.horse/hls/${station}/${station}.m3u8`;
|
||||
|
||||
// Check for native HLS support (Safari)
|
||||
if (audio.canPlayType("application/vnd.apple.mpegurl")) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect, useRef, Suspense, lazy, useMemo } from "react";
|
||||
import React, { useState, useEffect, useRef, Suspense, lazy, useMemo, useCallback } from "react";
|
||||
import type { RefObject } from "react";
|
||||
import type { Id } from "react-toastify";
|
||||
import { toast } from "react-toastify";
|
||||
import { Button } from "@mui/joy";
|
||||
import { Accordion, AccordionTab } from "primereact/accordion";
|
||||
import { AutoComplete } from "primereact/autocomplete";
|
||||
import { authFetch } from "@/utils/authFetch";
|
||||
import { authFetch, doRefresh } from "@/utils/authFetch";
|
||||
import { useAutoCompleteScrollFix } from '@/hooks/useAutoCompleteScrollFix';
|
||||
import BreadcrumbNav from "./BreadcrumbNav";
|
||||
import { API_URL, ENVIRONMENT } from "@/config";
|
||||
@@ -114,7 +114,21 @@ export default function MediaRequestForm() {
|
||||
const [videoPreviewId, setVideoPreviewId] = useState<string | number | null>(null);
|
||||
const [videoStreamUrl, setVideoStreamUrl] = useState<string | null>(null);
|
||||
const [isVideoLoading, setIsVideoLoading] = useState(false);
|
||||
const [videoDownloadProgress, setVideoDownloadProgress] = useState<number>(0); // 0-100
|
||||
const [showVideoSection, setShowVideoSection] = useState(false);
|
||||
const videoAbortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const resetVideoState = () => {
|
||||
videoAbortRef.current?.abort();
|
||||
if (videoStreamUrl?.startsWith("blob:")) URL.revokeObjectURL(videoStreamUrl);
|
||||
setVideoResults([]);
|
||||
setSelectedVideos(new Set());
|
||||
setVideoPreviewId(null);
|
||||
setVideoStreamUrl(null);
|
||||
setVideoSearchQuery("");
|
||||
setVideoDownloadProgress(0);
|
||||
setIsVideoLoading(false);
|
||||
};
|
||||
|
||||
const { attachScrollFix, cleanupScrollFix } = useAutoCompleteScrollFix();
|
||||
|
||||
@@ -506,6 +520,8 @@ export default function MediaRequestForm() {
|
||||
toast.dismiss();
|
||||
setIsSearching(true);
|
||||
resetQueueState();
|
||||
const videosWereOpen = showVideoSection;
|
||||
resetVideoState();
|
||||
setShuffleAlbums({});
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
@@ -558,6 +574,11 @@ export default function MediaRequestForm() {
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
// If the video section was open before, auto-fetch videos for the new artist
|
||||
if (videosWereOpen && selectedArtist) {
|
||||
fetchArtistVideos(selectedArtist.id);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error("Failed to fetch albums for artist.");
|
||||
setAlbums([]);
|
||||
@@ -793,27 +814,80 @@ export default function MediaRequestForm() {
|
||||
};
|
||||
|
||||
const handleVideoPreview = async (video: Video) => {
|
||||
if (videoPreviewId === video.id && videoStreamUrl) {
|
||||
// Already previewing this video, close it
|
||||
if (videoPreviewId === video.id) {
|
||||
// Already previewing this video — toggle off
|
||||
videoAbortRef.current?.abort();
|
||||
if (videoStreamUrl?.startsWith("blob:")) URL.revokeObjectURL(videoStreamUrl);
|
||||
setVideoPreviewId(null);
|
||||
setVideoStreamUrl(null);
|
||||
setVideoDownloadProgress(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Abort any in-flight preview download
|
||||
videoAbortRef.current?.abort();
|
||||
if (videoStreamUrl?.startsWith("blob:")) URL.revokeObjectURL(videoStreamUrl);
|
||||
|
||||
const abort = new AbortController();
|
||||
videoAbortRef.current = abort;
|
||||
|
||||
setVideoPreviewId(video.id);
|
||||
setVideoStreamUrl(null);
|
||||
setIsVideoLoading(true);
|
||||
setVideoDownloadProgress(0);
|
||||
|
||||
// Use XMLHttpRequest instead of fetch to avoid tying up a fetch
|
||||
// connection slot — the browser limits concurrent fetch connections
|
||||
// per origin (~6), and a long video download via ReadableStream
|
||||
// would starve the sequential track metadata fetches.
|
||||
const doXhr = (retry: boolean): Promise<void> => new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", `${API_URL}/trip/video/${video.id}/download`);
|
||||
xhr.responseType = "blob";
|
||||
xhr.withCredentials = true; // send cookies
|
||||
|
||||
abort.signal.addEventListener("abort", () => {
|
||||
xhr.abort();
|
||||
reject(new DOMException("Aborted", "AbortError"));
|
||||
});
|
||||
|
||||
xhr.onprogress = (e) => {
|
||||
if (e.lengthComputable) {
|
||||
setVideoDownloadProgress(Math.round((e.loaded / e.total) * 100));
|
||||
} else {
|
||||
setVideoDownloadProgress(Math.min(95, Math.round(e.loaded / 1024 / 1024)));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
const blobUrl = URL.createObjectURL(xhr.response);
|
||||
setVideoStreamUrl(blobUrl);
|
||||
setVideoDownloadProgress(100);
|
||||
resolve();
|
||||
} else if (xhr.status === 401 && retry) {
|
||||
// Token expired — refresh and retry once
|
||||
doRefresh().then((ok) => {
|
||||
if (!ok || abort.signal.aborted) {
|
||||
reject(new Error("Auth refresh failed"));
|
||||
return;
|
||||
}
|
||||
setVideoDownloadProgress(0);
|
||||
doXhr(false).then(resolve, reject);
|
||||
}).catch(reject);
|
||||
} else {
|
||||
reject(new Error(`Failed to fetch video: ${xhr.status}`));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => reject(new Error("Network error downloading video"));
|
||||
xhr.send();
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await authFetch(`${API_URL}/trip/video/${video.id}/stream`);
|
||||
if (!res.ok) throw new Error("Failed to fetch video stream URL");
|
||||
const data = await res.json();
|
||||
if (data.stream_url || data.url) {
|
||||
setVideoStreamUrl(data.stream_url || data.url);
|
||||
} else {
|
||||
throw new Error("No stream URL returned");
|
||||
}
|
||||
} catch (err) {
|
||||
await doXhr(true);
|
||||
} catch (err: any) {
|
||||
if (err?.name === "AbortError") return;
|
||||
console.error(err);
|
||||
toast.error("Failed to load video preview.");
|
||||
setVideoPreviewId(null);
|
||||
@@ -993,6 +1067,8 @@ export default function MediaRequestForm() {
|
||||
audio.removeEventListener("seeked", updateProgress);
|
||||
Object.values(audioSourcesRef.current).forEach((url) => URL.revokeObjectURL(url));
|
||||
audioSourcesRef.current = {};
|
||||
// Abort any in-flight video download and revoke blob URL
|
||||
videoAbortRef.current?.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -1072,6 +1148,7 @@ export default function MediaRequestForm() {
|
||||
|
||||
const fetchTracksSequentially: FetchTracksSequentiallyFn = async () => {
|
||||
const minDelay = 650; // ms between API requests
|
||||
const maxRetries = 3; // retry up to 3 times on failure
|
||||
setIsFetching(true);
|
||||
|
||||
const totalAlbums = albumsToFetch.length;
|
||||
@@ -1083,28 +1160,38 @@ export default function MediaRequestForm() {
|
||||
|
||||
setLoadingAlbumId(album.id);
|
||||
|
||||
try {
|
||||
const now = Date.now();
|
||||
if (!fetchTracksSequentially.lastCall) fetchTracksSequentially.lastCall = 0;
|
||||
const elapsed = now - fetchTracksSequentially.lastCall;
|
||||
if (elapsed < minDelay) await delay(minDelay - elapsed);
|
||||
fetchTracksSequentially.lastCall = Date.now();
|
||||
let succeeded = false;
|
||||
for (let attempt = 0; attempt < maxRetries && !isCancelled; attempt++) {
|
||||
try {
|
||||
const now = Date.now();
|
||||
if (!fetchTracksSequentially.lastCall) fetchTracksSequentially.lastCall = 0;
|
||||
const elapsed = now - fetchTracksSequentially.lastCall;
|
||||
const currentDelay = attempt === 0 ? minDelay : minDelay + attempt * 1000;
|
||||
if (elapsed < currentDelay) await delay(currentDelay - elapsed);
|
||||
fetchTracksSequentially.lastCall = Date.now();
|
||||
|
||||
const res = await authFetch(`${API_URL}/trip/get_tracks_by_album_id/${album.id}`);
|
||||
if (!res.ok) throw new Error("API error");
|
||||
const data = await res.json();
|
||||
const res = await authFetch(`${API_URL}/trip/get_tracks_by_album_id/${album.id}`);
|
||||
if (!res.ok) throw new Error("API error");
|
||||
const data = await res.json();
|
||||
|
||||
if (isCancelled) break;
|
||||
if (isCancelled) break;
|
||||
|
||||
setTracksByAlbum((prev) => ({ ...prev, [album.id]: data }));
|
||||
setSelectedTracks((prev) => ({
|
||||
...prev,
|
||||
[album.id]: data.map((t) => String(t.id)),
|
||||
}));
|
||||
} catch (err) {
|
||||
toast.error(`Failed to fetch tracks for album ${album.album}.`);
|
||||
setTracksByAlbum((prev) => ({ ...prev, [album.id]: [] }));
|
||||
setSelectedTracks((prev) => ({ ...prev, [album.id]: [] }));
|
||||
setTracksByAlbum((prev) => ({ ...prev, [album.id]: data }));
|
||||
setSelectedTracks((prev) => ({
|
||||
...prev,
|
||||
[album.id]: data.map((t) => String(t.id)),
|
||||
}));
|
||||
succeeded = true;
|
||||
break; // success — move to next album
|
||||
} catch (err) {
|
||||
if (attempt < maxRetries - 1) {
|
||||
console.warn(`Retry ${attempt + 1}/${maxRetries} for album "${album.album}" (id ${album.id})`);
|
||||
} else {
|
||||
toast.error(`Failed to fetch tracks for album ${album.album}.`);
|
||||
setTracksByAlbum((prev) => ({ ...prev, [album.id]: [] }));
|
||||
setSelectedTracks((prev) => ({ ...prev, [album.id]: [] }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress toast
|
||||
@@ -1596,8 +1683,8 @@ export default function MediaRequestForm() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Videos Section - Show when artist is selected */}
|
||||
{selectedArtist && (
|
||||
{/* Videos Section - Show when artist is selected or videos are loaded */}
|
||||
{(selectedArtist || (showVideoSection && videoResults.length > 0)) && (
|
||||
<div className="mt-8 pt-6 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
@@ -1694,41 +1781,78 @@ export default function MediaRequestForm() {
|
||||
: "border-neutral-200 dark:border-neutral-700"
|
||||
}`}
|
||||
>
|
||||
{/* Video Thumbnail */}
|
||||
<div
|
||||
className="relative aspect-video bg-neutral-200 dark:bg-neutral-800 cursor-pointer group"
|
||||
onClick={() => handleVideoPreview(video)}
|
||||
>
|
||||
{video.image || video.imageUrl ? (
|
||||
<img
|
||||
src={video.image || video.imageUrl}
|
||||
alt={video.title}
|
||||
className="w-full h-full object-cover"
|
||||
{/* Video Thumbnail / Player area */}
|
||||
{videoPreviewId === video.id && videoStreamUrl ? (
|
||||
/* Active video player — blob URL has correct duration & seeking */
|
||||
<div className="relative aspect-video bg-black">
|
||||
<video
|
||||
src={videoStreamUrl}
|
||||
controls
|
||||
autoPlay
|
||||
className="w-full h-full"
|
||||
onError={() => {
|
||||
toast.error("Failed to play video");
|
||||
if (videoStreamUrl?.startsWith("blob:")) URL.revokeObjectURL(videoStreamUrl);
|
||||
setVideoPreviewId(null);
|
||||
setVideoStreamUrl(null);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-neutral-400">
|
||||
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{/* Play overlay */}
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{isVideoLoading && videoPreviewId === video.id ? (
|
||||
<InlineSpinner sizeClass="h-8 w-8" />
|
||||
</div>
|
||||
) : (
|
||||
/* Thumbnail with play/progress overlay */
|
||||
<div
|
||||
className="relative aspect-video bg-neutral-200 dark:bg-neutral-800 cursor-pointer group"
|
||||
onClick={() => handleVideoPreview(video)}
|
||||
>
|
||||
{video.image || video.imageUrl ? (
|
||||
<img
|
||||
src={video.image || video.imageUrl}
|
||||
alt={video.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
<div className="w-full h-full flex items-center justify-center text-neutral-400">
|
||||
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{/* Play / Download progress overlay */}
|
||||
<div className={`absolute inset-0 flex flex-col items-center justify-center transition-opacity ${
|
||||
isVideoLoading && videoPreviewId === video.id
|
||||
? 'bg-black/60 opacity-100'
|
||||
: 'bg-black/30 opacity-0 group-hover:opacity-100'
|
||||
}`}>
|
||||
{isVideoLoading && videoPreviewId === video.id ? (
|
||||
<>
|
||||
<InlineSpinner sizeClass="h-6 w-6" />
|
||||
<span className="text-white text-xs mt-2 font-medium">
|
||||
{videoDownloadProgress > 0
|
||||
? `Loading… ${videoDownloadProgress}%`
|
||||
: 'Connecting…'}
|
||||
</span>
|
||||
{/* Progress bar */}
|
||||
<div className="w-3/4 mt-2 h-1.5 bg-white/20 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-400 rounded-full transition-all duration-300"
|
||||
style={{ width: `${videoDownloadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{/* Duration badge */}
|
||||
{video.duration && (
|
||||
<span className="absolute bottom-2 right-2 px-1.5 py-0.5 bg-black/70 text-white text-xs rounded">
|
||||
{formatVideoDuration(video.duration)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Duration badge */}
|
||||
{video.duration && (
|
||||
<span className="absolute bottom-2 right-2 px-1.5 py-0.5 bg-black/70 text-white text-xs rounded">
|
||||
{formatVideoDuration(video.duration)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Video Info */}
|
||||
<div className="p-3">
|
||||
@@ -1759,23 +1883,6 @@ export default function MediaRequestForm() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Video Preview Player */}
|
||||
{videoPreviewId === video.id && videoStreamUrl && (
|
||||
<div className="border-t border-neutral-200 dark:border-neutral-700">
|
||||
<video
|
||||
src={videoStreamUrl}
|
||||
controls
|
||||
autoPlay
|
||||
className="w-full"
|
||||
onError={() => {
|
||||
toast.error("Failed to play video");
|
||||
setVideoPreviewId(null);
|
||||
setVideoStreamUrl(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ interface RequestJob {
|
||||
}
|
||||
|
||||
const STATUS_OPTIONS = ["Queued", "Started", "Compressing", "Finished", "Failed"];
|
||||
const TAR_BASE_URL = "https://kr.codey.lol"; // configurable prefix
|
||||
const TAR_BASE_URL = "https://kr.codey.horse"; // configurable prefix
|
||||
|
||||
export default function RequestManagement() {
|
||||
const [requests, setRequests] = useState<RequestJob[]>([]);
|
||||
@@ -65,8 +65,8 @@ export default function RequestManagement() {
|
||||
// Check if path is /storage/music/TRIP
|
||||
if (absPath.includes("/storage/music/TRIP/")) {
|
||||
return isVideo
|
||||
? `https://_music.codey.lol/TRIP/videos/${filename}`
|
||||
: `https://_music.codey.lol/TRIP/${filename}`;
|
||||
? `https://_music.codey.horse/TRIP/videos/${filename}`
|
||||
: `https://_music.codey.horse/TRIP/${filename}`;
|
||||
}
|
||||
|
||||
// Otherwise, assume /storage/music2/completed/{quality} format
|
||||
|
||||
@@ -39,9 +39,9 @@ export interface ProtectedRoute {
|
||||
}
|
||||
|
||||
export const metaData: MetaData = {
|
||||
baseUrl: "https://codey.lol/",
|
||||
baseUrl: "https://codey.horse/",
|
||||
title: "CODEY STUFF",
|
||||
name: "codey.lol",
|
||||
name: "codey.horse",
|
||||
owner: "codey",
|
||||
ogImage: "/images/favicon.png",
|
||||
description: "CODEY STUFF!",
|
||||
@@ -57,8 +57,8 @@ export const metaData: MetaData = {
|
||||
],
|
||||
};
|
||||
|
||||
export const API_URL: string = "https://api.codey.lol";
|
||||
export const RADIO_API_URL: string = "https://radio-api.codey.lol";
|
||||
export const API_URL: string = "https://api.codey.horse";
|
||||
export const RADIO_API_URL: string = "https://radio-api.codey.horse";
|
||||
|
||||
export const socialLinks: Record<string, string> = {
|
||||
};
|
||||
|
||||
@@ -24,15 +24,15 @@ const navItems = [
|
||||
{ label: "Discord Logs", href: "/discord-logs", auth: true },
|
||||
{ label: "Lighting", href: "/lighting", auth: true, adminOnly: true },
|
||||
{ label: "Git", href: "https://kode.boatson.boats", icon: "external" },
|
||||
{ label: "Glances", href: "https://_gl.codey.lol", auth: true, icon: "external",
|
||||
{ label: "Glances", href: "https://_gl.codey.horse", auth: true, icon: "external",
|
||||
adminOnly: true },
|
||||
{ label: "PSQL", href: "https://_pg.codey.lol", auth: true, icon: "external",
|
||||
{ label: "PSQL", href: "https://_pg.codey.horse", auth: true, icon: "external",
|
||||
adminOnly: true },
|
||||
{ label: "qBitTorrent", href: "https://_qb.codey.lol", auth: true, icon: "external",
|
||||
{ label: "qBitTorrent", href: "https://_qb.codey.horse", auth: true, icon: "external",
|
||||
adminOnly: true },
|
||||
{ label: "RQ", href: "https://_rq.codey.lol", auth: true, icon: "external",
|
||||
{ label: "RQ", href: "https://_rq.codey.horse", auth: true, icon: "external",
|
||||
adminOnly: true },
|
||||
{ label: "RI", href: "https://_r0.codey.lol", auth: true, icon: "external",
|
||||
{ label: "RI", href: "https://_r0.codey.horse", auth: true, icon: "external",
|
||||
adminOnly: true },
|
||||
// { label: "Status", href: "https://status.boatson.boats", icon: "external" },
|
||||
{ label: "Login", href: "/login", guestOnly: true },
|
||||
|
||||
@@ -26,7 +26,7 @@ if (typeof globalThis.Headers !== 'undefined' && typeof globalThis.Headers.proto
|
||||
}
|
||||
}
|
||||
|
||||
const API_URL = "https://api.codey.lol";
|
||||
const API_URL = "https://api.codey.horse";
|
||||
const AUTH_TIMEOUT_MS = 3000; // 3 second timeout for auth requests
|
||||
|
||||
// Deduplication for concurrent refresh requests (prevents race condition where
|
||||
@@ -392,7 +392,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Block /subsites/req/* on main domain (codey.lol, local.codey.lol, etc)
|
||||
// Block /subsites/req/* on main domain (codey.horse, local.codey.horse, etc)
|
||||
const isMainDomain = !wantsSubsite;
|
||||
if (isMainDomain && Object.values(subsites || {}).some(p => context.url.pathname.startsWith(p))) {
|
||||
// Immediately return a 404 for /req on the main domain
|
||||
@@ -460,7 +460,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
"font-src 'self' https://fonts.gstatic.com data:",
|
||||
"img-src 'self' data: blob: https: http:",
|
||||
"media-src 'self' blob: https:",
|
||||
"connect-src 'self' https://api.codey.lol https://*.codey.lol https://*.audio.tidal.com wss:",
|
||||
"connect-src 'self' https://api.codey.horse https://*.codey.horse https://*.audio.tidal.com wss:",
|
||||
// Allow YouTube for video embeds and Cloudflare for challenges/Turnstile
|
||||
"frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com https://challenges.cloudflare.com",
|
||||
"object-src 'none'",
|
||||
|
||||
@@ -64,7 +64,7 @@ if (typeof globalThis.Headers !== 'undefined' && typeof (globalThis.Headers.prot
|
||||
}
|
||||
}
|
||||
|
||||
const API_URL: string = "https://api.codey.lol";
|
||||
const API_URL: string = "https://api.codey.horse";
|
||||
const AUTH_TIMEOUT_MS: number = 3000; // 3 second timeout for auth requests
|
||||
|
||||
// Deduplication for concurrent refresh requests (prevents race condition where
|
||||
@@ -430,7 +430,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Block /subsites/req/* on main domain (codey.lol, local.codey.lol, etc)
|
||||
// Block /subsites/req/* on main domain (codey.horse, local.codey.horse, etc)
|
||||
const isMainDomain = !wantsSubsite;
|
||||
if (isMainDomain && Object.values(subsites || {}).some(p => context.url.pathname.startsWith(p))) {
|
||||
// Immediately return a 404 for /req on the main domain
|
||||
@@ -501,7 +501,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
"font-src 'self' https://fonts.gstatic.com data:",
|
||||
"img-src 'self' data: blob: https: http:",
|
||||
"media-src 'self' blob: https:",
|
||||
"connect-src 'self' https://api.codey.lol https://*.codey.lol https://*.audio.tidal.com wss:",
|
||||
"connect-src 'self' https://api.codey.horse https://*.codey.horse https://*.audio.tidal.com wss:",
|
||||
// Allow YouTube for video embeds and Cloudflare for challenges/Turnstile
|
||||
"frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com https://challenges.cloudflare.com",
|
||||
"object-src 'none'",
|
||||
|
||||
@@ -23,7 +23,7 @@ const getCookie = (name: string): string | null => {
|
||||
// Detect if returnUrl is external (nginx-protected vhost redirect)
|
||||
// If logged-in user arrives with external returnUrl, nginx denied them - show access denied
|
||||
const returnUrl = Astro.url.searchParams.get('returnUrl') || Astro.url.searchParams.get('redirect');
|
||||
const trustedDomains = ['codey.lol', 'boatson.boats'];
|
||||
const trustedDomains = ['codey.horse', 'boatson.boats'];
|
||||
let isExternalReturn = false;
|
||||
let externalHostname = '';
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export const authFetch = async (
|
||||
};
|
||||
|
||||
// Centralized refresh function that handles deduplication properly
|
||||
async function doRefresh(): Promise<boolean> {
|
||||
export async function doRefresh(): Promise<boolean> {
|
||||
const now = Date.now();
|
||||
|
||||
// If a refresh just succeeded recently, assume we're good
|
||||
|
||||
Reference in New Issue
Block a user