const SUPABASE_URL = process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL || 'https://hwithddwaapyhnfwcesj.supabase.co'; const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv'; const DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY; const DROPBOX_APP_SECRET = process.env.DROPBOX_APP_SECRET; const DROPBOX_REFRESH_TOKEN = process.env.DROPBOX_REFRESH_TOKEN; const BACKUP_SECRET = process.env.BACKUP_SECRET; const BACKUP_CRON_ENABLED = process.env.BACKUP_CRON_ENABLED === 'true'; const DROPBOX_BACKUP_FOLDER = '/CrazeBackups'; const MAX_BACKUP_AGE_DAYS = 7; async function getDropboxToken() { const res = await fetch('https://api.dropboxapi.com/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: DROPBOX_REFRESH_TOKEN, client_id: DROPBOX_APP_KEY, client_secret: DROPBOX_APP_SECRET, }), }); const data = await res.json(); if (!data.access_token) throw new Error('Dropbox token failed: ' + JSON.stringify(data)); return data.access_token; } async function fetchSupabaseTable(table) { const PAGE_SIZE = 1000; const rows = []; let offset = 0; for (let page = 0; page < 50; page++) { const res = await fetch( `${SUPABASE_URL}/rest/v1/${table}?select=*&limit=${PAGE_SIZE}&offset=${offset}`, { headers: { apikey: SUPABASE_SERVICE_KEY, Authorization: `Bearer ${SUPABASE_SERVICE_KEY}` } } ); if (!res.ok) { const txt = await res.text(); throw new Error(`Supabase ${table} fetch failed (${res.status}): ${txt}`); } const batch = await res.json(); rows.push(...batch); if (batch.length < PAGE_SIZE) break; offset += PAGE_SIZE; } return rows; } async function uploadToDropbox(token, path, content) { const res = await fetch('https://content.dropboxapi.com/2/files/upload', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/octet-stream', 'Dropbox-API-Arg': JSON.stringify({ path, mode: 'overwrite', autorename: false }), }, body: content, }); if (!res.ok) { const txt = await res.text(); throw new Error(`Dropbox upload failed (${res.status}): ${txt}`); } return res.json(); } async function deleteOldBackups(token) { const listRes = await fetch('https://api.dropboxapi.com/2/files/list_folder', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ path: DROPBOX_BACKUP_FOLDER, limit: 200 }), }); if (!listRes.ok) return; // folder may not exist yet — skip const list = await listRes.json(); const cutoff = Date.now() - MAX_BACKUP_AGE_DAYS * 24 * 60 * 60 * 1000; for (const entry of list.entries || []) { if (entry['.tag'] !== 'file') continue; const modified = new Date(entry.server_modified).getTime(); if (modified < cutoff) { await fetch('https://api.dropboxapi.com/2/files/delete_v2', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ path: entry.path_lower }), }); } } } export default async function handler(req, res) { // Backup cron disabled by default to prevent noisy scheduled failures. if (!BACKUP_CRON_ENABLED) { return res.status(200).json({ success: true, skipped: true, reason: 'backup cron disabled', }); } const authHeader = req.headers['authorization']; const isCron = !req.headers.origin; const hasSecret = BACKUP_SECRET && authHeader === `Bearer ${BACKUP_SECRET}`; if (!isCron && !hasSecret) { return res.status(403).json({ error: 'Forbidden' }); } if (req.method !== 'GET' && req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { const [syncedRows, history] = await Promise.all([ fetchSupabaseTable('products'), fetchSupabaseTable('products_history'), ]); const now = new Date(); const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19); const filename = `backup_${timestamp}.json`; const path = `${DROPBOX_BACKUP_FOLDER}/${filename}`; const payload = JSON.stringify({ created_at: now.toISOString(), synced_rows_count: syncedRows.length, history_count: history.length, synced_rows: syncedRows, history, }); const token = await getDropboxToken(); await Promise.all([ uploadToDropbox(token, path, payload), deleteOldBackups(token), ]); console.log(`[backup] OK — ${filename} | rows: ${syncedRows.length} | history: ${history.length}`); return res.json({ success: true, filename, synced_rows_count: syncedRows.length, history_count: history.length, }); } catch (err) { console.error('[backup] error:', err.message); return res.status(500).json({ success: false, error: err.message }); } }