626 lines
28 KiB
JavaScript
626 lines
28 KiB
JavaScript
let currentTab = 'clients';
|
|
|
|
// Check authentication on load
|
|
async function checkAuth() {
|
|
try {
|
|
const res = await fetch('/admin/check');
|
|
const data = await res.json();
|
|
if (!data.authenticated) {
|
|
window.location.href = '/admin/';
|
|
}
|
|
} catch (e) {
|
|
console.error('Auth check failed:', e);
|
|
}
|
|
}
|
|
|
|
// Logout
|
|
async function logout() {
|
|
await fetch('/admin/logout', { method: 'POST' });
|
|
location.reload();
|
|
}
|
|
|
|
// Toggle dark/light theme
|
|
function toggleTheme() {
|
|
const html = document.documentElement;
|
|
const isDark = html.classList.contains('dark');
|
|
|
|
if (isDark) {
|
|
html.classList.remove('dark');
|
|
localStorage.setItem('theme', 'light');
|
|
} else {
|
|
html.classList.add('dark');
|
|
localStorage.setItem('theme', 'dark');
|
|
}
|
|
}
|
|
|
|
// Initialize theme on load
|
|
function initTheme() {
|
|
const savedTheme = localStorage.getItem('theme');
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
|
|
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
|
|
document.documentElement.classList.add('dark');
|
|
}
|
|
}
|
|
|
|
// Load stats
|
|
async function loadStats() {
|
|
try {
|
|
const res = await fetch('/admin/stats');
|
|
const data = await res.json();
|
|
document.getElementById('stats-grid').innerHTML =
|
|
'<div class="group relative bg-white dark:bg-slate-900/80 backdrop-blur-xl rounded-2xl p-6 border border-slate-200 dark:border-slate-700/50 hover:border-violet-400 dark:hover:border-violet-500/50 transition-all duration-300 overflow-hidden shadow-sm dark:shadow-none">' +
|
|
'<div class="absolute inset-0 bg-gradient-to-br from-violet-500/5 via-purple-500/5 to-transparent dark:from-violet-600/10 dark:via-purple-600/5"></div>' +
|
|
'<div class="relative">' +
|
|
'<div class="flex items-center justify-between mb-4">' +
|
|
'<span class="text-slate-500 dark:text-slate-400 text-sm font-medium">Clients</span>' +
|
|
'<div class="w-10 h-10 rounded-xl bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center">' +
|
|
'<i class="fas fa-users text-violet-600 dark:text-violet-400"></i>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="text-3xl font-bold text-slate-900 dark:text-white">' + data.client_count + '</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="group relative bg-white dark:bg-slate-900/80 backdrop-blur-xl rounded-2xl p-6 border border-slate-200 dark:border-slate-700/50 hover:border-violet-400 dark:hover:border-violet-500/50 transition-all duration-300 overflow-hidden shadow-sm dark:shadow-none">' +
|
|
'<div class="absolute inset-0 bg-gradient-to-br from-violet-500/5 via-purple-500/5 to-transparent dark:from-violet-600/10 dark:via-purple-600/5"></div>' +
|
|
'<div class="relative">' +
|
|
'<div class="flex items-center justify-between mb-4">' +
|
|
'<span class="text-slate-500 dark:text-slate-400 text-sm font-medium">Total Snapshots</span>' +
|
|
'<div class="w-10 h-10 rounded-xl bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center">' +
|
|
'<i class="fas fa-camera text-violet-600 dark:text-violet-400"></i>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="text-3xl font-bold text-slate-900 dark:text-white">' + data.total_snapshots + '</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="group relative bg-white dark:bg-slate-900/80 backdrop-blur-xl rounded-2xl p-6 border border-slate-200 dark:border-slate-700/50 hover:border-violet-400 dark:hover:border-violet-500/50 transition-all duration-300 overflow-hidden shadow-sm dark:shadow-none">' +
|
|
'<div class="absolute inset-0 bg-gradient-to-br from-violet-500/5 via-purple-500/5 to-transparent dark:from-violet-600/10 dark:via-purple-600/5"></div>' +
|
|
'<div class="relative">' +
|
|
'<div class="flex items-center justify-between mb-4">' +
|
|
'<span class="text-slate-500 dark:text-slate-400 text-sm font-medium">Total Storage</span>' +
|
|
'<div class="w-10 h-10 rounded-xl bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center">' +
|
|
'<i class="fas fa-hard-drive text-violet-600 dark:text-violet-400"></i>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="text-3xl font-bold text-slate-900 dark:text-white">' + data.total_storage_gb.toFixed(2) + ' GB</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
} catch (e) {
|
|
console.error('Failed to load stats:', e);
|
|
}
|
|
}
|
|
|
|
// Load clients
|
|
async function loadClients() {
|
|
try {
|
|
const res = await fetch('/admin/clients');
|
|
const clients = await res.json();
|
|
|
|
const tbody = document.getElementById('clients-table');
|
|
tbody.innerHTML = clients.map(c => {
|
|
const usedPercent = c.max_size_bytes > 0 ? (c.current_usage / c.max_size_bytes * 100).toFixed(1) : 0;
|
|
const usedGB = (c.current_usage / (1024*1024*1024)).toFixed(2);
|
|
const maxGB = (c.max_size_bytes / (1024*1024*1024)).toFixed(0);
|
|
return '<tr class="border-b border-slate-100 dark:border-slate-700/30 hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors">' +
|
|
'<td class="py-4 px-4 font-semibold text-slate-900 dark:text-white">' + c.client_id + '</td>' +
|
|
'<td class="py-4 px-4"><span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400 border border-blue-200 dark:border-blue-500/30">' + c.storage_type + '</span></td>' +
|
|
'<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + maxGB + ' GB</td>' +
|
|
'<td class="py-4 px-4">' +
|
|
'<div class="text-slate-600 dark:text-slate-300">' + usedGB + ' GB (' + usedPercent + '%)</div>' +
|
|
'<div class="w-24 h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden mt-2"><div class="h-full bg-gradient-to-r from-violet-500 to-fuchsia-500 transition-all rounded-full" style="width: ' + Math.min(usedPercent, 100) + '%"></div></div>' +
|
|
'</td>' +
|
|
'<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + c.snapshot_count + '</td>' +
|
|
'<td class="py-4 px-4">' + (c.enabled ? '<span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-500/30">Enabled</span>' : '<span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-400 border border-red-200 dark:border-red-500/30">Disabled</span>') + '</td>' +
|
|
'<td class="py-4 px-4 whitespace-nowrap">' +
|
|
'<button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-400 hover:to-orange-400 text-white mr-1.5 shadow-lg shadow-amber-500/20 transition-all" data-action="edit-client" data-client-id="' + c.client_id + '"><i class="fas fa-edit mr-1"></i>Edit</button>' +
|
|
'<button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-purple-500 to-violet-500 hover:from-purple-400 hover:to-violet-400 text-white mr-1.5 shadow-lg shadow-purple-500/20 transition-all" data-action="set-client-key" data-client-id="' + c.client_id + '"><i class="fas fa-key mr-1"></i>Set Key</button>' +
|
|
'<button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-400 hover:to-amber-400 text-white mr-1.5 shadow-lg shadow-orange-500/20 transition-all" data-action="reset-client-key" data-client-id="' + c.client_id + '"><i class="fas fa-rotate mr-1"></i>Reset</button>' +
|
|
'<button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-red-500 to-rose-500 hover:from-red-400 hover:to-rose-400 text-white shadow-lg shadow-red-500/20 transition-all" data-action="delete-client" data-client-id="' + c.client_id + '"><i class="fas fa-trash mr-1"></i>Delete</button>' +
|
|
'</td>' +
|
|
'</tr>';
|
|
}).join('');
|
|
|
|
// Update snapshot filter
|
|
const filter = document.getElementById('snapshot-client-filter');
|
|
filter.innerHTML = '<option value="">All Clients</option>' +
|
|
clients.map(c => '<option value="' + c.client_id + '">' + c.client_id + '</option>').join('');
|
|
} catch (e) {
|
|
console.error('Failed to load clients:', e);
|
|
}
|
|
}
|
|
|
|
// Load snapshots
|
|
async function loadSnapshots() {
|
|
const clientId = document.getElementById('snapshot-client-filter').value;
|
|
const url = '/admin/snapshots' + (clientId ? '?client_id=' + clientId : '');
|
|
|
|
try {
|
|
const res = await fetch(url);
|
|
const snapshots = await res.json();
|
|
|
|
const tbody = document.getElementById('snapshots-table');
|
|
tbody.innerHTML = snapshots.map(s => {
|
|
const sizeGB = (s.size_bytes / (1024*1024*1024)).toFixed(2);
|
|
return '<tr class="border-b border-slate-100 dark:border-slate-700/30 hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors">' +
|
|
'<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + s.client_id + '</td>' +
|
|
'<td class="py-4 px-4 font-semibold text-slate-900 dark:text-white">' + s.snapshot_id + '</td>' +
|
|
'<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + new Date(s.timestamp).toLocaleString() + '</td>' +
|
|
'<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + sizeGB + ' GB</td>' +
|
|
'<td class="py-4 px-4">' +
|
|
(s.incremental ? '<span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400 border border-blue-200 dark:border-blue-500/30">Incremental</span>' : '<span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-500/30">Full</span>') +
|
|
(s.compressed ? ' <span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-purple-100 text-purple-700 dark:bg-purple-500/20 dark:text-purple-400 border border-purple-200 dark:border-purple-500/30">LZ4</span>' : '') +
|
|
'</td>' +
|
|
'<td class="py-4 px-4"><button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-red-500 to-rose-500 hover:from-red-400 hover:to-rose-400 text-white shadow-lg shadow-red-500/20 transition-all" data-action="delete-snapshot" data-client-id="' + s.client_id + '" data-snapshot-id="' + s.snapshot_id + '"><i class="fas fa-trash mr-1"></i>Delete</button></td>' +
|
|
'</tr>';
|
|
}).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load snapshots:', e);
|
|
}
|
|
}
|
|
|
|
// Load admins
|
|
async function loadAdmins() {
|
|
try {
|
|
const res = await fetch('/admin/admins');
|
|
const admins = await res.json();
|
|
|
|
const tbody = document.getElementById('admins-table');
|
|
tbody.innerHTML = admins.map(a =>
|
|
'<tr class="border-b border-slate-100 dark:border-slate-700/30 hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors">' +
|
|
'<td class="py-4 px-4 text-slate-400 dark:text-slate-500">' + a.id + '</td>' +
|
|
'<td class="py-4 px-4 font-semibold text-slate-900 dark:text-white">' + a.username + '</td>' +
|
|
'<td class="py-4 px-4"><span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400 border border-blue-200 dark:border-blue-500/30">' + a.role + '</span></td>' +
|
|
'<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + new Date(a.created_at).toLocaleDateString() + '</td>' +
|
|
'<td class="py-4 px-4 whitespace-nowrap">' +
|
|
'<button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-400 hover:to-orange-400 text-white mr-1.5 shadow-lg shadow-amber-500/20 transition-all" data-action="change-admin-password" data-admin-id="' + a.id + '" data-admin-username="' + a.username + '"><i class="fas fa-key mr-1"></i>Change Password</button>' +
|
|
'<button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-red-500 to-rose-500 hover:from-red-400 hover:to-rose-400 text-white shadow-lg shadow-red-500/20 transition-all" data-action="delete-admin" data-admin-id="' + a.id + '"><i class="fas fa-trash mr-1"></i>Delete</button>' +
|
|
'</td>' +
|
|
'</tr>'
|
|
).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load admins:', e);
|
|
}
|
|
}
|
|
|
|
// Tab switching
|
|
function showTab(tab) {
|
|
currentTab = tab;
|
|
document.querySelectorAll('[data-tab]').forEach(t => {
|
|
t.classList.remove('bg-gradient-to-r', 'from-violet-600', 'to-fuchsia-600', 'text-white', 'shadow-lg', 'shadow-violet-500/25');
|
|
t.classList.add('bg-slate-100', 'dark:bg-slate-800', 'text-slate-600', 'dark:text-slate-400', 'hover:bg-slate-200', 'dark:hover:bg-slate-700', 'hover:text-slate-900', 'dark:hover:text-white', 'border', 'border-slate-200', 'dark:border-slate-700');
|
|
});
|
|
const activeTab = document.querySelector('[data-tab="' + tab + '"]');
|
|
activeTab.classList.remove('bg-slate-100', 'dark:bg-slate-800', 'text-slate-600', 'dark:text-slate-400', 'hover:bg-slate-200', 'dark:hover:bg-slate-700', 'hover:text-slate-900', 'dark:hover:text-white', 'border', 'border-slate-200', 'dark:border-slate-700');
|
|
activeTab.classList.add('bg-gradient-to-r', 'from-violet-600', 'to-fuchsia-600', 'text-white', 'shadow-lg', 'shadow-violet-500/25');
|
|
|
|
document.getElementById('clients-tab').classList.add('hidden');
|
|
document.getElementById('snapshots-tab').classList.add('hidden');
|
|
document.getElementById('admins-tab').classList.add('hidden');
|
|
document.getElementById(tab + '-tab').classList.remove('hidden');
|
|
|
|
if (tab === 'snapshots') loadSnapshots();
|
|
if (tab === 'admins') loadAdmins();
|
|
}
|
|
|
|
// Modal functions
|
|
function showModal(id) {
|
|
const modal = document.getElementById(id);
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
}
|
|
|
|
function closeModal(id) {
|
|
const modal = document.getElementById(id);
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}
|
|
|
|
// Edit client
|
|
async function editClient(clientId) {
|
|
try {
|
|
const res = await fetch('/admin/client?client_id=' + clientId);
|
|
const data = await res.json();
|
|
const c = data.client;
|
|
|
|
document.getElementById('edit-client-id').value = c.client_id;
|
|
document.getElementById('edit-client-apikey').value = '';
|
|
document.getElementById('edit-client-storage').value = c.storage_type;
|
|
document.getElementById('edit-client-dataset').value = c.dataset;
|
|
document.getElementById('edit-client-quota').value = Math.round(c.max_size_bytes / (1024*1024*1024));
|
|
document.getElementById('edit-client-enabled').checked = c.enabled;
|
|
|
|
if (c.rotation_policy) {
|
|
document.getElementById('edit-client-hourly').value = c.rotation_policy.keep_hourly || 0;
|
|
document.getElementById('edit-client-daily').value = c.rotation_policy.keep_daily || 0;
|
|
document.getElementById('edit-client-weekly').value = c.rotation_policy.keep_weekly || 0;
|
|
document.getElementById('edit-client-monthly').value = c.rotation_policy.keep_monthly || 0;
|
|
}
|
|
|
|
showModal('edit-client-modal');
|
|
} catch (e) {
|
|
alert('Failed to load client data');
|
|
}
|
|
}
|
|
|
|
// Delete client
|
|
async function deleteClient(clientId) {
|
|
if (!confirm('Are you sure you want to delete client "' + clientId + '"? This will also delete all snapshots.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('/admin/client/delete', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ client_id: clientId })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
loadClients();
|
|
loadStats();
|
|
} else {
|
|
alert(data.message || 'Failed to delete client');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to delete client');
|
|
}
|
|
}
|
|
|
|
// Delete snapshot
|
|
async function deleteSnapshot(clientId, snapshotId) {
|
|
if (!confirm('Are you sure you want to delete snapshot "' + snapshotId + '"?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('/admin/snapshot/delete', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ client_id: clientId, snapshot_id: snapshotId })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
loadSnapshots();
|
|
loadStats();
|
|
} else {
|
|
alert(data.message || 'Failed to delete snapshot');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to delete snapshot');
|
|
}
|
|
}
|
|
|
|
// Delete admin
|
|
async function deleteAdmin(adminId) {
|
|
if (!confirm('Are you sure you want to delete this admin?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('/admin/admin/delete', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ admin_id: adminId })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
loadAdmins();
|
|
} else {
|
|
alert(data.message || 'Failed to delete admin');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to delete admin');
|
|
}
|
|
}
|
|
|
|
// Show change password modal
|
|
function showChangePassword(adminId, username) {
|
|
document.getElementById('change-password-admin-id').value = adminId;
|
|
document.getElementById('change-password-username').value = username;
|
|
document.getElementById('change-password-new').value = '';
|
|
document.getElementById('change-password-confirm').value = '';
|
|
showModal('change-password-modal');
|
|
}
|
|
|
|
// Show client change password modal
|
|
function showClientChangePassword(clientId) {
|
|
document.getElementById('client-password-client-id').value = clientId;
|
|
document.getElementById('client-password-client-name').value = clientId;
|
|
document.getElementById('client-password-new').value = '';
|
|
document.getElementById('client-password-confirm').value = '';
|
|
showModal('client-password-modal');
|
|
}
|
|
|
|
// Reset client password
|
|
async function resetClientPassword(clientId) {
|
|
if (!confirm('Are you sure you want to reset the API key for "' + clientId + '"? A new random key will be generated.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('/admin/client/reset-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ client_id: clientId })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
alert('New API key for ' + clientId + ': ' + data.api_key + '\n\nPlease save this key as it will not be shown again.');
|
|
} else {
|
|
alert(data.message || 'Failed to reset API key');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to reset API key');
|
|
}
|
|
}
|
|
|
|
// Event delegation for click handlers
|
|
document.addEventListener('click', function(e) {
|
|
const target = e.target;
|
|
|
|
// Handle modal content clicks - stop propagation
|
|
if (target.classList.contains('modal-content') || target.closest('.modal-content')) {
|
|
// Don't process if clicking on close button
|
|
if (!target.classList.contains('modal-close')) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Handle modal close buttons
|
|
if (target.classList.contains('modal-close')) {
|
|
const modalId = target.getAttribute('data-modal-id');
|
|
if (modalId) {
|
|
closeModal(modalId);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Handle modal overlay clicks (close on backdrop click)
|
|
if (target.classList.contains('fixed') && target.hasAttribute('data-modal-id')) {
|
|
closeModal(target.id);
|
|
return;
|
|
}
|
|
|
|
// Handle data-action buttons
|
|
const action = target.getAttribute('data-action');
|
|
if (!action) return;
|
|
|
|
switch (action) {
|
|
case 'show-modal':
|
|
const modalId = target.getAttribute('data-modal');
|
|
if (modalId) showModal(modalId);
|
|
break;
|
|
case 'edit-client':
|
|
const clientId = target.getAttribute('data-client-id');
|
|
if (clientId) editClient(clientId);
|
|
break;
|
|
case 'delete-client':
|
|
const delClientId = target.getAttribute('data-client-id');
|
|
if (delClientId) deleteClient(delClientId);
|
|
break;
|
|
case 'set-client-key':
|
|
const setKeyClientId = target.getAttribute('data-client-id');
|
|
if (setKeyClientId) showClientChangePassword(setKeyClientId);
|
|
break;
|
|
case 'reset-client-key':
|
|
const resetKeyClientId = target.getAttribute('data-client-id');
|
|
if (resetKeyClientId) resetClientPassword(resetKeyClientId);
|
|
break;
|
|
case 'delete-snapshot':
|
|
const snapClientId = target.getAttribute('data-client-id');
|
|
const snapshotId = target.getAttribute('data-snapshot-id');
|
|
if (snapClientId && snapshotId) deleteSnapshot(snapClientId, snapshotId);
|
|
break;
|
|
case 'change-admin-password':
|
|
const adminId = target.getAttribute('data-admin-id');
|
|
const adminUsername = target.getAttribute('data-admin-username');
|
|
if (adminId && adminUsername) showChangePassword(adminId, adminUsername);
|
|
break;
|
|
case 'delete-admin':
|
|
const delAdminId = target.getAttribute('data-admin-id');
|
|
if (delAdminId) deleteAdmin(parseInt(delAdminId));
|
|
break;
|
|
}
|
|
});
|
|
|
|
// Tab click handler
|
|
document.addEventListener('click', function(e) {
|
|
const tab = e.target.getAttribute('data-tab');
|
|
if (tab) {
|
|
showTab(tab);
|
|
}
|
|
});
|
|
|
|
// Snapshot filter change handler
|
|
document.addEventListener('change', function(e) {
|
|
if (e.target.id === 'snapshot-client-filter') {
|
|
loadSnapshots();
|
|
}
|
|
});
|
|
|
|
// Add client form
|
|
document.getElementById('add-client-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const client = {
|
|
client_id: document.getElementById('new-client-id').value,
|
|
api_key: document.getElementById('new-client-apikey').value,
|
|
storage_type: document.getElementById('new-client-storage').value,
|
|
dataset: document.getElementById('new-client-dataset').value,
|
|
max_size_bytes: parseInt(document.getElementById('new-client-quota').value) * 1024 * 1024 * 1024,
|
|
enabled: document.getElementById('new-client-enabled').checked,
|
|
rotation_policy: {
|
|
keep_hourly: parseInt(document.getElementById('new-client-hourly').value) || 0,
|
|
keep_daily: parseInt(document.getElementById('new-client-daily').value) || 0,
|
|
keep_weekly: parseInt(document.getElementById('new-client-weekly').value) || 0,
|
|
keep_monthly: parseInt(document.getElementById('new-client-monthly').value) || 0
|
|
}
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/admin/client/create', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(client)
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
closeModal('add-client-modal');
|
|
document.getElementById('add-client-form').reset();
|
|
loadClients();
|
|
loadStats();
|
|
} else {
|
|
alert(data.message || 'Failed to create client');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to create client');
|
|
}
|
|
});
|
|
|
|
// Edit client form
|
|
document.getElementById('edit-client-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const client = {
|
|
client_id: document.getElementById('edit-client-id').value,
|
|
api_key: document.getElementById('edit-client-apikey').value,
|
|
storage_type: document.getElementById('edit-client-storage').value,
|
|
dataset: document.getElementById('edit-client-dataset').value,
|
|
max_size_bytes: parseInt(document.getElementById('edit-client-quota').value) * 1024 * 1024 * 1024,
|
|
enabled: document.getElementById('edit-client-enabled').checked,
|
|
rotation_policy: {
|
|
keep_hourly: parseInt(document.getElementById('edit-client-hourly').value) || 0,
|
|
keep_daily: parseInt(document.getElementById('edit-client-daily').value) || 0,
|
|
keep_weekly: parseInt(document.getElementById('edit-client-weekly').value) || 0,
|
|
keep_monthly: parseInt(document.getElementById('edit-client-monthly').value) || 0
|
|
}
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/admin/client/update', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(client)
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
closeModal('edit-client-modal');
|
|
loadClients();
|
|
loadStats();
|
|
} else {
|
|
alert(data.message || 'Failed to update client');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to update client');
|
|
}
|
|
});
|
|
|
|
// Add admin form
|
|
document.getElementById('add-admin-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const admin = {
|
|
username: document.getElementById('new-admin-username').value,
|
|
password: document.getElementById('new-admin-password').value,
|
|
role: document.getElementById('new-admin-role').value
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/admin/admin/create', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(admin)
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
closeModal('add-admin-modal');
|
|
document.getElementById('add-admin-form').reset();
|
|
loadAdmins();
|
|
} else {
|
|
alert(data.message || 'Failed to create admin');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to create admin');
|
|
}
|
|
});
|
|
|
|
// Change password form
|
|
document.getElementById('change-password-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const adminId = document.getElementById('change-password-admin-id').value;
|
|
const newPassword = document.getElementById('change-password-new').value;
|
|
const confirmPassword = document.getElementById('change-password-confirm').value;
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
alert('Passwords do not match');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('/admin/admin/change-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
admin_id: parseInt(adminId),
|
|
new_password: newPassword
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
closeModal('change-password-modal');
|
|
} else {
|
|
alert(data.message || 'Failed to change password');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to change password');
|
|
}
|
|
});
|
|
|
|
// Client password form
|
|
document.getElementById('client-password-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const clientId = document.getElementById('client-password-client-id').value;
|
|
const newKey = document.getElementById('client-password-new').value;
|
|
const confirmKey = document.getElementById('client-password-confirm').value;
|
|
|
|
if (newKey !== confirmKey) {
|
|
alert('API keys do not match');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('/admin/client/set-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
client_id: clientId,
|
|
new_api_key: newKey
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
closeModal('client-password-modal');
|
|
} else {
|
|
alert(data.message || 'Failed to set API key');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to set API key');
|
|
}
|
|
});
|
|
|
|
// Initialize
|
|
checkAuth();
|
|
loadStats();
|
|
loadClients();
|