diff --git a/admin/index.php b/admin/index.php index 77bc9a4..1eaf831 100644 --- a/admin/index.php +++ b/admin/index.php @@ -315,6 +315,8 @@ tr:hover td{background:rgba(255,255,255,.015)} + + @@ -978,6 +980,32 @@ tr:hover td{background:rgba(255,255,255,.015)}
+ +
+
πŸ’Ύ Backup System
+ +
+
+
πŸ• Automated Schedule
+
Daily at 2:00 AM Β· 7 rolling backups Β· Files + full database export
+
+
+ + +
+
+ +
+ +
+
+
Available Backups (last 7 days)
+
+
+
Loading...
+
+
+
⏳ Pending Signups
@@ -3341,6 +3369,72 @@ async function jumpToPurchase(purchaseId) { } } +// ── BACKUP SYSTEM ──────────────────────────────────────────── +async function loadBackups() { + const list = document.getElementById('backup-list'); + const count = document.getElementById('backup-count'); + list.innerHTML = '
Loading...
'; + const d = await fetch('/api/backup.php?action=list').then(r=>r.json()); + if (!d.success) { list.innerHTML='
Failed to load backups.
'; return; } + count.textContent = d.backups.length + ' / 7'; + if (!d.backups.length) { + list.innerHTML='
No backups yet. Click Create Backup Now to make the first one.
'; + return; + } + list.innerHTML = d.backups.map((b, i) => { + const sizeMB = (b.size / 1048576).toFixed(2); + const isLatest = i === 0; + return `
+
${isLatest ? '🟒' : 'πŸ’Ώ'}
+
+
${escHtmlA(b.name)}
+
${escHtmlA(b.created)} Β· ${sizeMB} MB${isLatest?' Β· Latest':''}
+
+
+ + ⬇ Download + + +
+
`; + }).join('') + '
Oldest backup is automatically removed when a new one is created beyond the 7-backup limit.
'; +} + +async function createBackup() { + const btn = document.getElementById('backup-create-btn'); + const al = document.getElementById('backup-alert'); + btn.disabled = true; + btn.textContent = '⏳ Creating...'; + al.className = 'alert'; + showAdminAlert(al, 'Creating backup β€” this may take up to 30 seconds…', 'info'); + try { + const d = await fetch('/api/backup.php?action=create', {method:'POST'}).then(r=>r.json()); + if (d.success) { + const sizeMB = (d.size / 1048576).toFixed(2); + showAdminAlert(al, `βœ… Backup created: ${d.name} (${sizeMB} MB)`, 'success'); + loadBackups(); + loadPlatformStats(); + } else { + showAdminAlert(al, '❌ ' + (d.error || 'Backup failed'), 'error'); + } + } catch(e) { + showAdminAlert(al, '❌ Request failed β€” server may have timed out', 'error'); + } + btn.disabled = false; + btn.textContent = 'πŸ“¦ Create Backup Now'; +} + +async function deleteBackup(name) { + if (!confirm(`Delete backup "${name}"?\nThis cannot be undone.`)) return; + const d = await fetch('/api/backup.php?action=delete', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name})}).then(r=>r.json()); + if (d.success) { toast('Backup deleted', 'ok'); loadBackups(); } + else toast(d.error || 'Delete failed', 'err'); +} + // Sync hex input with color picker document.addEventListener('DOMContentLoaded', function() { const picker = document.getElementById('gf-color'); @@ -3533,6 +3627,7 @@ function showSec(name) { if (name === 'payments') loadPaymentSettings(); if (name === 'payout-settings') loadPayoutSettings(); if (name === 'cashout-methods') loadCashoutMethods(); + if (name === 'backups') loadBackups(); } async function loadChatInbox(silent) { diff --git a/api/backup.php b/api/backup.php new file mode 100644 index 0000000..f32978c --- /dev/null +++ b/api/backup.php @@ -0,0 +1,113 @@ +false,'error'=>'Forbidden']); + else { http_response_code(403); echo 'Forbidden'; } + exit; +} + +$backupDir = '/home/tomtomgames.com/backups'; +if (!is_dir($backupDir)) @mkdir($backupDir, 0750, true); + +switch ($action) { + + case 'list': + $files = glob($backupDir . '/ttg_backup_*.zip') ?: []; + rsort($files); + $backups = array_map(fn($f) => [ + 'name' => basename($f), + 'size' => filesize($f), + 'created' => date('Y-m-d H:i:s', filemtime($f)), + ], $files); + echo json_encode(['success'=>true, 'backups'=>$backups]); + break; + + case 'create': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'POST required']); exit; } + set_time_limit(300); + ignore_user_abort(true); + + $date = date('Y-m-d_H-i-s'); + $sqlFile = "/tmp/ttg_db_{$date}.sql"; + $zipFile = "{$backupDir}/ttg_backup_{$date}.zip"; + $siteDir = '/home/tomtomgames.com/public_html'; + + // Export database + $dbCmd = sprintf( + '/usr/bin/mysqldump -u %s -p%s %s > %s 2>&1', + escapeshellarg(DB_USER), escapeshellarg(DB_PASS), + escapeshellarg(DB_NAME), escapeshellarg($sqlFile) + ); + exec($dbCmd, $dbOut, $dbRc); + if ($dbRc !== 0 || !file_exists($sqlFile) || filesize($sqlFile) < 10) { + @unlink($sqlFile); + echo json_encode(['success'=>false,'error'=>'Database export failed']); exit; + } + + // Zip site files + db dump into one archive + $zipCmd = sprintf( + '/usr/bin/zip -r %s %s %s -x "*/backups/*" 2>&1', + escapeshellarg($zipFile), + escapeshellarg($siteDir), + escapeshellarg($sqlFile) + ); + exec($zipCmd, $zipOut, $zipRc); + @unlink($sqlFile); + + if ($zipRc !== 0 || !file_exists($zipFile)) { + @unlink($zipFile); + echo json_encode(['success'=>false,'error'=>'Archive creation failed']); exit; + } + + // Prune β€” keep only the 7 most recent + $all = glob($backupDir . '/ttg_backup_*.zip') ?: []; + rsort($all); + foreach (array_slice($all, 7) as $old) @unlink($old); + + echo json_encode([ + 'success' => true, + 'name' => basename($zipFile), + 'size' => filesize($zipFile), + 'created' => date('Y-m-d H:i:s'), + ]); + break; + + case 'download': + $name = basename($_GET['file'] ?? ''); + if (!preg_match('/^ttg_backup_[\d_-]+\.zip$/', $name)) { + http_response_code(400); echo 'Invalid filename'; exit; + } + $path = $backupDir . '/' . $name; + if (!file_exists($path)) { http_response_code(404); echo 'Not found'; exit; } + + header('Content-Type: application/zip'); + header('Content-Disposition: attachment; filename="' . $name . '"'); + header('Content-Length: ' . filesize($path)); + header('Cache-Control: no-cache'); + readfile($path); + exit; + + case 'delete': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'POST required']); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $name = basename($data['name'] ?? ''); + if (!preg_match('/^ttg_backup_[\d_-]+\.zip$/', $name)) { + echo json_encode(['success'=>false,'error'=>'Invalid filename']); exit; + } + $path = $backupDir . '/' . $name; + if (file_exists($path)) @unlink($path); + echo json_encode(['success'=>true]); + break; + + default: + echo json_encode(['success'=>false,'error'=>'Unknown action']); +} diff --git a/scripts/ttg-backup.sh b/scripts/ttg-backup.sh new file mode 100644 index 0000000..e0ebf31 --- /dev/null +++ b/scripts/ttg-backup.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# TomTomGames automated backup β€” runs daily at 2 AM via cron +# Cron entry: 0 2 * * * /usr/local/bin/ttg-backup.sh >> /home/tomtomgames.com/backups/backup.log 2>&1 + +BACKUP_DIR="/home/tomtomgames.com/backups" +SITE_DIR="/home/tomtomgames.com/public_html" +DB_NAME="tomt_ttg_db" +DB_USER="tomt_ttg_user" +DB_PASS='q#q+mrOcozsa7I6J' +DATE=$(date +%Y-%m-%d_%H-%M-%S) +SQL_FILE="/tmp/ttg_db_${DATE}.sql" +ZIP_FILE="${BACKUP_DIR}/ttg_backup_${DATE}.zip" + +mkdir -p "$BACKUP_DIR" +echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting backup..." + +# Export database +/usr/bin/mysqldump -u "$DB_USER" "-p${DB_PASS}" "$DB_NAME" > "$SQL_FILE" 2>&1 +if [ $? -ne 0 ] || [ ! -s "$SQL_FILE" ]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Database export failed" + rm -f "$SQL_FILE" + exit 1 +fi +echo "[$(date '+%Y-%m-%d %H:%M:%S')] Database exported ($(du -sh "$SQL_FILE" | cut -f1))" + +# Create zip archive (site files + db dump) +/usr/bin/zip -r "$ZIP_FILE" "$SITE_DIR" "$SQL_FILE" -x "*/backups/*" > /dev/null 2>&1 +RC=$? +rm -f "$SQL_FILE" + +if [ $RC -ne 0 ] || [ ! -f "$ZIP_FILE" ]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Archive creation failed" + rm -f "$ZIP_FILE" + exit 1 +fi +echo "[$(date '+%Y-%m-%d %H:%M:%S')] Archive created: $(basename "$ZIP_FILE") ($(du -sh "$ZIP_FILE" | cut -f1))" + +# Keep only the 7 most recent backups +ls -t "${BACKUP_DIR}"/ttg_backup_*.zip 2>/dev/null | tail -n +8 | while read old; do + rm -f "$old" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Pruned old backup: $(basename "$old")" +done + +echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup complete."