2026-05-21 10:41:20 +02:00
|
|
|
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';
|
2026-05-15 08:06:54 +02:00
|
|
|
|
|
|
|
|
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;
|
2026-05-29 11:02:52 +02:00
|
|
|
const BACKUP_CRON_ENABLED = process.env.BACKUP_CRON_ENABLED === 'true';
|
2026-05-15 08:06:54 +02:00
|
|
|
|
|
|
|
|
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}`,
|
2026-05-20 13:33:37 +02:00
|
|
|
{ headers: { apikey: SUPABASE_SERVICE_KEY, Authorization: `Bearer ${SUPABASE_SERVICE_KEY}` } }
|
2026-05-15 08:06:54 +02:00
|
|
|
);
|
2026-05-20 13:33:37 +02:00
|
|
|
if (!res.ok) {
|
|
|
|
|
const txt = await res.text();
|
|
|
|
|
throw new Error(`Supabase ${table} fetch failed (${res.status}): ${txt}`);
|
|
|
|
|
}
|
2026-05-15 08:06:54 +02:00
|
|
|
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) {
|
2026-05-29 11:02:52 +02:00
|
|
|
// 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',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 08:06:54 +02:00
|
|
|
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([
|
2026-05-20 13:33:37 +02:00
|
|
|
fetchSupabaseTable('products'),
|
|
|
|
|
fetchSupabaseTable('products_history'),
|
2026-05-15 08:06:54 +02:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
}
|
|
|
|
|
}
|