295 lines
15 KiB
HTML
295 lines
15 KiB
HTML
{{define "admin"}}
|
|
{{template "base" .}}
|
|
{{end}}
|
|
|
|
{{define "admin_content"}}
|
|
<div class="max-w-6xl mx-auto p-6">
|
|
<!-- Header -->
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold text-white mb-2">Admin Dashboard</h1>
|
|
<p class="text-gray-400">Manage users, groups, and permissions</p>
|
|
</div>
|
|
|
|
<div class="space-y-8">
|
|
<!-- Users -->
|
|
<div class="bg-gray-800 rounded-lg p-6">
|
|
<h2 class="text-xl font-semibold text-white mb-4">
|
|
<i class="fas fa-users mr-2"></i>Users
|
|
</h2>
|
|
<!-- Create User -->
|
|
<form id="form-create-user" class="mb-4 grid grid-cols-1 md:grid-cols-4 gap-2">
|
|
<input name="username" type="text" class="form-input" placeholder="Username" required />
|
|
<input name="email" type="email" class="form-input" placeholder="Email" required />
|
|
<input name="password" type="password" class="form-input" placeholder="Password" required />
|
|
<button type="submit" class="btn-primary text-sm"><i class="fas fa-plus mr-2"></i>Create</button>
|
|
</form>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full text-sm">
|
|
<thead>
|
|
<tr class="text-left text-gray-300">
|
|
<th class="px-3 py-2">Username</th>
|
|
<th class="px-3 py-2">Email</th>
|
|
<th class="px-3 py-2">Status</th>
|
|
<th class="px-3 py-2">MFA</th>
|
|
<th class="px-3 py-2">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{if .users}}
|
|
{{range .users}}
|
|
<tr class="border-t border-gray-700" data-user-id="{{.ID}}" data-username="{{.Username}}" data-has-mfa="{{if .MFASecret.Valid}}1{{else}}0{{end}}">
|
|
<td class="px-3 py-2 text-white">{{.Username}}</td>
|
|
<td class="px-3 py-2 text-gray-300">{{.Email}}</td>
|
|
<td class="px-3 py-2">
|
|
{{if .IsActive}}<span class="text-green-400">Active</span>{{else}}<span class="text-red-400">Disabled</span>{{end}}
|
|
</td>
|
|
<td class="px-3 py-2">
|
|
{{if .MFASecret.Valid}}<span class="text-green-400">Enabled</span>{{else}}<span class="text-gray-400">None</span>{{end}}
|
|
</td>
|
|
<td class="px-3 py-2 space-x-1">
|
|
{{if ne .ID $.CurrentUserID}}
|
|
{{if .IsActive}}
|
|
<button class="btn-secondary text-xs px-2 py-1" data-action="user-deactivate" title="Disable account">Disable</button>
|
|
{{else}}
|
|
<button class="btn-primary text-xs px-2 py-1" data-action="user-activate" title="Enable account">Enable</button>
|
|
{{end}}
|
|
{{if .MFASecret.Valid}}
|
|
<button class="btn-secondary text-xs px-2 py-1" data-action="mfa-disable" title="Disable MFA">MFA Off</button>
|
|
<button class="btn-warning text-xs px-2 py-1" data-action="mfa-reset" title="Reset MFA secret">MFA Reset</button>
|
|
{{else}}
|
|
<button class="btn-primary text-xs px-2 py-1" data-action="mfa-enable" title="Enable MFA">MFA On</button>
|
|
{{end}}
|
|
<button class="btn-danger text-xs px-2 py-1" data-action="delete-user" title="Delete user"><i class="fas fa-trash"></i></button>
|
|
{{else}}
|
|
<span class="text-xs text-gray-500">Actions disabled for current user</span>
|
|
{{end}}
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
{{else}}
|
|
<tr><td colspan="5" class="px-3 py-4 text-gray-400">No users found.</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Groups -->
|
|
<div class="bg-gray-800 rounded-lg p-6">
|
|
<h2 class="text-xl font-semibold text-white mb-4">
|
|
<i class="fas fa-user-group mr-2"></i>Groups
|
|
</h2>
|
|
<form id="form-create-group" class="mb-4 grid grid-cols-1 md:grid-cols-4 gap-2">
|
|
<input name="name" type="text" class="form-input md:col-span-3" placeholder="Group name" required />
|
|
<button type="submit" class="btn-primary text-sm"><i class="fas fa-plus mr-2"></i>Create</button>
|
|
</form>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full text-sm">
|
|
<thead>
|
|
<tr class="text-left text-gray-300">
|
|
<th class="px-3 py-2">Name</th>
|
|
<th class="px-3 py-2">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{if .groups}}
|
|
{{range .groups}}
|
|
<tr class="border-t border-gray-700" data-group-id="{{.ID}}" data-group-name="{{.Name}}">
|
|
<td class="px-3 py-2 text-white">{{.Name}}</td>
|
|
<td class="px-3 py-2">
|
|
<button class="btn-danger text-xs px-2 py-1" data-action="delete-group" title="Delete group"><i class="fas fa-trash"></i></button>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
{{else}}
|
|
<tr><td colspan="2" class="px-3 py-4 text-gray-400">No groups found.</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Permissions and Memberships -->
|
|
<div class="bg-gray-800 rounded-lg p-6">
|
|
<h2 class="text-xl font-semibold text-white mb-4">
|
|
<i class="fas fa-key mr-2"></i>Permissions</h2>
|
|
<div class="overflow-x-auto mb-6">
|
|
<table class="min-w-full text-sm">
|
|
<thead>
|
|
<tr class="text-left text-gray-300">
|
|
<th class="px-3 py-2">Group</th>
|
|
<th class="px-3 py-2">Path</th>
|
|
<th class="px-3 py-2">Read</th>
|
|
<th class="px-3 py-2">Write</th>
|
|
<th class="px-3 py-2">Delete</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{if .permissions}}
|
|
{{range .permissions}}
|
|
<tr class="border-t border-gray-700">
|
|
<td class="px-3 py-2 text-white">{{.Group}}</td>
|
|
<td class="px-3 py-2"><code class="bg-gray-900 px-1 py-0.5 rounded">{{.Path}}</code></td>
|
|
<td class="px-3 py-2">{{if .CanRead}}✅{{else}}❌{{end}}</td>
|
|
<td class="px-3 py-2">{{if .CanWrite}}✅{{else}}❌{{end}}</td>
|
|
<td class="px-3 py-2">{{if .CanDelete}}✅{{else}}❌{{end}}</td>
|
|
</tr>
|
|
{{end}}
|
|
{{else}}
|
|
<tr><td colspan="5" class="px-3 py-4 text-gray-400">No permissions configured.</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h3 class="text-white font-semibold mb-3">Manage Memberships</h3>
|
|
<form id="form-add-membership" class="grid grid-cols-1 md:grid-cols-3 gap-2 mb-3">
|
|
<select name="user_id" class="form-input" required>
|
|
<option value="" disabled selected>Select user</option>
|
|
{{range .users}}<option value="{{.ID}}">{{.Username}}</option>{{end}}
|
|
</select>
|
|
<select name="group_id" class="form-input" required>
|
|
<option value="" disabled selected>Select group</option>
|
|
{{range .groups}}<option value="{{.ID}}">{{.Name}}</option>{{end}}
|
|
</select>
|
|
<button type="submit" class="btn-primary text-sm"><i class="fas fa-user-plus mr-2"></i>Add to Group</button>
|
|
</form>
|
|
<form id="form-remove-membership" class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
|
<select name="user_id" class="form-input" required>
|
|
<option value="" disabled selected>Select user</option>
|
|
{{range .users}}<option value="{{.ID}}">{{.Username}}</option>{{end}}
|
|
</select>
|
|
<select name="group_id" class="form-input" required>
|
|
<option value="" disabled selected>Select group</option>
|
|
{{range .groups}}<option value="{{.ID}}">{{.Name}}</option>{{end}}
|
|
</select>
|
|
<button type="submit" class="btn-danger text-sm"><i class="fas fa-user-minus mr-2"></i>Remove from Group</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "admin_scripts"}}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const getCSRF = () => {
|
|
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
|
return m && m[1] ? decodeURIComponent(m[1]) : '';
|
|
};
|
|
|
|
// Create user
|
|
const formCreateUser = document.getElementById('form-create-user');
|
|
if (formCreateUser) {
|
|
formCreateUser.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const fd = new FormData(formCreateUser);
|
|
const res = await fetch(window.prefix('/editor/admin/users'), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (res.ok && data.success) { showNotification('User created', 'success'); window.location.reload(); }
|
|
else { showNotification('Create user failed: ' + (data.error || res.statusText), 'error'); }
|
|
});
|
|
}
|
|
|
|
// Delete user
|
|
document.querySelectorAll('[data-action="delete-user"]').forEach(btn => {
|
|
btn.addEventListener('click', async (e) => {
|
|
const row = e.currentTarget.closest('[data-user-id]');
|
|
const id = row && row.getAttribute('data-user-id');
|
|
const username = (row && row.getAttribute('data-username')) || '';
|
|
if (!id) return;
|
|
if (username === 'admin') { showNotification('Cannot delete default admin user', 'error'); return; }
|
|
if (!confirm('Delete user ' + username + ' ?')) return;
|
|
const res = await fetch(window.prefix('/editor/admin/users/' + encodeURIComponent(id)), { method: 'DELETE', headers: { 'X-CSRF-Token': getCSRF() } });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (res.ok && data.success) { showNotification('User deleted', 'success'); window.location.reload(); }
|
|
else { showNotification('Delete user failed: ' + (data.error || res.statusText), 'error'); }
|
|
});
|
|
});
|
|
|
|
// Activate/deactivate user
|
|
document.querySelectorAll('[data-action="user-activate"], [data-action="user-deactivate"]').forEach(btn => {
|
|
btn.addEventListener('click', async (e) => {
|
|
const row = e.currentTarget.closest('[data-user-id]');
|
|
const id = row && row.getAttribute('data-user-id');
|
|
const action = e.currentTarget.getAttribute('data-action');
|
|
const active = action === 'user-activate' ? '1' : '0';
|
|
const fd = new FormData();
|
|
fd.set('active', active);
|
|
const res = await fetch(window.prefix('/editor/admin/users/' + encodeURIComponent(id) + '/active'), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (res.ok && data.success) { showNotification('User status updated', 'success'); window.location.reload(); }
|
|
else { showNotification('Update status failed: ' + (data.error || res.statusText), 'error'); }
|
|
});
|
|
});
|
|
|
|
// MFA actions
|
|
const mfaRequest = async (row, path, okMsg) => {
|
|
const id = row && row.getAttribute('data-user-id');
|
|
const res = await fetch(window.prefix('/editor/admin/users/' + encodeURIComponent(id) + path), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() } });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (res.ok && data.success) { showNotification(okMsg, 'success'); window.location.reload(); }
|
|
else { showNotification('MFA action failed: ' + (data.error || res.statusText), 'error'); }
|
|
};
|
|
document.querySelectorAll('[data-action="mfa-enable"]').forEach(btn => btn.addEventListener('click', (e) => mfaRequest(e.currentTarget.closest('[data-user-id]'), '/mfa/enable', 'MFA enabled')));
|
|
document.querySelectorAll('[data-action="mfa-disable"]').forEach(btn => btn.addEventListener('click', (e) => mfaRequest(e.currentTarget.closest('[data-user-id]'), '/mfa/disable', 'MFA disabled')));
|
|
document.querySelectorAll('[data-action="mfa-reset"]').forEach(btn => btn.addEventListener('click', (e) => mfaRequest(e.currentTarget.closest('[data-user-id]'), '/mfa/reset', 'MFA reset')));
|
|
|
|
// Create group
|
|
const formCreateGroup = document.getElementById('form-create-group');
|
|
if (formCreateGroup) {
|
|
formCreateGroup.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const fd = new FormData(formCreateGroup);
|
|
const res = await fetch(window.prefix('/editor/admin/groups'), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (res.ok && data.success) { showNotification('Group created', 'success'); window.location.reload(); }
|
|
else { showNotification('Create group failed: ' + (data.error || res.statusText), 'error'); }
|
|
});
|
|
}
|
|
|
|
// Delete group
|
|
document.querySelectorAll('[data-action="delete-group"]').forEach(btn => {
|
|
btn.addEventListener('click', async (e) => {
|
|
const card = e.currentTarget.closest('[data-group-id]');
|
|
const id = card && card.getAttribute('data-group-id');
|
|
const name = (card && card.getAttribute('data-group-name')) || '';
|
|
if (!id) return;
|
|
if (name === 'admin' || name === 'public') { showNotification('Cannot delete core group: ' + name, 'error'); return; }
|
|
if (!confirm('Delete group ' + name + ' ?')) return;
|
|
const res = await fetch(window.prefix('/editor/admin/groups/' + encodeURIComponent(id)), { method: 'DELETE', headers: { 'X-CSRF-Token': getCSRF() } });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (res.ok && data.success) { showNotification('Group deleted', 'success'); window.location.reload(); }
|
|
else { showNotification('Delete group failed: ' + (data.error || res.statusText), 'error'); }
|
|
});
|
|
});
|
|
|
|
// Add membership
|
|
const formAddMem = document.getElementById('form-add-membership');
|
|
if (formAddMem) {
|
|
formAddMem.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const fd = new FormData(formAddMem);
|
|
const res = await fetch(window.prefix('/editor/admin/memberships/add'), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (res.ok && data.success) { showNotification('User added to group', 'success'); }
|
|
else { showNotification('Add membership failed: ' + (data.error || res.statusText), 'error'); }
|
|
});
|
|
}
|
|
|
|
// Remove membership
|
|
const formRemMem = document.getElementById('form-remove-membership');
|
|
if (formRemMem) {
|
|
formRemMem.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const fd = new FormData(formRemMem);
|
|
const res = await fetch(window.prefix('/editor/admin/memberships/remove'), { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (res.ok && data.success) { showNotification('User removed from group', 'success'); }
|
|
else { showNotification('Remove membership failed: ' + (data.error || res.statusText), 'error'); }
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{{end}}
|