711 lines
27 KiB
HTML
711 lines
27 KiB
HTML
{{define "management-content"}}
|
|
<div class="space-y-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-white mb-2">System Management</h1>
|
|
<p class="text-slate-400">Manage services and users</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="border-b border-slate-800">
|
|
<nav class="flex gap-4">
|
|
<button onclick="switchTab('services')" id="tab-services" class="tab-button px-4 py-2 border-b-2 border-blue-600 text-blue-400 font-medium">
|
|
Services
|
|
</button>
|
|
<button onclick="switchTab('users')" id="tab-users" class="tab-button px-4 py-2 border-b-2 border-transparent text-slate-400 hover:text-slate-300">
|
|
Users
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Services Tab -->
|
|
<div id="content-services" class="tab-content">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
|
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-white">Service Management</h2>
|
|
<div class="flex gap-2">
|
|
<button onclick="loadServiceStatus()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
</div>
|
|
</div>
|
|
<div class="p-4 space-y-4">
|
|
<!-- Services List -->
|
|
<div id="services-list" class="space-y-4">
|
|
<p class="text-slate-400 text-sm">Loading services...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Users Tab -->
|
|
<div id="content-users" class="tab-content hidden">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
|
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-white">User Management</h2>
|
|
<div class="flex gap-2">
|
|
<button onclick="showCreateUserModal()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Create User
|
|
</button>
|
|
<button onclick="loadUsers()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
</div>
|
|
</div>
|
|
<div id="users-list" class="p-4">
|
|
<p class="text-slate-400 text-sm">Loading...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Service Logs Modal -->
|
|
<div id="service-logs-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-4xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-xl font-semibold text-white" id="service-logs-title">Service Logs</h3>
|
|
<div class="flex items-center gap-2">
|
|
<input type="number" id="logs-lines" value="50" min="10" max="1000" class="w-20 px-2 py-1 bg-slate-900 border border-slate-700 rounded text-white text-sm">
|
|
<button onclick="loadServiceLogs()" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">Refresh</button>
|
|
<button onclick="closeModal('service-logs-modal')" class="px-3 py-1 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">Close</button>
|
|
</div>
|
|
</div>
|
|
<div id="service-logs-content" class="flex-1 overflow-y-auto bg-slate-900 rounded p-4 text-sm text-slate-300 font-mono whitespace-pre-wrap">
|
|
Loading logs...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create User Modal -->
|
|
<div id="create-user-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-md w-full mx-4">
|
|
<h3 class="text-xl font-semibold text-white mb-4">Create User</h3>
|
|
<form id="create-user-form" onsubmit="createUser(event)" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Username *</label>
|
|
<input type="text" name="username" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Email</label>
|
|
<input type="email" name="email" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Password *</label>
|
|
<input type="password" name="password" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Role *</label>
|
|
<select name="role" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
<option value="viewer">Viewer</option>
|
|
<option value="operator">Operator</option>
|
|
<option value="administrator">Administrator</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex gap-2 justify-end">
|
|
<button type="button" onclick="closeModal('create-user-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Create
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit User Modal -->
|
|
<div id="edit-user-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-md w-full mx-4">
|
|
<h3 class="text-xl font-semibold text-white mb-4">Edit User</h3>
|
|
<form id="edit-user-form" onsubmit="updateUser(event)" class="space-y-4">
|
|
<input type="hidden" id="edit-user-id" name="id">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Username</label>
|
|
<input type="text" id="edit-username" disabled class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-slate-500 text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Email</label>
|
|
<input type="email" id="edit-email" name="email" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Role</label>
|
|
<select id="edit-role" name="role" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
<option value="viewer">Viewer</option>
|
|
<option value="operator">Operator</option>
|
|
<option value="administrator">Administrator</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="flex items-center gap-2 text-sm text-slate-300">
|
|
<input type="checkbox" id="edit-active" name="active" class="rounded bg-slate-900 border-slate-700">
|
|
<span>Active</span>
|
|
</label>
|
|
</div>
|
|
<div class="flex gap-2 justify-end">
|
|
<button type="button" onclick="closeModal('edit-user-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Update
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Change Password Modal -->
|
|
<div id="change-password-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-md w-full mx-4">
|
|
<h3 class="text-xl font-semibold text-white mb-4">Change Password</h3>
|
|
<form id="change-password-form" onsubmit="changePassword(event)" class="space-y-4">
|
|
<input type="hidden" id="change-password-user-id" name="id">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Current Password *</label>
|
|
<input type="password" name="old_password" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">New Password *</label>
|
|
<input type="password" name="new_password" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Confirm New Password *</label>
|
|
<input type="password" name="confirm_password" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div class="flex gap-2 justify-end">
|
|
<button type="button" onclick="closeModal('change-password-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Change Password
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function getAuthHeaders() {
|
|
const token = localStorage.getItem('atlas_token');
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
|
};
|
|
}
|
|
|
|
function switchTab(tab) {
|
|
// Hide all tabs
|
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
|
document.querySelectorAll('.tab-button').forEach(el => {
|
|
el.classList.remove('border-blue-600', 'text-blue-400', 'font-medium');
|
|
el.classList.add('border-transparent', 'text-slate-400');
|
|
});
|
|
|
|
// Show selected tab
|
|
document.getElementById(`content-${tab}`).classList.remove('hidden');
|
|
document.getElementById(`tab-${tab}`).classList.remove('border-transparent', 'text-slate-400');
|
|
document.getElementById(`tab-${tab}`).classList.add('border-blue-600', 'text-blue-400', 'font-medium');
|
|
|
|
// Load data for the tab
|
|
if (tab === 'services') loadServiceStatus();
|
|
else if (tab === 'users') loadUsers();
|
|
}
|
|
|
|
function closeModal(modalId) {
|
|
document.getElementById(modalId).classList.add('hidden');
|
|
}
|
|
|
|
// ===== Service Management =====
|
|
|
|
let currentServiceName = null;
|
|
|
|
async function loadServiceStatus() {
|
|
const token = localStorage.getItem('atlas_token');
|
|
const listEl = document.getElementById('services-list');
|
|
|
|
if (!token) {
|
|
listEl.innerHTML = `
|
|
<div class="text-center py-4">
|
|
<p class="text-slate-400 mb-2">Authentication required</p>
|
|
<a href="/login?return=/management" class="text-blue-400 hover:text-blue-300">Click here to login</a>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = '<p class="text-slate-400 text-sm">Loading services...</p>';
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/services', { headers: getAuthHeaders() });
|
|
const data = await res.json().catch(() => null);
|
|
|
|
if (!res.ok) {
|
|
if (res.status === 401) {
|
|
localStorage.removeItem('atlas_token');
|
|
localStorage.removeItem('atlas_user');
|
|
listEl.innerHTML = `
|
|
<div class="text-center py-4">
|
|
<p class="text-slate-400 mb-2">Session expired. Please login again.</p>
|
|
<a href="/login?return=/management" class="text-blue-400 hover:text-blue-300">Click here to login</a>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
const errorMsg = (data && data.error) ? data.error : `HTTP ${res.status}`;
|
|
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${errorMsg}</p>`;
|
|
return;
|
|
}
|
|
|
|
const services = (data && data.services) ? data.services : [];
|
|
|
|
if (services.length === 0) {
|
|
listEl.innerHTML = '<p class="text-slate-400 text-sm">No services found</p>';
|
|
return;
|
|
}
|
|
|
|
// Render all services
|
|
listEl.innerHTML = services.map(svc => {
|
|
const statusBadge = getStatusBadge(svc.status);
|
|
const statusColor = getStatusColor(svc.status);
|
|
|
|
return `
|
|
<div class="bg-slate-900 rounded-lg border border-slate-700 p-4">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-md font-semibold text-white">${escapeHtml(svc.display_name)}</h3>
|
|
<div class="px-3 py-1 rounded text-xs font-medium ${statusColor}">
|
|
${statusBadge}
|
|
</div>
|
|
</div>
|
|
<div class="text-xs text-slate-500 font-mono mb-4 max-h-24 overflow-y-auto whitespace-pre-wrap">
|
|
${escapeHtml((svc.output || '').split('\n').slice(-5).join('\n')) || 'No status information'}
|
|
</div>
|
|
<div class="flex gap-2 flex-wrap">
|
|
<button onclick="serviceAction('${svc.name}', 'start')" class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded text-xs">
|
|
Start
|
|
</button>
|
|
<button onclick="serviceAction('${svc.name}', 'stop')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-xs">
|
|
Stop
|
|
</button>
|
|
<button onclick="serviceAction('${svc.name}', 'restart')" class="px-3 py-1.5 bg-yellow-600 hover:bg-yellow-700 text-white rounded text-xs">
|
|
Restart
|
|
</button>
|
|
<button onclick="serviceAction('${svc.name}', 'reload')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs">
|
|
Reload
|
|
</button>
|
|
<button onclick="showServiceLogs('${svc.name}', '${escapeHtml(svc.display_name)}')" class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded text-xs">
|
|
View Logs
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
} catch (err) {
|
|
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
|
|
}
|
|
}
|
|
|
|
function getStatusBadge(status) {
|
|
switch(status) {
|
|
case 'running': return 'Running';
|
|
case 'stopped': return 'Stopped';
|
|
case 'failed': return 'Failed';
|
|
case 'not-found': return 'Not Found';
|
|
default: return 'Unknown';
|
|
}
|
|
}
|
|
|
|
function getStatusColor(status) {
|
|
switch(status) {
|
|
case 'running': return 'bg-green-900 text-green-300';
|
|
case 'stopped': return 'bg-red-900 text-red-300';
|
|
case 'failed': return 'bg-red-900 text-red-300';
|
|
case 'not-found': return 'bg-slate-700 text-slate-300';
|
|
default: return 'bg-slate-700 text-slate-300';
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async function serviceAction(serviceName, action) {
|
|
const token = localStorage.getItem('atlas_token');
|
|
if (!token) {
|
|
alert('Please login first');
|
|
window.location.href = '/login?return=/management';
|
|
return;
|
|
}
|
|
|
|
if (!confirm(`Are you sure you want to ${action} ${serviceName}?`)) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/services/${action}?service=${encodeURIComponent(serviceName)}`, {
|
|
method: 'POST',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
const data = await res.json().catch(() => null);
|
|
|
|
if (res.ok) {
|
|
alert(`${serviceName} ${action}ed successfully`);
|
|
loadServiceStatus();
|
|
} else {
|
|
const errorMsg = (data && data.error) ? data.error : `Failed to ${action} service`;
|
|
alert(`Error: ${errorMsg}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
function showServiceLogs(serviceName, displayName) {
|
|
const token = localStorage.getItem('atlas_token');
|
|
if (!token) {
|
|
alert('Please login first');
|
|
window.location.href = '/login?return=/management';
|
|
return;
|
|
}
|
|
|
|
currentServiceName = serviceName;
|
|
document.getElementById('service-logs-title').textContent = `${displayName} Logs`;
|
|
document.getElementById('service-logs-modal').classList.remove('hidden');
|
|
loadServiceLogs();
|
|
}
|
|
|
|
async function loadServiceLogs() {
|
|
const token = localStorage.getItem('atlas_token');
|
|
if (!token) {
|
|
document.getElementById('service-logs-content').textContent = 'Authentication required';
|
|
return;
|
|
}
|
|
|
|
if (!currentServiceName) {
|
|
document.getElementById('service-logs-content').textContent = 'No service selected';
|
|
return;
|
|
}
|
|
|
|
const lines = document.getElementById('logs-lines').value || '50';
|
|
try {
|
|
const res = await fetch(`/api/v1/services/logs?service=${encodeURIComponent(currentServiceName)}&lines=${lines}`, {
|
|
headers: getAuthHeaders()
|
|
});
|
|
const data = await res.json().catch(() => null);
|
|
|
|
if (!res.ok) {
|
|
const errorMsg = (data && data.error) ? data.error : `HTTP ${res.status}`;
|
|
document.getElementById('service-logs-content').textContent = `Error: ${errorMsg}`;
|
|
return;
|
|
}
|
|
|
|
document.getElementById('service-logs-content').textContent = data.logs || 'No logs available';
|
|
} catch (err) {
|
|
document.getElementById('service-logs-content').textContent = `Error: ${err.message}`;
|
|
}
|
|
}
|
|
|
|
// ===== User Management =====
|
|
|
|
async function loadUsers(forceRefresh = false) {
|
|
const token = localStorage.getItem('atlas_token');
|
|
const listEl = document.getElementById('users-list');
|
|
|
|
if (!token) {
|
|
listEl.innerHTML = `
|
|
<div class="text-center py-8">
|
|
<p class="text-slate-400 mb-2">Authentication required to view users</p>
|
|
<a href="/login?return=/management" class="text-blue-400 hover:text-blue-300">Click here to login</a>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
listEl.innerHTML = '<p class="text-slate-400 text-sm">Loading users...</p>';
|
|
|
|
try {
|
|
// Add cache busting parameter if force refresh
|
|
const url = forceRefresh ? `/api/v1/users?_t=${Date.now()}` : '/api/v1/users';
|
|
const res = await fetch(url, {
|
|
headers: getAuthHeaders(),
|
|
cache: forceRefresh ? 'no-cache' : 'default'
|
|
});
|
|
|
|
const rawText = await res.text();
|
|
let data = null;
|
|
try {
|
|
data = JSON.parse(rawText);
|
|
} catch (parseErr) {
|
|
console.error('Failed to parse JSON response:', parseErr, 'Raw response:', rawText);
|
|
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid JSON response from server</p>';
|
|
return;
|
|
}
|
|
|
|
if (!res.ok) {
|
|
if (res.status === 401) {
|
|
localStorage.removeItem('atlas_token');
|
|
localStorage.removeItem('atlas_user');
|
|
listEl.innerHTML = `
|
|
<div class="text-center py-8">
|
|
<p class="text-slate-400 mb-2">Session expired. Please login again.</p>
|
|
<a href="/login?return=/management" class="text-blue-400 hover:text-blue-300">Click here to login</a>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
const errorMsg = (data && data.error) ? data.error : `HTTP ${res.status}`;
|
|
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${errorMsg}</p>`;
|
|
return;
|
|
}
|
|
|
|
if (!data || !Array.isArray(data)) {
|
|
console.error('Invalid response format:', data);
|
|
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format (expected array)</p>';
|
|
return;
|
|
}
|
|
|
|
console.log(`Loaded ${data.length} users:`, data);
|
|
|
|
if (data.length === 0) {
|
|
listEl.innerHTML = '<p class="text-slate-400 text-sm">No users found</p>';
|
|
return;
|
|
}
|
|
|
|
// Sort users by ID to ensure consistent ordering
|
|
const sortedUsers = data.sort((a, b) => {
|
|
const idA = (a.id || '').toLowerCase();
|
|
const idB = (b.id || '').toLowerCase();
|
|
return idA.localeCompare(idB);
|
|
});
|
|
|
|
listEl.innerHTML = sortedUsers.map(user => {
|
|
// Escape HTML to prevent XSS
|
|
const escapeHtml = (text) => {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
};
|
|
|
|
const username = escapeHtml(user.username || 'N/A');
|
|
const email = user.email ? escapeHtml(user.email) : '';
|
|
const userId = escapeHtml(user.id || 'N/A');
|
|
const role = escapeHtml(user.role || 'N/A');
|
|
const active = user.active !== false;
|
|
|
|
return `
|
|
<div class="border-b border-slate-700 last:border-0 py-4">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<h3 class="text-lg font-semibold text-white">${username}</h3>
|
|
${active ? '<span class="px-2 py-1 rounded text-xs font-medium bg-green-900 text-green-300">Active</span>' : '<span class="px-2 py-1 rounded text-xs font-medium bg-slate-700 text-slate-300">Inactive</span>'}
|
|
<span class="px-2 py-1 rounded text-xs font-medium bg-blue-900 text-blue-300">${role}</span>
|
|
</div>
|
|
<div class="text-sm text-slate-400 space-y-1">
|
|
${email ? `<p>Email: <span class="text-slate-300">${email}</span></p>` : ''}
|
|
<p>ID: <span class="text-slate-300 font-mono text-xs">${userId}</span></p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2 ml-4">
|
|
<button onclick="showEditUserModal('${userId}')" class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
|
Edit
|
|
</button>
|
|
<button onclick="showChangePasswordModal('${userId}', '${username}')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Change Password
|
|
</button>
|
|
<button onclick="deleteUser('${userId}', '${username}')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
} catch (err) {
|
|
document.getElementById('users-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
|
|
}
|
|
}
|
|
|
|
function showCreateUserModal() {
|
|
document.getElementById('create-user-modal').classList.remove('hidden');
|
|
}
|
|
|
|
async function createUser(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/users', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
username: formData.get('username'),
|
|
email: formData.get('email') || '',
|
|
password: formData.get('password'),
|
|
role: formData.get('role')
|
|
})
|
|
});
|
|
|
|
const rawText = await res.text();
|
|
let data = null;
|
|
try {
|
|
data = JSON.parse(rawText);
|
|
} catch (parseErr) {
|
|
console.error('Failed to parse create user response:', parseErr, 'Raw:', rawText);
|
|
alert('Error: Invalid response from server');
|
|
return;
|
|
}
|
|
|
|
console.log('Create user response:', res.status, data);
|
|
|
|
if (res.ok || res.status === 201) {
|
|
console.log('User created successfully, refreshing list...');
|
|
closeModal('create-user-modal');
|
|
e.target.reset();
|
|
// Force reload users list - add cache busting
|
|
await loadUsers(true);
|
|
alert('User created successfully');
|
|
} else {
|
|
const errorMsg = (data && data.error) ? data.error : 'Failed to create user';
|
|
console.error('Create user failed:', errorMsg);
|
|
alert(`Error: ${errorMsg}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function showEditUserModal(userId) {
|
|
try {
|
|
const res = await fetch(`/api/v1/users/${userId}`, { headers: getAuthHeaders() });
|
|
if (!res.ok) {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to load user'}`);
|
|
return;
|
|
}
|
|
|
|
const user = await res.json();
|
|
document.getElementById('edit-user-id').value = user.id;
|
|
document.getElementById('edit-username').value = user.username || '';
|
|
document.getElementById('edit-email').value = user.email || '';
|
|
document.getElementById('edit-role').value = user.role || 'Viewer';
|
|
document.getElementById('edit-active').checked = user.active !== false;
|
|
|
|
document.getElementById('edit-user-modal').classList.remove('hidden');
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function updateUser(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
const userId = formData.get('id');
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/users/${userId}`, {
|
|
method: 'PUT',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
email: formData.get('email') || '',
|
|
role: formData.get('role'),
|
|
active: formData.get('active') === 'on'
|
|
})
|
|
});
|
|
|
|
if (res.ok) {
|
|
closeModal('edit-user-modal');
|
|
await loadUsers(true);
|
|
alert('User updated successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to update user'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function deleteUser(userId, username) {
|
|
if (!confirm(`Are you sure you want to delete user "${username}"? This action cannot be undone.`)) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/users/${userId}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
if (res.ok) {
|
|
await loadUsers(true);
|
|
alert('User deleted successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to delete user'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
function showChangePasswordModal(userId, username) {
|
|
document.getElementById('change-password-user-id').value = userId;
|
|
document.getElementById('change-password-modal').classList.remove('hidden');
|
|
}
|
|
|
|
async function changePassword(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
const userId = document.getElementById('change-password-user-id').value;
|
|
const newPassword = formData.get('new_password');
|
|
const confirmPassword = formData.get('confirm_password');
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
alert('New passwords do not match');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/users/${userId}/password`, {
|
|
method: 'PUT',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
old_password: formData.get('old_password'),
|
|
new_password: newPassword
|
|
})
|
|
});
|
|
|
|
if (res.ok) {
|
|
closeModal('change-password-modal');
|
|
e.target.reset();
|
|
alert('Password changed successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to change password'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Load initial data when page loads
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadServiceStatus();
|
|
// Load users if on users tab
|
|
const usersTab = document.getElementById('tab-users');
|
|
if (usersTab && usersTab.classList.contains('border-blue-600')) {
|
|
loadUsers();
|
|
}
|
|
});
|
|
} else {
|
|
loadServiceStatus();
|
|
// Load users if on users tab
|
|
const usersTab = document.getElementById('tab-users');
|
|
if (usersTab && usersTab.classList.contains('border-blue-600')) {
|
|
loadUsers();
|
|
}
|
|
}
|
|
</script>
|
|
{{end}}
|
|
|
|
{{define "management.html"}}
|
|
{{template "base" .}}
|
|
{{end}}
|