Initial LinkLog implementation
This commit is contained in:
@@ -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 = '';
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user