email in admin
Build LinkLog Development Image / development-image (push) Successful in 9s

This commit is contained in:
2026-08-26 12:38:38 +02:00
parent 158bc64268
commit fec4c8def5
7 changed files with 337 additions and 16 deletions
+111 -11
View File
@@ -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();
});
})();
+32
View File
@@ -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>