import config from '@/config'; const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '[::1]']); const EXPLICIT_BACKEND_PORT = process.env.NEXT_PUBLIC_BACKEND_PORT; const buildBaseUrl = (protocol: string, hostname: string, port?: string) => { if (port && port.length > 0) { return `${protocol}//${hostname}:${port}`; } return `${protocol}//${hostname}`; }; const inferPort = (configuredPort?: string, runtimePort?: string, runtimeProtocol?: string): string | undefined => { if (EXPLICIT_BACKEND_PORT && EXPLICIT_BACKEND_PORT.length > 0) { return EXPLICIT_BACKEND_PORT; } if (configuredPort && configuredPort.length > 0) { return configuredPort; } if (runtimePort && runtimePort.length > 0) { return runtimePort === '3001' ? '3000' : runtimePort; } if (runtimeProtocol === 'https:') { return undefined; } return undefined; }; const resolveBrowserBaseUrl = (staticBaseUrl: string) => { if (typeof window === 'undefined') { return staticBaseUrl; } const { protocol, hostname, port } = window.location; if (!staticBaseUrl || staticBaseUrl.length === 0) { const inferredPort = inferPort(undefined, port, protocol); return buildBaseUrl(protocol, hostname, inferredPort); } try { const parsed = new URL(staticBaseUrl); const shouldSwapHost = LOOPBACK_HOSTS.has(parsed.hostname) && !LOOPBACK_HOSTS.has(hostname); if (shouldSwapHost) { const inferredPort = inferPort(parsed.port, port, parsed.protocol); return buildBaseUrl(parsed.protocol, hostname, inferredPort); } return staticBaseUrl; } catch { return staticBaseUrl; } }; interface ApiOptions { method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; headers?: Record; body?: any; auth?: boolean; } class ApiClient { private baseUrl: string; private cachedBrowserBaseUrl: string | null = null; constructor(baseUrl: string) { this.baseUrl = baseUrl; } private getBaseUrl(): string { if (typeof window === 'undefined') { return this.baseUrl || 'http://localhost:3000'; } if (this.cachedBrowserBaseUrl) { return this.cachedBrowserBaseUrl; } const resolved = resolveBrowserBaseUrl(this.baseUrl || 'http://localhost:3000'); this.cachedBrowserBaseUrl = resolved; return resolved; } private getAuthToken(): string | null { if (typeof window === 'undefined') return null; return localStorage.getItem('auth_token'); } async request(path: string, options: ApiOptions = {}): Promise { const { method = 'GET', headers = {}, body, auth = false } = options; const url = `${this.getBaseUrl()}${path}`; const requestHeaders: Record = { 'Content-Type': 'application/json', ...headers, }; if (auth) { const token = this.getAuthToken(); if (token) { requestHeaders['Authorization'] = `Bearer ${token}`; } } const requestOptions: RequestInit = { method, headers: requestHeaders, }; if (body && method !== 'GET') { requestOptions.body = JSON.stringify(body); } try { const response = await fetch(url, requestOptions); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'API request failed'); } return data; } catch (error: any) { console.error('API request error:', error); throw error; } } // Public endpoints async getNextJackpot() { return this.request('/jackpot/next'); } async buyTickets(lightningAddress: string, tickets: number, buyerName?: string) { return this.request('/jackpot/buy', { method: 'POST', body: { lightning_address: lightningAddress, tickets, name: buyerName }, auth: true, }); } async getTicketStatus(ticketPurchaseId: string) { return this.request(`/tickets/${ticketPurchaseId}`); } async getPastWins(limit = 25, offset = 0) { const params = new URLSearchParams({ limit: String(limit), offset: String(offset), }); return this.request(`/jackpot/past-wins?${params.toString()}`); } async getMaintenanceStatus(): Promise<{ maintenance_mode: boolean; maintenance_pending: boolean; message: string | null }> { try { const response = await this.request<{ data: { maintenance_mode: boolean; maintenance_pending: boolean; message: string | null } }>('/status/maintenance'); return response.data; } catch { // If endpoint doesn't exist or fails, assume not in maintenance return { maintenance_mode: false, maintenance_pending: false, message: null }; } } // Auth endpoints async nostrAuth(nostrPubkey: string, signedMessage: string, nonce: string) { return this.request('/auth/nostr', { method: 'POST', body: { nostr_pubkey: nostrPubkey, signed_message: signedMessage, nonce }, }); } // User endpoints async getProfile() { return this.request('/me', { auth: true }); } async updateLightningAddress(lightningAddress: string) { return this.request('/me/lightning-address', { method: 'PATCH', body: { lightning_address: lightningAddress }, auth: true, }); } async getUserTickets(limit = 50, offset = 0) { return this.request(`/me/tickets?limit=${limit}&offset=${offset}`, { auth: true }); } async getUserWins() { return this.request('/me/wins', { auth: true }); } // Admin endpoints async listCycles(status?: string, cycleType?: string) { const params = new URLSearchParams(); if (status) params.append('status', status); if (cycleType) params.append('cycle_type', cycleType); return this.request(`/admin/cycles?${params.toString()}`, { headers: { 'X-Admin-Key': process.env.NEXT_PUBLIC_ADMIN_KEY || '' }, }); } async runDrawManually(cycleId: string) { return this.request(`/admin/cycles/${cycleId}/run-draw`, { method: 'POST', headers: { 'X-Admin-Key': process.env.NEXT_PUBLIC_ADMIN_KEY || '' }, }); } async listPayouts(status?: string) { const params = status ? `?status=${status}` : ''; return this.request(`/admin/payouts${params}`, { headers: { 'X-Admin-Key': process.env.NEXT_PUBLIC_ADMIN_KEY || '' }, }); } async retryPayout(payoutId: string) { return this.request(`/admin/payouts/${payoutId}/retry`, { method: 'POST', headers: { 'X-Admin-Key': process.env.NEXT_PUBLIC_ADMIN_KEY || '' }, }); } } export const api = new ApiClient(config.apiBaseUrl); export default api;