Files
zfs/internal/server/templates/static/admin.js
2026-02-13 23:27:54 +01:00

563 lines
22 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();
}
// Load stats
async function loadStats() {
try {
const res = await fetch('/admin/stats');
const data = await res.json();
document.getElementById('stats-grid').innerHTML =
'<div class="bg-white p-4 rounded-lg shadow text-center"><h4 class="text-gray-500 text-sm mb-2">Clients</h4><div class="text-2xl font-bold text-slate-800">' + data.client_count + '</div></div>' +
'<div class="bg-white p-4 rounded-lg shadow text-center"><h4 class="text-gray-500 text-sm mb-2">Total Snapshots</h4><div class="text-2xl font-bold text-slate-800">' + data.total_snapshots + '</div></div>' +
'<div class="bg-white p-4 rounded-lg shadow text-center"><h4 class="text-gray-500 text-sm mb-2">Total Storage</h4><div class="text-2xl font-bold text-slate-800">' + data.total_storage_gb.toFixed(2) + ' GB</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 hover:bg-gray-50">' +
'<td class="py-2 px-2 font-semibold">' + c.client_id + '</td>' +
'<td class="py-2 px-2"><span class="px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">' + c.storage_type + '</span></td>' +
'<td class="py-2 px-2">' + maxGB + ' GB</td>' +
'<td class="py-2 px-2">' +
'<div>' + usedGB + ' GB (' + usedPercent + '%)</div>' +
'<div class="w-24 h-2 bg-gray-200 rounded overflow-hidden mt-1"><div class="h-full bg-primary transition-all" style="width: ' + Math.min(usedPercent, 100) + '%"></div></div>' +
'</td>' +
'<td class="py-2 px-2">' + c.snapshot_count + '</td>' +
'<td class="py-2 px-2">' + (c.enabled ? '<span class="px-2 py-0.5 rounded text-xs font-semibold bg-green-100 text-green-800">Enabled</span>' : '<span class="px-2 py-0.5 rounded text-xs font-semibold bg-red-100 text-red-800">Disabled</span>') + '</td>' +
'<td class="py-2 px-2 whitespace-nowrap">' +
'<button class="px-2 py-1 text-xs rounded bg-warning hover:bg-yellow-600 text-white mr-1" data-action="edit-client" data-client-id="' + c.client_id + '">Edit</button>' +
'<button class="px-2 py-1 text-xs rounded bg-purple-500 hover:bg-purple-600 text-white mr-1" data-action="set-client-key" data-client-id="' + c.client_id + '">Set Key</button>' +
'<button class="px-2 py-1 text-xs rounded bg-orange-500 hover:bg-orange-600 text-white mr-1" data-action="reset-client-key" data-client-id="' + c.client_id + '">Reset Key</button>' +
'<button class="px-2 py-1 text-xs rounded bg-danger hover:bg-red-600 text-white" data-action="delete-client" data-client-id="' + c.client_id + '">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 hover:bg-gray-50">' +
'<td class="py-2 px-2">' + s.client_id + '</td>' +
'<td class="py-2 px-2">' + s.snapshot_id + '</td>' +
'<td class="py-2 px-2">' + new Date(s.timestamp).toLocaleString() + '</td>' +
'<td class="py-2 px-2">' + sizeGB + ' GB</td>' +
'<td class="py-2 px-2">' +
(s.incremental ? '<span class="px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">Incremental</span>' : '<span class="px-2 py-0.5 rounded text-xs font-semibold bg-green-100 text-green-800">Full</span>') +
(s.compressed ? ' <span class="px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">LZ4</span>' : '') +
'</td>' +
'<td class="py-2 px-2"><button class="px-2 py-1 text-xs rounded bg-danger hover:bg-red-600 text-white" data-action="delete-snapshot" data-client-id="' + s.client_id + '" data-snapshot-id="' + s.snapshot_id + '">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 hover:bg-gray-50">' +
'<td class="py-2 px-2">' + a.id + '</td>' +
'<td class="py-2 px-2 font-semibold">' + a.username + '</td>' +
'<td class="py-2 px-2"><span class="px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">' + a.role + '</span></td>' +
'<td class="py-2 px-2">' + new Date(a.created_at).toLocaleDateString() + '</td>' +
'<td class="py-2 px-2 whitespace-nowrap">' +
'<button class="px-2 py-1 text-xs rounded bg-warning hover:bg-yellow-600 text-white mr-1" data-action="change-admin-password" data-admin-id="' + a.id + '" data-admin-username="' + a.username + '">Change Password</button>' +
'<button class="px-2 py-1 text-xs rounded bg-danger hover:bg-red-600 text-white" data-action="delete-admin" data-admin-id="' + a.id + '">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-primary', 'text-white');
t.classList.add('bg-white', 'text-gray-600');
});
document.querySelector('[data-tab="' + tab + '"]').classList.remove('bg-white', 'text-gray-600');
document.querySelector('[data-tab="' + tab + '"]').classList.add('bg-primary', 'text-white');
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/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');
alert('Password changed successfully');
} 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/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: clientId, api_key: newKey })
});
const data = await res.json();
if (data.success) {
closeModal('client-password-modal');
alert('API key changed successfully for ' + clientId);
} else {
alert(data.message || 'Failed to change API key');
}
} catch (e) {
alert('Failed to change API key');
}
});
// Initialize
loadStats();
loadClients();