Initial config with email service test
Build LinkLog Development Image / development-image (push) Successful in 11s

This commit is contained in:
Olaf
2026-08-25 23:23:28 +02:00
parent defe7a83a9
commit 102d8e533c
20 changed files with 482 additions and 34 deletions
+19
View File
@@ -0,0 +1,19 @@
// Copyright © 2026 Olaf Kolkman
// SPDX-License-Identifier: GPL-3.0-or-later
const resetForm = document.querySelector('#reset-password-form');
const resetStatus = document.querySelector('#reset-password-status');
const resetToken = new URLSearchParams(window.location.search).get('token');
resetForm.addEventListener('submit', async (event) => {
event.preventDefault();
const response = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({token: resetToken, password: new FormData(resetForm).get('password')}),
});
const result = await response.json();
resetStatus.textContent = response.ok ? `${result.message} Redirecting...` : (result.detail || 'Reset failed.');
resetStatus.style.color = response.ok ? '#166534' : '#b91c1c';
if (response.ok) window.setTimeout(() => window.location.assign('/login'), 1000);
});
+106 -9
View File
@@ -1,26 +1,123 @@
// Copyright © 2026 Olaf Kolkman
// SPDX-License-Identifier: GPL-3.0-or-later
document.querySelector('#setup-form').addEventListener('submit', async (event) => {
const setupForm = document.querySelector('#setup-form');
const testMailButton = document.querySelector('#test-mail-button');
const completeSetupButton = document.querySelector('#complete-setup-button');
const setupStatus = document.querySelector('#setup-status');
const setupTimer = document.querySelector('#setup-timer');
let nextAllowedAt = null;
let timerHandle = null;
function updateTimer() {
if (timerHandle) window.clearTimeout(timerHandle);
if (!nextAllowedAt) {
setupTimer.textContent = '';
testMailButton.disabled = !setupForm.dataset.saved;
return;
}
const seconds = Math.max(0, Math.ceil((nextAllowedAt - Date.now()) / 1000));
if (seconds === 0) {
nextAllowedAt = null;
updateTimer();
return;
}
const minutes = Math.floor(seconds / 60);
setupTimer.textContent = `Next test mail available in ${minutes ? `${minutes}m ` : ''}${seconds % 60}s.`;
testMailButton.disabled = true;
timerHandle = window.setTimeout(updateTimer, 1000);
}
function applyStatus(result) {
setupForm.dataset.saved = result.pending ? 'true' : '';
testMailButton.disabled = !result.pending;
const smtpDefaults = result.smtp_defaults || {};
for (const [name, value] of Object.entries(smtpDefaults)) {
const field = setupForm.elements[name];
if (!field || field.value || field.type === 'checkbox') continue;
field.value = value;
}
if (typeof smtpDefaults.smtp_use_tls === 'boolean') {
setupForm.elements.smtp_use_tls.checked = smtpDefaults.smtp_use_tls;
}
if (result.next_allowed_at) nextAllowedAt = Date.parse(result.next_allowed_at);
updateTimer();
}
async function loadSetupStatus() {
const response = await fetch('/api/setup/status');
if (!response.ok) return;
applyStatus(await response.json());
}
setupForm.addEventListener('submit', async (event) => {
event.preventDefault();
const form = event.currentTarget;
const status = document.querySelector('#setup-status');
const values = Object.fromEntries(new FormData(form));
values.smtp_port = Number(values.smtp_port);
values.smtp_use_tls = form.elements.smtp_use_tls.checked;
try {
const response = await fetch('/api/setup', {
const response = await fetch('/api/setup/configuration', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(values),
});
const result = await response.json();
if (!response.ok) throw new Error(result.detail || 'Setup failed');
status.textContent = result.message;
status.style.color = '#94e2d5';
form.replaceChildren(status);
setupStatus.textContent = result.message;
setupStatus.style.color = '#94e2d5';
form.dataset.saved = 'true';
testMailButton.disabled = false;
} catch (error) {
status.textContent = error.message;
status.style.color = '#f38ba8';
setupStatus.textContent = error.message;
setupStatus.style.color = '#f38ba8';
}
});
});
testMailButton.addEventListener('click', async () => {
testMailButton.disabled = true;
try {
const response = await fetch('/api/setup/test-mail', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: setupForm.elements.email.value}),
});
const result = await response.json();
if (!response.ok) {
if (response.status === 429 && response.headers.get('Retry-After')) {
nextAllowedAt = Date.now() + Number(response.headers.get('Retry-After')) * 1000;
updateTimer();
}
throw new Error(result.detail || 'Test mail failed');
}
setupStatus.textContent = `${result.message} ${result.sends_remaining} sends remaining.`;
setupStatus.style.color = '#94e2d5';
if (result.next_allowed_at) nextAllowedAt = Date.parse(result.next_allowed_at);
updateTimer();
completeSetupButton.hidden = false;
} catch (error) {
setupStatus.textContent = error.message;
setupStatus.style.color = '#f38ba8';
if (!nextAllowedAt) testMailButton.disabled = false;
}
});
completeSetupButton.addEventListener('click', async () => {
const response = await fetch('/api/setup/complete', {method: 'POST'});
const result = await response.json();
if (!response.ok) {
setupStatus.textContent = result.detail || 'Could not complete setup.';
setupStatus.style.color = '#f38ba8';
return;
}
setupStatus.style.color = '#94e2d5';
setupStatus.textContent = '';
setupStatus.append(document.createTextNode(`${result.message} `));
const homeLink = document.createElement('a');
homeLink.href = '/';
homeLink.textContent = 'Home';
setupStatus.append(homeLink);
setupForm.replaceChildren(setupStatus);
});
loadSetupStatus();
+1
View File
@@ -47,6 +47,7 @@
</label>
<button type="submit">Sign in</button>
<p id="login-status" class="status" role="status"></p>
<p><small>A mistyped password sends a reset link to your verified email address.</small></p>
</form>
</section>
</main>
+24
View File
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<!-- Copyright © 2026 Olaf Kolkman -->
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Reset password - LinkLog</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<main class="container">
<section class="link-item settings-panel">
<h1>Reset password</h1>
<form id="reset-password-form">
<label>New password<input name="password" type="password" minlength="8" autocomplete="new-password" required /></label>
<button type="submit">Reset password</button>
<p id="reset-password-status" class="status" role="status"></p>
</form>
</section>
</main>
<script src="/static/reset-password.js"></script>
</body>
</html>
+7 -4
View File
@@ -29,13 +29,16 @@
<label>Password<input name="password" type="password" autocomplete="new-password" minlength="8" required /></label>
<h2>SMTP</h2>
<label>SMTP host<input name="smtp_host" type="text" required /></label>
<label>SMTP port<input name="smtp_port" type="number" min="1" max="65535" value="587" 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" value="LinkLog &lt;no-reply@example.com&gt;" required /></label>
<label class="checkbox-label"><input name="smtp_use_tls" type="checkbox" checked /> Use STARTTLS</label>
<button type="submit">Save and send test mail</button>
<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 type="submit">Save configuration</button>
<button id="test-mail-button" type="button" disabled>Send test mail</button>
<button id="complete-setup-button" type="button" hidden>Complete setup</button>
<p id="setup-status" class="status" role="status"></p>
<p id="setup-timer" class="status" role="timer" aria-live="polite"></p>
</form>
</section>
</main>