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 <noreply@anthropic.com>
This commit is contained in:
@@ -171,7 +171,10 @@ INSERT INTO geofeed_settings (setting_key, setting_value) VALUES
|
|||||||
('whitelabel_company_name', ''),
|
('whitelabel_company_name', ''),
|
||||||
('whitelabel_icon_url', ''),
|
('whitelabel_icon_url', ''),
|
||||||
('whitelabel_favicon_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;
|
ON DUPLICATE KEY UPDATE setting_key = setting_key;
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|||||||
316
webapp/api.php
316
webapp/api.php
@@ -171,6 +171,26 @@ try {
|
|||||||
handleSchemaApply($db);
|
handleSchemaApply($db);
|
||||||
break;
|
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':
|
case 'error_logs':
|
||||||
handleErrorLogs($db);
|
handleErrorLogs($db);
|
||||||
break;
|
break;
|
||||||
@@ -3712,3 +3732,299 @@ function handleLicenseUsage($db) {
|
|||||||
'usage' => $usage
|
'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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -747,6 +747,86 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
<div id="schemaCheckResult" style="margin-top: 16px; display: none;"></div>
|
<div id="schemaCheckResult" style="margin-top: 16px; display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Database Backup/Restore -->
|
||||||
|
<div class="advanced-section">
|
||||||
|
<h2 class="advanced-section-title">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||||
|
<polyline points="7 10 12 15 17 10"/>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||||
|
</svg>
|
||||||
|
Database Backup & Restore
|
||||||
|
</h2>
|
||||||
|
<p class="advanced-section-desc">Export or import a full database backup as JSON (entries, settings, logos, audit log).</p>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 16px;">
|
||||||
|
<button class="btn btn-primary" onclick="downloadBackup()">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||||
|
<polyline points="7 10 12 15 17 10"/>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||||
|
</svg>
|
||||||
|
Download Backup
|
||||||
|
</button>
|
||||||
|
<input type="file" id="restoreFileInput" accept=".json" style="display: none;" onchange="handleRestoreFile(this)">
|
||||||
|
<button class="btn btn-secondary" onclick="document.getElementById('restoreFileInput').click()">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||||
|
<polyline points="17 8 12 3 7 8"/>
|
||||||
|
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||||
|
</svg>
|
||||||
|
Restore from File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="backupRestoreStatus" style="margin-top: 12px; display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WebDAV Backup -->
|
||||||
|
<div class="advanced-section">
|
||||||
|
<h2 class="advanced-section-title">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
||||||
|
</svg>
|
||||||
|
WebDAV Backup
|
||||||
|
</h2>
|
||||||
|
<p class="advanced-section-desc">Backup database to a WebDAV server (e.g., Nextcloud public share).</p>
|
||||||
|
|
||||||
|
<div class="form-grid" style="margin-top: 16px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">WebDAV Server URL</label>
|
||||||
|
<input type="url" class="form-input" id="webdavServerUrl" placeholder="https://cloud.example.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Username / Share Token</label>
|
||||||
|
<input type="text" class="form-input" id="webdavUsername" placeholder="SoXpcAG5WPdBTKi">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Password (leave empty if none)</label>
|
||||||
|
<input type="password" class="form-input" id="webdavPassword" placeholder="Optional password" style="max-width: 300px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 16px;">
|
||||||
|
<button class="btn btn-primary" onclick="backupToWebDAV()">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||||
|
<polyline points="17 8 12 3 7 8"/>
|
||||||
|
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||||
|
</svg>
|
||||||
|
Backup to WebDAV
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="saveWebDAVSettings()">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
||||||
|
<polyline points="17 21 17 13 7 13 7 21"/>
|
||||||
|
<polyline points="7 3 7 8 15 8"/>
|
||||||
|
</svg>
|
||||||
|
Save Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="webdavBackupStatus" style="margin-top: 12px; display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- System Info -->
|
<!-- System Info -->
|
||||||
<div class="advanced-section">
|
<div class="advanced-section">
|
||||||
<h2 class="advanced-section-title">System Information</h2>
|
<h2 class="advanced-section-title">System Information</h2>
|
||||||
@@ -1364,6 +1444,185 @@ async function applySchemaUpdates() {
|
|||||||
applyBtn.disabled = false;
|
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 = \'<div class="loading"><div class="spinner"></div></div>\';
|
||||||
|
|
||||||
|
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 = \'<div style="padding: 12px; background: var(--success-bg); border: 1px solid var(--success); border-radius: var(--radius-md);"><strong style="color: var(--success);">Restore completed successfully!</strong></div>\';
|
||||||
|
showNotification("Database restored successfully", "success");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Restore error:", error);
|
||||||
|
statusDiv.innerHTML = `<div style="padding: 12px; background: var(--error-bg); border: 1px solid var(--error); border-radius: var(--radius-md);"><strong style="color: var(--error);">Restore failed:</strong> ${escapeHtml(error.message)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = \'<div class="loading"><div class="spinner"></div></div>\';
|
||||||
|
|
||||||
|
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 = `<div style="padding: 12px; background: var(--success-bg); border: 1px solid var(--success); border-radius: var(--radius-md);"><strong style="color: var(--success);">Backup uploaded successfully!</strong><br><small>Filename: ${escapeHtml(data.filename)}</small></div>`;
|
||||||
|
showNotification("Backup uploaded to WebDAV", "success");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("WebDAV backup error:", error);
|
||||||
|
statusDiv.innerHTML = `<div style="padding: 12px; background: var(--error-bg); border: 1px solid var(--error); border-radius: var(--radius-md);"><strong style="color: var(--error);">WebDAV backup failed:</strong> ${escapeHtml(error.message)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load WebDAV settings when on developer tab
|
||||||
|
if (document.getElementById("webdavServerUrl")) {
|
||||||
|
loadWebDAVSettings();
|
||||||
|
}
|
||||||
</script>';
|
</script>';
|
||||||
|
|
||||||
// Include footer
|
// Include footer
|
||||||
|
|||||||
Reference in New Issue
Block a user