- Real email/password login backed by SQLite (better-sqlite3) - HttpOnly cookie sessions with 8h sliding TTL - Admin role: invite users via Mailgun magic-link, manage roles/status - Per-user One2Edit username mapping for job filtering - Self-service forgot-password / reset-password via email - Admin console (admin.html) with user table, invite modal, row actions - New pages: change-password, forgot-password, reset-password, accept-invite - Gated /api proxy: requires valid session, anti-hijack sessionId check - Bootstrap initial admins from INITIAL_ADMINS env var on first boot - Remove Oliver login button, SSO buttons, and legacy api.js/login.js - deploy.sh: add build-essential (for native module), npm install, data dir Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
78 lines
2.8 KiB
JavaScript
78 lines
2.8 KiB
JavaScript
const token = new URLSearchParams(window.location.search).get('token');
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
const loading = document.getElementById('loading');
|
|
const invalidState = document.getElementById('invalidState');
|
|
const inviteForm = document.getElementById('inviteForm');
|
|
const inviteEmail = document.getElementById('inviteEmail');
|
|
|
|
if (!token) {
|
|
loading.style.display = 'none';
|
|
invalidState.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`/api/auth/invite-info?token=${encodeURIComponent(token)}`);
|
|
const data = await res.json();
|
|
|
|
loading.style.display = 'none';
|
|
if (!data.valid) {
|
|
invalidState.style.display = 'block';
|
|
return;
|
|
}
|
|
inviteEmail.textContent = `Setting up account for: ${data.email}`;
|
|
inviteForm.style.display = 'block';
|
|
} catch {
|
|
loading.style.display = 'none';
|
|
invalidState.style.display = 'block';
|
|
}
|
|
});
|
|
|
|
async function handleAcceptInvite() {
|
|
const newPass = document.getElementById('newPassword').value;
|
|
const confirm = document.getElementById('confirmPassword').value;
|
|
const errorMessage = document.getElementById('errorMessage');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const submitText = document.getElementById('submitText');
|
|
const spinner = document.getElementById('submitSpinner');
|
|
|
|
errorMessage.style.display = 'none';
|
|
|
|
if (newPass !== confirm) {
|
|
errorMessage.textContent = 'Passwords do not match.';
|
|
errorMessage.style.display = 'block';
|
|
return;
|
|
}
|
|
if (newPass.length < 10) {
|
|
errorMessage.textContent = 'Password must be at least 10 characters.';
|
|
errorMessage.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
submitBtn.disabled = true;
|
|
submitText.textContent = 'Creating account...';
|
|
spinner.style.display = 'inline-block';
|
|
|
|
try {
|
|
const res = await fetch('/api/auth/accept-invite', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token, password: newPass }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Failed to accept invitation');
|
|
window.location.href = 'dashboard.html';
|
|
} catch (err) {
|
|
errorMessage.textContent = err.message;
|
|
errorMessage.style.display = 'block';
|
|
submitBtn.disabled = false;
|
|
submitText.textContent = 'Create Account';
|
|
spinner.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
document.getElementById('submitBtn')?.addEventListener('click', handleAcceptInvite);
|
|
document.getElementById('inviteForm')?.addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') handleAcceptInvite();
|
|
});
|