Files
vtuber-awards/frontend/src/lib/http.ts
T
AzuTear b53c7fb736 Add viewer-range categories, nomination tracking, dynamic showact form, session timeout, share URLs, and workflow-per-season
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>
2026-06-28 23:32:21 +02:00

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>
}