Initial config with email service test
Build LinkLog Development Image / development-image (push) Successful in 11s
Build LinkLog Development Image / development-image (push) Successful in 11s
This commit is contained in:
@@ -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
@@ -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();
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 <no-reply@example.com>" 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>
|
||||
|
||||
Reference in New Issue
Block a user