const API_URL = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:3001' export interface AuthUser { id: string email: string name: string avatarUrl: string | null } export interface AuthTokens { accessToken: string refreshToken: string expiresIn: number user: AuthUser } const ACCESS_TOKEN_KEY = 'timelapse_access_token' const REFRESH_TOKEN_KEY = 'timelapse_refresh_token' const USER_KEY = 'timelapse_user' // ── Token storage ──────────────────────────────────────────────── export function getAccessToken(): string | null { if (typeof window === 'undefined') return null return localStorage.getItem(ACCESS_TOKEN_KEY) } export function getRefreshToken(): string | null { if (typeof window === 'undefined') return null return localStorage.getItem(REFRESH_TOKEN_KEY) } export function getStoredUser(): AuthUser | null { if (typeof window === 'undefined') return null const raw = localStorage.getItem(USER_KEY) if (!raw) return null try { return JSON.parse(raw) } catch { return null } } export function storeTokens(tokens: AuthTokens): void { localStorage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken) localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken) localStorage.setItem(USER_KEY, JSON.stringify(tokens.user)) } export function clearAuth(): void { localStorage.removeItem(ACCESS_TOKEN_KEY) localStorage.removeItem(REFRESH_TOKEN_KEY) localStorage.removeItem(USER_KEY) } // ── API helpers ────────────────────────────────────────────────── async function apiFetch(path: string, options: RequestInit = {}): Promise { const token = getAccessToken() const headers: Record = { 'Content-Type': 'application/json', ...(options.headers as Record), } if (token) headers['Authorization'] = `Bearer ${token}` const res = await fetch(`${API_URL}${path}`, { ...options, headers }) if (res.status === 401) { // Try refresh const refreshed = await tryRefresh() if (refreshed) { const retryHeaders = { ...headers, Authorization: `Bearer ${getAccessToken()}` } const retry = await fetch(`${API_URL}${path}`, { ...options, headers: retryHeaders }) if (!retry.ok) throw new Error(`API error ${retry.status}`) return retry.json() as Promise } clearAuth() if (typeof window !== 'undefined') window.location.href = '/login' throw new Error('Unauthorized') } if (!res.ok) { const body = await res.json().catch(() => ({ message: 'Unknown error' })) throw new Error(body.message || `API error ${res.status}`) } return res.json() as Promise } export async function tryRefresh(): Promise { const refreshToken = getRefreshToken() if (!refreshToken) return false try { const res = await fetch(`${API_URL}/v1/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken }), }) if (!res.ok) return false const tokens: AuthTokens = await res.json() storeTokens(tokens) return true } catch { return false } } // ── Auth API calls ────────────────────────────────────────────── export const authApi = { login: (email: string, password: string) => apiFetch('/v1/auth/login', { method: 'POST', body: JSON.stringify({ email, password }), }), register: (email: string, password: string, name: string) => apiFetch('/v1/auth/register', { method: 'POST', body: JSON.stringify({ email, password, name }), }), logout: () => apiFetch('/v1/auth/logout', { method: 'POST', body: JSON.stringify({ refreshToken: getRefreshToken() }), }).finally(() => clearAuth()), me: () => apiFetch('/v1/auth/me'), } // ── Devices API (JWT-authenticated) ───────────────────────────── export const devicesApi = { list: (projectId?: string) => { const qs = projectId ? `?projectId=${projectId}` : '' return apiFetch(`/v1/devices${qs}`) }, get: (id: string) => apiFetch(`/v1/devices/${id}`), stats: () => apiFetch<{ devicesOnline: number; devicesOffline: number; capturesToday: number; devicesTotal: number }>('/v1/devices/stats'), heartbeats: (id: string, limit?: number) => { const qs = limit ? `?limit=${limit}` : '' return apiFetch(`/v1/devices/${id}/heartbeats${qs}`) }, }