Files
gobsidian/web/templates/admin.html
2025-08-26 20:55:08 +01:00

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}}