From c67d7ff139102d8775354a85ae59d86bf23b7ccd Mon Sep 17 00:00:00 2001 From: Purple Date: Sun, 18 Jan 2026 13:42:13 +0000 Subject: [PATCH] Add database backup/restore and WebDAV backup features Developer tab now includes: - Database Backup & Restore section with download/upload JSON backup - WebDAV Backup section to upload backups to Nextcloud/WebDAV servers with configurable server URL, username, and password Features: - Full JSON export of entries, settings, logos, users, license - Import with data replacement and transaction safety - WebDAV upload via cURL with PUT request - Automatic timestamped filenames (ipmanager-YYYY-MM-DD_HH-mm-ss.json) - Settings persistence for WebDAV credentials Co-Authored-By: Claude Opus 4.5 --- database/schema.sql | 5 +- webapp/api.php | 316 ++++++++++++++++++++++++++++++++++++++++++++ webapp/settings.php | 259 ++++++++++++++++++++++++++++++++++++ 3 files changed, 579 insertions(+), 1 deletion(-) diff --git a/database/schema.sql b/database/schema.sql index aa7f654..bfddf84 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -171,7 +171,10 @@ INSERT INTO geofeed_settings (setting_key, setting_value) VALUES ('whitelabel_company_name', ''), ('whitelabel_icon_url', ''), ('whitelabel_favicon_url', ''), -('whitelabel_default_import_url', '') +('whitelabel_default_import_url', ''), +('webdav_server_url', ''), +('webdav_username', ''), +('webdav_password', '') ON DUPLICATE KEY UPDATE setting_key = setting_key; -- ============================================ diff --git a/webapp/api.php b/webapp/api.php index f0f6a94..5c2effd 100644 --- a/webapp/api.php +++ b/webapp/api.php @@ -171,6 +171,26 @@ try { handleSchemaApply($db); break; + case 'backup_export': + handleBackupExport($db); + break; + + case 'backup_import': + handleBackupImport($db); + break; + + case 'webdav_settings_get': + handleWebDAVSettingsGet($db); + break; + + case 'webdav_settings_save': + handleWebDAVSettingsSave($db); + break; + + case 'webdav_backup': + handleWebDAVBackup($db); + break; + case 'error_logs': handleErrorLogs($db); break; @@ -3712,3 +3732,299 @@ function handleLicenseUsage($db) { 'usage' => $usage ]); } + +/** + * Export full database backup as JSON + */ +function handleBackupExport($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'GET') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + try { + $backup = [ + 'version' => APP_VERSION, + 'exported_at' => date('Y-m-d H:i:s'), + 'entries' => [], + 'settings' => [], + 'logos' => [], + 'users' => [], + 'license' => null + ]; + + // Export geofeed entries + $stmt = $db->query("SELECT * FROM geofeed_entries ORDER BY id"); + $backup['entries'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Export settings + $stmt = $db->query("SELECT * FROM geofeed_settings"); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $backup['settings'][$row['setting_key']] = $row['setting_value']; + } + + // Export logos + $stmt = $db->query("SELECT * FROM client_logos ORDER BY id"); + $backup['logos'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Export admin users + $stmt = $db->query("SELECT id, email, role, display_name, active, created_at FROM admin_users ORDER BY id"); + $backup['users'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Export license info + $stmt = $db->query("SELECT * FROM license_info WHERE is_active = 1 ORDER BY id DESC LIMIT 1"); + $backup['license'] = $stmt->fetch(PDO::FETCH_ASSOC); + + jsonResponse([ + 'success' => true, + 'backup' => $backup + ]); + + } catch (Exception $e) { + jsonResponse(['error' => 'Backup export failed: ' . $e->getMessage()], 500); + } +} + +/** + * Import database from JSON backup + */ +function handleBackupImport($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + $input = json_decode(file_get_contents('php://input'), true); + + if (!validateCSRFToken($input['csrf_token'] ?? '')) { + jsonResponse(['error' => 'Invalid CSRF token'], 403); + } + + if (empty($input['backup_data'])) { + jsonResponse(['error' => 'No backup data provided'], 400); + } + + $backup = $input['backup_data']; + + try { + $db->beginTransaction(); + + // Clear existing data + $db->exec("DELETE FROM geofeed_entries"); + $db->exec("DELETE FROM client_logos"); + + // Import entries + if (!empty($backup['entries'])) { + $columns = ['ip_prefix', 'country_code', 'region_code', 'city', 'postal_code', 'client_short_name', 'notes', 'sort_order']; + foreach ($backup['entries'] as $entry) { + $stmt = $db->prepare("INSERT INTO geofeed_entries (ip_prefix, country_code, region_code, city, postal_code, client_short_name, notes, sort_order) VALUES (:ip_prefix, :country_code, :region_code, :city, :postal_code, :client_short_name, :notes, :sort_order)"); + $stmt->execute([ + ':ip_prefix' => $entry['ip_prefix'], + ':country_code' => $entry['country_code'] ?? null, + ':region_code' => $entry['region_code'] ?? null, + ':city' => $entry['city'] ?? null, + ':postal_code' => $entry['postal_code'] ?? null, + ':client_short_name' => $entry['client_short_name'] ?? null, + ':notes' => $entry['notes'] ?? null, + ':sort_order' => $entry['sort_order'] ?? 0 + ]); + } + } + + // Import settings (skip sensitive ones) + $skipSettings = ['aws_secret_access_key', 'ipregistry_api_key', 'webdav_password']; + if (!empty($backup['settings'])) { + foreach ($backup['settings'] as $key => $value) { + if (!in_array($key, $skipSettings) && $value !== null) { + saveSetting($db, $key, $value); + } + } + } + + // Import logos + if (!empty($backup['logos'])) { + foreach ($backup['logos'] as $logo) { + $stmt = $db->prepare("INSERT INTO client_logos (short_name, logo_url) VALUES (:short_name, :logo_url) ON DUPLICATE KEY UPDATE logo_url = VALUES(logo_url)"); + $stmt->execute([ + ':short_name' => $logo['short_name'], + ':logo_url' => $logo['logo_url'] + ]); + } + } + + $db->commit(); + + // Log the import + logAudit($db, null, 'backup_import', null, [ + 'entries_imported' => count($backup['entries'] ?? []), + 'logos_imported' => count($backup['logos'] ?? []) + ]); + + jsonResponse([ + 'success' => true, + 'message' => 'Backup imported successfully', + 'entries_imported' => count($backup['entries'] ?? []) + ]); + + } catch (Exception $e) { + $db->rollBack(); + jsonResponse(['error' => 'Backup import failed: ' . $e->getMessage()], 500); + } +} + +/** + * Get WebDAV settings + */ +function handleWebDAVSettingsGet($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'GET') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + jsonResponse([ + 'success' => true, + 'settings' => [ + 'webdav_server_url' => getSetting($db, 'webdav_server_url', ''), + 'webdav_username' => getSetting($db, 'webdav_username', '') + // Password not returned for security + ] + ]); +} + +/** + * Save WebDAV settings + */ +function handleWebDAVSettingsSave($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + $input = json_decode(file_get_contents('php://input'), true); + + if (!validateCSRFToken($input['csrf_token'] ?? '')) { + jsonResponse(['error' => 'Invalid CSRF token'], 403); + } + + saveSetting($db, 'webdav_server_url', trim($input['webdav_server_url'] ?? '')); + saveSetting($db, 'webdav_username', trim($input['webdav_username'] ?? '')); + + // Only update password if provided + if (!empty($input['webdav_password'])) { + saveSetting($db, 'webdav_password', $input['webdav_password']); + } + + jsonResponse(['success' => true, 'message' => 'WebDAV settings saved']); +} + +/** + * Backup to WebDAV server + */ +function handleWebDAVBackup($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + $input = json_decode(file_get_contents('php://input'), true); + + if (!validateCSRFToken($input['csrf_token'] ?? '')) { + jsonResponse(['error' => 'Invalid CSRF token'], 403); + } + + $serverUrl = trim($input['webdav_server_url'] ?? ''); + $username = trim($input['webdav_username'] ?? ''); + $password = $input['webdav_password'] ?? ''; + + // Use saved password if not provided + if (empty($password)) { + $password = getSetting($db, 'webdav_password', ''); + } + + if (empty($serverUrl) || empty($username)) { + jsonResponse(['error' => 'WebDAV server URL and username are required'], 400); + } + + try { + // Generate backup data + $backup = [ + 'version' => APP_VERSION, + 'exported_at' => date('Y-m-d H:i:s'), + 'entries' => [], + 'settings' => [], + 'logos' => [], + 'users' => [], + 'license' => null + ]; + + // Export geofeed entries + $stmt = $db->query("SELECT * FROM geofeed_entries ORDER BY id"); + $backup['entries'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Export settings (skip sensitive) + $skipSettings = ['aws_secret_access_key', 'ipregistry_api_key', 'webdav_password']; + $stmt = $db->query("SELECT * FROM geofeed_settings"); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + if (!in_array($row['setting_key'], $skipSettings)) { + $backup['settings'][$row['setting_key']] = $row['setting_value']; + } + } + + // Export logos + $stmt = $db->query("SELECT * FROM client_logos ORDER BY id"); + $backup['logos'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Export admin users + $stmt = $db->query("SELECT id, email, role, display_name, active, created_at FROM admin_users ORDER BY id"); + $backup['users'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $jsonData = json_encode($backup, JSON_PRETTY_PRINT); + + // Generate filename with timestamp + $timestamp = date('Y-m-d_H-i-s'); + $filename = "ipmanager-{$timestamp}.json"; + + // Build WebDAV URL + $webdavUrl = rtrim($serverUrl, '/') . '/public.php/webdav/' . $filename; + + // Upload via cURL + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $webdavUrl, + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_POSTFIELDS => $jsonData, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'X-Requested-With: XMLHttpRequest' + ], + CURLOPT_USERPWD => $username . ':' . $password, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_TIMEOUT => 60 + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error) { + jsonResponse(['error' => 'WebDAV upload failed: ' . $error], 500); + } + + if ($httpCode >= 200 && $httpCode < 300) { + // Log the backup + logAudit($db, null, 'webdav_backup', null, [ + 'filename' => $filename, + 'server' => $serverUrl, + 'entries' => count($backup['entries']) + ]); + + jsonResponse([ + 'success' => true, + 'message' => 'Backup uploaded successfully', + 'filename' => $filename + ]); + } else { + jsonResponse(['error' => "WebDAV upload failed with HTTP $httpCode"], 500); + } + + } catch (Exception $e) { + jsonResponse(['error' => 'WebDAV backup failed: ' . $e->getMessage()], 500); + } +} diff --git a/webapp/settings.php b/webapp/settings.php index d564d72..cc3ca84 100644 --- a/webapp/settings.php +++ b/webapp/settings.php @@ -747,6 +747,86 @@ require_once __DIR__ . '/includes/header.php'; + +
+

+ + + + + + Database Backup & Restore +

+

Export or import a full database backup as JSON (entries, settings, logos, audit log).

+ +
+ + + +
+ +
+ + +
+

+ + + + WebDAV Backup +

+

Backup database to a WebDAV server (e.g., Nextcloud public share).

+ +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ + +
+ +
+

System Information

@@ -1364,6 +1444,185 @@ async function applySchemaUpdates() { applyBtn.disabled = false; } } + +// Database Backup/Restore Functions +async function downloadBackup() { + try { + showNotification("Generating backup...", "info"); + const response = await fetch("api.php?action=backup_export", { + headers: { "X-CSRF-Token": CSRF_TOKEN }, + credentials: "same-origin" + }); + + if (!response.ok) { + throw new Error("Failed to generate backup"); + } + + const data = await response.json(); + if (!data.success) { + throw new Error(data.error || "Backup failed"); + } + + // Create download + const blob = new Blob([JSON.stringify(data.backup, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + a.href = url; + a.download = `ipmanager-backup-${timestamp}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + showNotification("Backup downloaded successfully", "success"); + } catch (error) { + console.error("Backup error:", error); + showNotification("Backup failed: " + error.message, "error"); + } +} + +async function handleRestoreFile(input) { + const file = input.files[0]; + if (!file) return; + + if (!confirm("WARNING: This will replace all existing data with the backup. Are you sure you want to continue?")) { + input.value = ""; + return; + } + + const statusDiv = document.getElementById("backupRestoreStatus"); + statusDiv.style.display = "block"; + statusDiv.innerHTML = \'
\'; + + try { + const text = await file.text(); + const backupData = JSON.parse(text); + + const response = await fetch("api.php?action=backup_import", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": CSRF_TOKEN + }, + credentials: "same-origin", + body: JSON.stringify({ backup_data: backupData, csrf_token: CSRF_TOKEN }) + }); + + const data = await response.json(); + if (!data.success) { + throw new Error(data.error || "Restore failed"); + } + + statusDiv.innerHTML = \'
Restore completed successfully!
\'; + showNotification("Database restored successfully", "success"); + } catch (error) { + console.error("Restore error:", error); + statusDiv.innerHTML = `
Restore failed: ${escapeHtml(error.message)}
`; + } + + input.value = ""; +} + +// WebDAV Backup Functions +async function loadWebDAVSettings() { + try { + const response = await fetch("api.php?action=webdav_settings_get", { + headers: { "X-CSRF-Token": CSRF_TOKEN }, + credentials: "same-origin" + }); + const data = await response.json(); + + if (data.success) { + document.getElementById("webdavServerUrl").value = data.settings.webdav_server_url || ""; + document.getElementById("webdavUsername").value = data.settings.webdav_username || ""; + // Password is not returned for security + } + } catch (error) { + console.error("Failed to load WebDAV settings:", error); + } +} + +async function saveWebDAVSettings() { + const serverUrl = document.getElementById("webdavServerUrl").value.trim(); + const username = document.getElementById("webdavUsername").value.trim(); + const password = document.getElementById("webdavPassword").value; + + try { + const response = await fetch("api.php?action=webdav_settings_save", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": CSRF_TOKEN + }, + credentials: "same-origin", + body: JSON.stringify({ + webdav_server_url: serverUrl, + webdav_username: username, + webdav_password: password, + csrf_token: CSRF_TOKEN + }) + }); + + const data = await response.json(); + if (!data.success) { + throw new Error(data.error || "Failed to save settings"); + } + + showNotification("WebDAV settings saved", "success"); + } catch (error) { + console.error("Save WebDAV settings error:", error); + showNotification("Failed to save settings: " + error.message, "error"); + } +} + +async function backupToWebDAV() { + const serverUrl = document.getElementById("webdavServerUrl").value.trim(); + const username = document.getElementById("webdavUsername").value.trim(); + const password = document.getElementById("webdavPassword").value; + + if (!serverUrl || !username) { + showNotification("Please enter WebDAV server URL and username", "error"); + return; + } + + const statusDiv = document.getElementById("webdavBackupStatus"); + statusDiv.style.display = "block"; + statusDiv.innerHTML = \'
\'; + + try { + const response = await fetch("api.php?action=webdav_backup", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": CSRF_TOKEN + }, + credentials: "same-origin", + body: JSON.stringify({ + webdav_server_url: serverUrl, + webdav_username: username, + webdav_password: password, + csrf_token: CSRF_TOKEN + }) + }); + + const data = await response.json(); + if (!data.success) { + throw new Error(data.error || "WebDAV backup failed"); + } + + statusDiv.innerHTML = `
Backup uploaded successfully!
Filename: ${escapeHtml(data.filename)}
`; + showNotification("Backup uploaded to WebDAV", "success"); + } catch (error) { + console.error("WebDAV backup error:", error); + statusDiv.innerHTML = `
WebDAV backup failed: ${escapeHtml(error.message)}
`; + } +} + +// Load WebDAV settings when on developer tab +if (document.getElementById("webdavServerUrl")) { + loadWebDAVSettings(); +} '; // Include footer