b53c7fb736
Features: - Category viewer ranges + subcategory templates (admin group modal, tree workspace) - Nomination enrichment via TwitchTracker API (NominationEnrichmentService, TwitchTrackerViewerStatsProvider) with admin tracking rules editor - Nomination group tracker: CategoryGroupName as primary identifier, CategoryId stays as nullable legacy field; StreamerIdentity table - Dynamic showact application form builder (AdminShowactFormBuilder, ShowactApplicationSchedule) - Session idle timeout setting (AdminSessionTimeoutCard) - Share URLs for X and Discord (SiteSettings, public extras) - Workflow rules now stored per season (falls back to global SiteSettings) - New admin routes: settings/access, settings/workflows, tracking-rules - New admin review workspace with subcategory tabs - AdminCategoriesView rebuilt with group/subcategory modals Migrations (all additive): - AddShareUrls, AddShowactDynamicForm, AddCategoryViewerRanges, AddSessionIdleTimeoutSettings, AddSeasonSubcategoryTemplates, AddNominationGroupTrackerIdentity, AddShowactApplicationSchedule, AddSeasonWorkflowRulesJson Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
73 lines
1.9 KiB
TypeScript
73 lines
1.9 KiB
TypeScript
import { notifyAuthCleared } from './authSessionManager'
|
|
import { AUTH_TOKEN_KEY } from './authConstants'
|
|
|
|
const API_URL = (import.meta.env.VITE_API_URL ?? '').replace(/\/$/, '')
|
|
const API_LABEL = API_URL || 'same-origin /api'
|
|
|
|
export class ApiRequestError extends Error {
|
|
status: number | null
|
|
|
|
constructor(message: string, status: number | null = null) {
|
|
super(message)
|
|
this.name = 'ApiRequestError'
|
|
this.status = status
|
|
}
|
|
}
|
|
|
|
function getAuthToken() {
|
|
if (typeof window === 'undefined') return null
|
|
return window.localStorage.getItem(AUTH_TOKEN_KEY)
|
|
}
|
|
|
|
function clearAuthToken() {
|
|
if (typeof window === 'undefined') return
|
|
window.localStorage.removeItem(AUTH_TOKEN_KEY)
|
|
notifyAuthCleared()
|
|
}
|
|
|
|
async function parseError(response: Response, path: string) {
|
|
const error = await response.text().catch(() => '')
|
|
const message = extractErrorMessage(error) || `API request failed for ${path}`
|
|
throw new ApiRequestError(message, response.status)
|
|
}
|
|
|
|
function extractErrorMessage(error: string) {
|
|
if (!error.trim()) return ''
|
|
|
|
try {
|
|
const parsed = JSON.parse(error) as { message?: unknown; title?: unknown; detail?: unknown }
|
|
const message = parsed.message ?? parsed.detail ?? parsed.title
|
|
return typeof message === 'string' ? message : error
|
|
} catch {
|
|
return error
|
|
}
|
|
}
|
|
|
|
export async function requestJson<TResponse>(
|
|
path: string,
|
|
options: RequestInit = {},
|
|
): Promise<TResponse> {
|
|
const token = getAuthToken()
|
|
const headers = new Headers(options.headers)
|
|
|
|
if (token) {
|
|
headers.set('Authorization', `Bearer ${token}`)
|
|
}
|
|
|
|
const response = await fetch(`${API_URL}${path}`, {
|
|
...options,
|
|
headers,
|
|
}).catch(() => {
|
|
throw new ApiRequestError(`API nicht erreichbar (${API_LABEL}). Bitte Backend starten.`)
|
|
})
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401 || response.status === 403) {
|
|
clearAuthToken()
|
|
}
|
|
await parseError(response, path)
|
|
}
|
|
|
|
return response.json() as Promise<TResponse>
|
|
}
|