Login recovery (UI): Settings setup + "Forgot password?" on the login screen
- Settings → Security: a recovery-question picker (5 presets + Custom) + answer field + Save, posting to /api/profiles/1/set-recovery. handleRecoveryQuestionChange reveals the custom box. - Login screen: a "Forgot password?" link opens a recovery view — enter username → fetch your question → answer + new password → reset → reload signed in. Reuses the launch-PIN overlay styling/structure (entry + recovery views). All inert unless login mode is on, so a default/LAN install never sees any of it. 64 script-split integrity tests pass (every new handler resolves).
This commit is contained in:
parent
613688a9ad
commit
5c80ee1010
2 changed files with 138 additions and 7 deletions
|
|
@ -54,13 +54,33 @@
|
|||
<!-- Login Screen (username/password mode) -->
|
||||
<div id="login-overlay" class="launch-pin-overlay" style="display: none;">
|
||||
<div class="launch-pin-container">
|
||||
<div class="launch-pin-icon">🔐</div>
|
||||
<h2 class="launch-pin-title">Sign in to SoulSync</h2>
|
||||
<p class="launch-pin-subtitle">Enter your account name and password</p>
|
||||
<input type="text" id="login-username" class="launch-pin-input" placeholder="Username" autocomplete="username" maxlength="40" style="margin-bottom: 8px; letter-spacing: normal;">
|
||||
<input type="password" id="login-password" class="launch-pin-input" placeholder="Password" autocomplete="current-password" maxlength="200" style="letter-spacing: normal;" onkeydown="if(event.key==='Enter') submitLogin()">
|
||||
<button id="login-submit" class="launch-pin-submit" onclick="submitLogin()">Sign in</button>
|
||||
<p id="login-error" class="launch-pin-error" style="display: none;"></p>
|
||||
<!-- Sign-in view -->
|
||||
<div id="login-entry">
|
||||
<div class="launch-pin-icon">🔐</div>
|
||||
<h2 class="launch-pin-title">Sign in to SoulSync</h2>
|
||||
<p class="launch-pin-subtitle">Enter your account name and password</p>
|
||||
<input type="text" id="login-username" class="launch-pin-input" placeholder="Username" autocomplete="username" maxlength="40" style="margin-bottom: 8px; letter-spacing: normal;">
|
||||
<input type="password" id="login-password" class="launch-pin-input" placeholder="Password" autocomplete="current-password" maxlength="200" style="letter-spacing: normal;" onkeydown="if(event.key==='Enter') submitLogin()">
|
||||
<button id="login-submit" class="launch-pin-submit" onclick="submitLogin()">Sign in</button>
|
||||
<p id="login-error" class="launch-pin-error" style="display: none;"></p>
|
||||
<button class="launch-pin-forgot" onclick="showLoginRecovery()">Forgot password?</button>
|
||||
</div>
|
||||
<!-- Recovery view -->
|
||||
<div id="login-recovery" style="display: none;">
|
||||
<div class="launch-pin-icon">🔑</div>
|
||||
<h2 class="launch-pin-title">Reset your password</h2>
|
||||
<p class="launch-pin-subtitle">Answer your recovery question</p>
|
||||
<input type="text" id="recovery-username" class="launch-pin-input" placeholder="Username" autocomplete="username" maxlength="40" style="margin-bottom: 8px; letter-spacing: normal;">
|
||||
<button id="recovery-fetch-btn" class="launch-pin-submit" onclick="fetchRecoveryQuestion()">Continue</button>
|
||||
<div id="recovery-answer-section" style="display: none;">
|
||||
<p id="recovery-question-text" class="launch-pin-subtitle" style="margin-top: 12px; font-weight: 600;"></p>
|
||||
<input type="text" id="recovery-answer" class="launch-pin-input" placeholder="Your answer" autocomplete="off" maxlength="120" style="margin-bottom: 8px; letter-spacing: normal;">
|
||||
<input type="password" id="recovery-new-password" class="launch-pin-input" placeholder="New password (min 6)" autocomplete="new-password" maxlength="200" style="letter-spacing: normal;" onkeydown="if(event.key==='Enter') submitRecoveryReset()">
|
||||
<button class="launch-pin-submit" onclick="submitRecoveryReset()">Reset password</button>
|
||||
</div>
|
||||
<p id="recovery-error" class="launch-pin-error" style="display: none;"></p>
|
||||
<button class="launch-pin-forgot" onclick="showLoginEntry()">← Back to sign in</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -6040,6 +6060,26 @@
|
|||
<p id="security-login-password-msg" class="setting-help-text" style="margin-top: 6px; display: none;"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="security-recovery-question">Password recovery question:</label>
|
||||
<div class="setting-help-text" style="margin-bottom: 8px;">
|
||||
Lets you reset a forgotten login password by answering this. Recommended if you enable login. (You can set one per profile in <strong>Manage Profiles</strong> too.)
|
||||
</div>
|
||||
<select id="security-recovery-question" class="form-select" onchange="handleRecoveryQuestionChange()" style="margin-bottom: 6px;">
|
||||
<option value="">— Select a question —</option>
|
||||
<option>What was the name of your first pet?</option>
|
||||
<option>What city were you born in?</option>
|
||||
<option>What was your first concert?</option>
|
||||
<option>What is your all-time favorite album?</option>
|
||||
<option>What was the make of your first car?</option>
|
||||
<option value="__custom__">Custom question…</option>
|
||||
</select>
|
||||
<input type="text" id="security-recovery-custom" placeholder="Type your own question" maxlength="120" style="display:none; margin-bottom: 6px;">
|
||||
<input type="text" id="security-recovery-answer" placeholder="Your answer" maxlength="120" autocomplete="off" style="margin-bottom: 6px;">
|
||||
<button class="auth-button" onclick="saveRecoveryQuestion()">Save Recovery Question</button>
|
||||
<p id="security-recovery-msg" class="setting-help-text" style="margin-top: 6px; display: none;"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="security-require-login">
|
||||
|
|
|
|||
|
|
@ -441,6 +441,65 @@ async function soulsyncLogout() {
|
|||
window.location.reload();
|
||||
}
|
||||
|
||||
function showLoginRecovery() {
|
||||
const entry = document.getElementById('login-entry');
|
||||
const rec = document.getElementById('login-recovery');
|
||||
if (entry) entry.style.display = 'none';
|
||||
if (rec) rec.style.display = 'block';
|
||||
const u = document.getElementById('recovery-username');
|
||||
const lu = document.getElementById('login-username');
|
||||
if (u && lu && lu.value) u.value = lu.value;
|
||||
const errEl = document.getElementById('recovery-error');
|
||||
if (errEl) errEl.style.display = 'none';
|
||||
}
|
||||
|
||||
function showLoginEntry() {
|
||||
const entry = document.getElementById('login-entry');
|
||||
const rec = document.getElementById('login-recovery');
|
||||
if (rec) rec.style.display = 'none';
|
||||
if (entry) entry.style.display = 'block';
|
||||
}
|
||||
|
||||
async function fetchRecoveryQuestion() {
|
||||
const username = (document.getElementById('recovery-username')?.value || '').trim();
|
||||
const errEl = document.getElementById('recovery-error');
|
||||
const section = document.getElementById('recovery-answer-section');
|
||||
const qText = document.getElementById('recovery-question-text');
|
||||
const showErr = (m) => { if (errEl) { errEl.textContent = m; errEl.style.display = 'block'; } };
|
||||
if (errEl) errEl.style.display = 'none';
|
||||
if (!username) { showErr('Enter your username'); return; }
|
||||
try {
|
||||
const res = await fetch('/api/auth/recovery-question?username=' + encodeURIComponent(username));
|
||||
const data = await res.json();
|
||||
if (data.success && data.question) {
|
||||
if (qText) qText.textContent = data.question;
|
||||
if (section) section.style.display = 'block';
|
||||
} else {
|
||||
showErr('No recovery question is set for that account.');
|
||||
}
|
||||
} catch (e) { showErr('Connection error'); }
|
||||
}
|
||||
|
||||
async function submitRecoveryReset() {
|
||||
const username = (document.getElementById('recovery-username')?.value || '').trim();
|
||||
const answer = document.getElementById('recovery-answer')?.value || '';
|
||||
const newPassword = document.getElementById('recovery-new-password')?.value || '';
|
||||
const errEl = document.getElementById('recovery-error');
|
||||
const showErr = (m) => { if (errEl) { errEl.textContent = m; errEl.style.display = 'block'; } };
|
||||
if (errEl) errEl.style.display = 'none';
|
||||
if (!answer || !newPassword) { showErr('Enter your answer and a new password'); return; }
|
||||
if (newPassword.length < 6) { showErr('New password must be at least 6 characters'); return; }
|
||||
try {
|
||||
const res = await fetch('/api/auth/recovery-reset', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, answer, new_password: newPassword }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) { window.location.reload(); }
|
||||
else { showErr(res.status === 429 ? 'Too many attempts — wait a moment.' : (data.error || 'Reset failed')); }
|
||||
} catch (e) { showErr('Connection error'); }
|
||||
}
|
||||
|
||||
// ── Launch PIN Lock Screen ─────────────────────────────────────────────
|
||||
|
||||
function showLaunchPinScreen() {
|
||||
|
|
@ -527,6 +586,38 @@ async function saveLoginPassword() {
|
|||
} catch (e) { show('Connection error', false); }
|
||||
}
|
||||
|
||||
function handleRecoveryQuestionChange() {
|
||||
const sel = document.getElementById('security-recovery-question');
|
||||
const custom = document.getElementById('security-recovery-custom');
|
||||
if (sel && custom) custom.style.display = (sel.value === '__custom__') ? 'block' : 'none';
|
||||
}
|
||||
|
||||
async function saveRecoveryQuestion() {
|
||||
const sel = document.getElementById('security-recovery-question');
|
||||
const custom = document.getElementById('security-recovery-custom');
|
||||
const answer = document.getElementById('security-recovery-answer')?.value || '';
|
||||
const msg = document.getElementById('security-recovery-msg');
|
||||
const show = (text, ok) => {
|
||||
if (!msg) return;
|
||||
msg.textContent = text;
|
||||
msg.style.color = ok ? '#4caf50' : '#ff5252';
|
||||
msg.style.display = 'block';
|
||||
};
|
||||
let question = sel?.value || '';
|
||||
if (question === '__custom__') question = (custom?.value || '').trim();
|
||||
if (!question) { show('Pick or type a question', false); return; }
|
||||
if (!answer.trim()) { show('Enter an answer', false); return; }
|
||||
try {
|
||||
const res = await fetch('/api/profiles/1/set-recovery', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ question, answer }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) { show('Recovery question saved', true); const a = document.getElementById('security-recovery-answer'); if (a) a.value = ''; }
|
||||
else show(data.error || 'Failed to save', false);
|
||||
} catch (e) { show('Connection error', false); }
|
||||
}
|
||||
|
||||
async function saveSecurityPin() {
|
||||
const pin = document.getElementById('security-new-pin').value;
|
||||
const confirm = document.getElementById('security-confirm-pin').value;
|
||||
|
|
|
|||
Loading…
Reference in a new issue