This commit is contained in:
+111
-11
@@ -8,6 +8,11 @@ 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 smtpForm = document.querySelector('#smtp-form');
|
||||
const smtpTestButton = document.querySelector('#smtp-test-button');
|
||||
const smtpStatus = document.querySelector('#smtp-status');
|
||||
let smtpNextAllowedAt = null;
|
||||
let smtpTimerHandle = null;
|
||||
const accessToken = localStorage.getItem('linklogAccessToken');
|
||||
let currentUserId = null;
|
||||
|
||||
@@ -18,6 +23,15 @@ function authHeaders(includeJson = false) {
|
||||
};
|
||||
}
|
||||
|
||||
async function responseError(response, fallback) {
|
||||
try {
|
||||
const result = await response.json();
|
||||
return result.detail || result.message || fallback;
|
||||
} catch (error) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function renderPlugins(plugins) {
|
||||
pluginList.replaceChildren(...plugins.map((plugin) => {
|
||||
const row = document.createElement('div');
|
||||
@@ -61,6 +75,44 @@ async function loadLabels() {
|
||||
renderLabels(await response.json());
|
||||
}
|
||||
|
||||
function showSmtpStatus(message, isError = false) {
|
||||
smtpStatus.textContent = message;
|
||||
smtpStatus.style.color = isError ? '#f38ba8' : '#94e2d5';
|
||||
}
|
||||
|
||||
function updateSmtpTimer() {
|
||||
if (smtpTimerHandle) window.clearTimeout(smtpTimerHandle);
|
||||
if (!smtpNextAllowedAt) {
|
||||
smtpTestButton.disabled = false;
|
||||
return;
|
||||
}
|
||||
const seconds = Math.max(0, Math.ceil((smtpNextAllowedAt - Date.now()) / 1000));
|
||||
if (seconds === 0) {
|
||||
smtpNextAllowedAt = null;
|
||||
updateSmtpTimer();
|
||||
return;
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
showSmtpStatus(`Next validation email available in ${minutes ? `${minutes}m ` : ''}${seconds % 60}s.`);
|
||||
smtpTestButton.disabled = true;
|
||||
smtpTimerHandle = window.setTimeout(updateSmtpTimer, 1000);
|
||||
}
|
||||
|
||||
async function loadSmtpSettings() {
|
||||
const response = await fetch('/api/admin/smtp', {headers: authHeaders()});
|
||||
if (!response.ok) throw new Error('Could not load SMTP settings');
|
||||
const settings = await response.json();
|
||||
for (const [name, value] of Object.entries(settings)) {
|
||||
const field = smtpForm.elements[name];
|
||||
if (!field || name === 'password_configured') continue;
|
||||
if (field.type === 'checkbox') {
|
||||
field.checked = value;
|
||||
} else {
|
||||
field.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderUsers(users) {
|
||||
userList.replaceChildren(...users.map((user) => {
|
||||
const row = document.createElement('div');
|
||||
@@ -133,7 +185,7 @@ async function loadAdminState() {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([loadUsers(), loadPlugins(), loadLabels()]);
|
||||
await Promise.all([loadUsers(), loadPlugins(), loadLabels(), loadSmtpSettings()]);
|
||||
showAdminState(true);
|
||||
}
|
||||
|
||||
@@ -199,20 +251,67 @@ 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();
|
||||
try {
|
||||
const response = await fetch('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: authHeaders(true),
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
status.textContent = response.ok ? 'User added.' : await responseError(response, 'Could not add user.');
|
||||
status.style.color = response.ok ? '#94e2d5' : '#f38ba8';
|
||||
if (response.ok) {
|
||||
userForm.reset();
|
||||
await loadUsers();
|
||||
}
|
||||
} catch (error) {
|
||||
status.textContent = `Could not add user: ${error.message}`;
|
||||
status.style.color = '#f38ba8';
|
||||
}
|
||||
});
|
||||
|
||||
smtpForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const values = Object.fromEntries(new FormData(smtpForm));
|
||||
values.smtp_port = Number(values.smtp_port);
|
||||
values.smtp_use_tls = smtpForm.elements.smtp_use_tls.checked;
|
||||
try {
|
||||
const response = await fetch('/api/admin/smtp', {
|
||||
method: 'PUT',
|
||||
headers: authHeaders(true),
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
showSmtpStatus(response.ok ? 'SMTP settings saved.' : await responseError(response, 'Could not save SMTP settings.'), !response.ok);
|
||||
if (response.ok) smtpForm.elements.smtp_password.value = '';
|
||||
} catch (error) {
|
||||
showSmtpStatus(`Could not save SMTP settings: ${error.message}`, true);
|
||||
}
|
||||
});
|
||||
|
||||
smtpTestButton.addEventListener('click', async () => {
|
||||
smtpTestButton.disabled = true;
|
||||
const values = Object.fromEntries(new FormData(smtpForm));
|
||||
values.smtp_port = Number(values.smtp_port);
|
||||
values.smtp_use_tls = smtpForm.elements.smtp_use_tls.checked;
|
||||
try {
|
||||
const response = await fetch('/api/admin/smtp/test', {
|
||||
method: 'POST',
|
||||
headers: authHeaders(true),
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
const result = response.ok ? await response.json() : {};
|
||||
showSmtpStatus(response.ok ? result.message : await responseError(response, 'SMTP validation failed.'), !response.ok);
|
||||
if (response.headers.get('Retry-After')) {
|
||||
smtpNextAllowedAt = Date.now() + Number(response.headers.get('Retry-After')) * 1000;
|
||||
} else if (result.next_allowed_at) {
|
||||
smtpNextAllowedAt = Date.parse(result.next_allowed_at);
|
||||
}
|
||||
} catch (error) {
|
||||
showSmtpStatus(`SMTP validation failed: ${error.message}`, true);
|
||||
}
|
||||
updateSmtpTimer();
|
||||
});
|
||||
|
||||
loadAdminState().catch((error) => {
|
||||
showAdminState(false);
|
||||
adminAuthNotice.textContent = accessToken
|
||||
@@ -222,5 +321,6 @@ loadAdminState().catch((error) => {
|
||||
userList.textContent = '';
|
||||
pluginList.textContent = '';
|
||||
adminLabelList.textContent = '';
|
||||
smtpForm.reset();
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -72,6 +72,38 @@
|
||||
<h2>Labels</h2>
|
||||
<div id="admin-label-list" class="plugin-list" aria-live="polite">Loading labels...</div>
|
||||
</section>
|
||||
<section class="link-item settings-panel">
|
||||
<h2>SMTP settings</h2>
|
||||
<form id="smtp-form">
|
||||
<label>
|
||||
SMTP host
|
||||
<input name="smtp_host" type="text" required />
|
||||
</label>
|
||||
<label>
|
||||
SMTP port
|
||||
<input name="smtp_port" type="number" min="1" max="65535" required />
|
||||
</label>
|
||||
<label>
|
||||
SMTP username
|
||||
<input name="smtp_username" type="text" autocomplete="off" />
|
||||
</label>
|
||||
<label>
|
||||
SMTP password
|
||||
<input name="smtp_password" type="password" autocomplete="new-password" />
|
||||
</label>
|
||||
<label>
|
||||
From address
|
||||
<input name="smtp_from" type="text" required />
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input name="smtp_use_tls" type="checkbox" /> Use STARTTLS
|
||||
</label>
|
||||
<button id="smtp-test-button" type="button">Send validation email</button>
|
||||
<p id="smtp-status" class="status" role="status"></p>
|
||||
<button type="submit">Save SMTP settings</button>
|
||||
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="site-footer">Copyright © 2026 Olaf Kolkman · <a href="https://git.kolkman.org/olaf/Link-Log">git.kolkman.org/LinkLog</a></footer>
|
||||
|
||||
Reference in New Issue
Block a user