Initial LinkLog implementation

This commit is contained in:
Olaf
2026-08-24 14:30:30 +02:00
commit 1c827956c4
50 changed files with 3436 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
(() => {
const pluginList = document.querySelector('#plugin-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 adminLoginButton = document.querySelector('#admin-login-button');
const adminLogoutButton = document.querySelector('#logout-button');
const accessToken = localStorage.getItem('linklogAccessToken');
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 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;
privilegeCheckbox.setAttribute('aria-label', `Administrator rights for ${user.username}`);
privilegeCheckbox.addEventListener('change', () => updateUserPrivilege(user, privilegeCheckbox));
privilegeLabel.append(privilegeCheckbox, document.createTextNode(' Administrator'));
const button = document.createElement('button');
button.type = 'button';
button.className = 'danger-button';
button.textContent = 'Remove';
button.addEventListener('click', () => removeUser(user, button));
row.append(label, privilegeLabel, 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);
adminLoginButton.classList.toggle('hidden', isAdmin);
adminLogoutButton.classList.toggle('hidden', !accessToken);
}
function showSignedOutState() {
showAdminState(false);
adminAuthNotice.innerHTML = 'Administrator sign-in required. <a href="/login">Sign in</a>';
adminLoginButton.classList.remove('hidden');
adminLogoutButton.classList.add('hidden');
}
function showUnauthorizedState() {
showAdminState(false);
adminAuthNotice.textContent = 'You are signed in, but you are not authorized to access this page.';
adminLoginButton.classList.add('hidden');
adminLogoutButton.classList.remove('hidden');
}
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();
if (!user.is_admin) {
showUnauthorizedState();
return;
}
await Promise.all([loadUsers(), loadPlugins()]);
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 response = await fetch(`/api/admin/users/${encodeURIComponent(user.id)}`, {
method: 'DELETE',
headers: authHeaders(),
});
if (response.ok) {
await loadUsers();
} else {
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 = '';
});
})();