- When admin enables maintenance, it's set to 'pending' state - Maintenance activates automatically after the current draw completes - Admin can use immediate=true to force immediate activation - Frontend shows 'Maintenance Scheduled' banner when pending - Telegram bot warns users but still allows purchases when pending - Both mode and pending status tracked in system_settings table
235 lines
6.4 KiB
TypeScript
235 lines
6.4 KiB
TypeScript
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<string, string>;
|
|
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<T = any>(path: string, options: ApiOptions = {}): Promise<T> {
|
|
const { method = 'GET', headers = {}, body, auth = false } = options;
|
|
|
|
const url = `${this.getBaseUrl()}${path}`;
|
|
|
|
const requestHeaders: Record<string, string> = {
|
|
'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;
|
|
|