Files
Link-Log/frontend/static/admin.js
T
2026-08-25 21:05:17 +02:00

227 lines
7.4 KiB
JavaScript

// Copyright © 2026 Olaf Kolkman
// SPDX-License-Identifier: GPL-3.0-or-later
(() => {
const pluginList = document.querySelector('#plugin-list');
const adminLabelList = document.querySelector('#admin-label-list');
const userList = document.querySelector('#user-list');
const userForm = document.querySelector('#user-form');
const adminControls = document.querySelector('#admin-controls');
const adminAuthNotice = document.querySelector('#admin-auth-notice');
const accessToken = localStorage.getItem('linklogAccessToken');
let currentUserId = null;
function authHeaders(includeJson = false) {
return {
...(includeJson ? {'Content-Type': 'application/json'} : {}),
...(accessToken ? {Authorization: `Bearer ${accessToken}`} : {}),
};
}
function renderPlugins(plugins) {
pluginList.replaceChildren(...plugins.map((plugin) => {
const row = document.createElement('div');
row.className = 'plugin-row';
const label = document.createElement('span');
label.textContent = `${plugin.name} (${plugin.version})`;
const button = document.createElement('button');
button.type = 'button';
button.textContent = plugin.enabled ? 'Disable' : 'Enable';
button.addEventListener('click', () => updatePlugin(plugin, button));
row.append(label, button);
return row;
}));
}
function renderLabels(labels) {
adminLabelList.replaceChildren(...labels.map((label) => {
const row = document.createElement('div');
row.className = 'plugin-row';
const text = document.createElement('span');
text.textContent = `${label.name}${label.creator ? ` (${label.creator})` : ' (default)'}`;
const button = document.createElement('button');
button.type = 'button';
button.className = 'danger-button';
button.textContent = 'Delete';
button.addEventListener('click', async () => {
const response = await fetch(`/api/admin/labels/${label.id}`, {method: 'DELETE', headers: authHeaders()});
if (response.ok) loadLabels();
});
row.append(text, button);
return row;
}));
}
async function loadLabels() {
const response = await fetch('/api/admin/labels', {headers: authHeaders()});
if (!response.ok) throw new Error('Could not load labels');
renderLabels(await response.json());
}
function renderUsers(users) {
userList.replaceChildren(...users.map((user) => {
const row = document.createElement('div');
row.className = 'plugin-row';
const label = document.createElement('span');
label.textContent = `${user.username} (${user.email})${user.is_admin ? ' - admin' : ''}`;
const privilegeLabel = document.createElement('label');
privilegeLabel.className = 'user-admin-toggle';
const privilegeCheckbox = document.createElement('input');
privilegeCheckbox.type = 'checkbox';
privilegeCheckbox.checked = user.is_admin;
const isCurrentUser = user.id === currentUserId;
privilegeCheckbox.disabled = isCurrentUser;
privilegeCheckbox.setAttribute('aria-label', `Administrator rights for ${user.username}`);
if (!isCurrentUser) {
privilegeCheckbox.addEventListener('change', () => updateUserPrivilege(user, privilegeCheckbox));
}
privilegeLabel.append(privilegeCheckbox, document.createTextNode(' Administrator'));
row.append(label, privilegeLabel);
if (!isCurrentUser) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'danger-button';
button.textContent = 'Remove';
button.addEventListener('click', () => removeUser(user, button));
row.append(button);
}
return row;
}));
}
async function loadUsers() {
const response = await fetch('/api/admin/users', {headers: authHeaders()});
if (!response.ok) throw new Error('Could not load users');
renderUsers(await response.json());
}
function showAdminState(isAdmin) {
adminControls.classList.toggle('hidden', !isAdmin);
adminAuthNotice.classList.toggle('hidden', isAdmin);
}
function showSignedOutState() {
showAdminState(false);
adminAuthNotice.innerHTML = 'Administrator sign-in required. <a href="/login">Sign in</a>';
}
function showUnauthorizedState() {
showAdminState(false);
adminAuthNotice.textContent = 'You are signed in, but you are not authorized to access this page.';
}
async function loadAdminState() {
if (!accessToken) {
showSignedOutState();
return;
}
const sessionResponse = await fetch(`/api/auth/me?token=${encodeURIComponent(accessToken)}`);
if (!sessionResponse.ok) {
localStorage.removeItem('linklogAccessToken');
showSignedOutState();
return;
}
const user = await sessionResponse.json();
currentUserId = user.id;
if (!user.is_admin) {
showUnauthorizedState();
return;
}
await Promise.all([loadUsers(), loadPlugins(), loadLabels()]);
showAdminState(true);
}
async function loadPlugins() {
const response = await fetch('/api/admin/plugins', {headers: authHeaders()});
if (!response.ok) throw new Error('Could not load plugins');
renderPlugins(await response.json());
}
async function updatePlugin(plugin, button) {
button.disabled = true;
const response = await fetch(`/api/admin/plugins/${encodeURIComponent(plugin.name)}`, {
method: 'PUT',
headers: authHeaders(true),
body: JSON.stringify({enabled: !plugin.enabled}),
});
if (response.ok) {
await loadPlugins();
} else {
button.disabled = false;
}
}
async function removeUser(user, button) {
if (!window.confirm(`Remove ${user.username}?`)) return;
button.disabled = true;
const status = document.querySelector('#user-status');
try {
const response = await fetch(`/api/admin/users/${encodeURIComponent(user.id)}`, {
method: 'DELETE',
headers: authHeaders(),
});
if (!response.ok) {
const detail = await response.text();
throw new Error(detail || `Request failed (${response.status})`);
}
await loadUsers();
status.textContent = `Removed ${user.username}.`;
status.style.color = '#94e2d5';
} catch (error) {
status.textContent = `Could not remove ${user.username}: ${error.message}`;
status.style.color = '#f38ba8';
button.disabled = false;
}
}
async function updateUserPrivilege(user, checkbox) {
checkbox.disabled = true;
const response = await fetch(`/api/admin/users/${encodeURIComponent(user.id)}`, {
method: 'PUT',
headers: authHeaders(true),
body: JSON.stringify({is_admin: checkbox.checked}),
});
if (response.ok) {
await loadUsers();
} else {
checkbox.checked = user.is_admin;
checkbox.disabled = false;
}
}
userForm.addEventListener('submit', async (event) => {
event.preventDefault();
const values = Object.fromEntries(new FormData(userForm));
values.is_admin = userForm.elements.is_admin.checked;
const response = await fetch('/api/admin/users', {
method: 'POST',
headers: authHeaders(true),
body: JSON.stringify(values),
});
const status = document.querySelector('#user-status');
status.textContent = response.ok ? 'User added.' : 'Could not add user.';
status.style.color = response.ok ? '#94e2d5' : '#f38ba8';
if (response.ok) {
userForm.reset();
await loadUsers();
}
});
loadAdminState().catch((error) => {
showAdminState(false);
adminAuthNotice.textContent = accessToken
? `Could not verify administrator access: ${error.message}`
: 'Administrator sign-in required.';
adminAuthNotice.classList.remove('hidden');
userList.textContent = '';
pluginList.textContent = '';
adminLabelList.textContent = '';
});
})();