Files
study-bible-project/src/components/ResetPasswordForm.jsx
T
nmemmertandClaude Sonnet 5.5 f274bc9c8a
Build and Push Docker Image / build-and-push (push) Successful in 7m14s
Harden sync and security; add IQ Bible, templates, password reset, backups
Sync and data safety
- Autosave flushes on leaving a study or the tab; storage-full errors are reported
- Unsynced studies, deletes and ink are queued and retried when back online
- Deletes propagate across devices (with a "Keep it" undo) and no longer resurrect
- Server assigns study versions (no client-clock conflicts); "Combine both"
  merges a conflicted study chunk by chunk
- Reading-plan progress made offline is merged, not overwritten; plans, ink and
  marks are cleared from the device on sign-out

Security
- Block SSRF through the ntfy reminder URL; failure-only rate limiting per IP and
  account; constant-time login; TOTP replay protection; input validation;
  SESSION_SECRET required in production; admin user delete fixed
- Forgot password: one-time code sent to the account's ntfy topic

Features
- IQ Bible proxy (route allow-list, text-only caching per its terms): more
  commentaries, lexicons, study aids, daily verse and devotional, plan calendar
  export, narrations, podcast background
- Study templates, reading-plan catch-up, account export, daily database snapshots

Tests and tooling
- ESLint config; App tests split into src/test; tests for sync queues, library
  hook, merge, templates, IQ Bible UI and reset form

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 08:13:36 -04:00

83 lines
4.8 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// "Forgot password": ask for a code (it is sent to the account's notification topic), then choose a new
// password with it. Shown on the sign-in page in place of the sign-in form.
import { useState } from 'react';
import { requestPasswordReset, resetPassword } from '../syncService.js';
const input = 'mt-1 block w-full rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2.5 text-sm text-[var(--r-text)]';
const primary = 'w-full rounded-full bg-[var(--r-accent)] px-4 py-2.5 text-sm font-medium text-[var(--r-on)] transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50';
export default function ResetPasswordForm({ initialEmail = '', onDone, onBack }) {
const [step, setStep] = useState('ask'); // 'ask' | 'code' | 'done'
const [email, setEmail] = useState(initialEmail);
const [form, setForm] = useState({ code: '', password: '', token: '' });
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const [busy, setBusy] = useState(false);
const ask = async (e) => {
e.preventDefault();
setError(''); setBusy(true);
const result = await requestPasswordReset(email.trim());
setBusy(false);
if (!result.ok) { setError(result.error ?? 'Something went wrong.'); return; }
setMessage(result.data?.message ?? 'If that account has a notification topic, a code was sent.');
setStep('code');
};
const reset = async (e) => {
e.preventDefault();
setError(''); setBusy(true);
const result = await resetPassword({ email: email.trim(), code: form.code, newPassword: form.password, token: form.token.trim() || undefined });
setBusy(false);
if (!result.ok) { setError(result.error ?? 'Something went wrong.'); return; }
setStep('done');
};
if (step === 'done') {
return (
<div>
<h2 className="text-xl font-medium">Password changed</h2>
<p className="mt-2 text-sm text-[var(--r-sub)]">You’ve been signed out everywhere. Sign in with your new password.</p>
<button type="button" onClick={onDone} className={`mt-6 ${primary}`}>Back to sign in</button>
</div>
);
}
return (
<div>
<h2 className="text-xl font-medium">Reset your password</h2>
{step === 'ask' ? (
<form onSubmit={ask} className="mt-4 space-y-4">
<p className="text-sm text-[var(--r-sub)]">We’ll send a one-time code to the notification topic you set up for reading reminders (ntfy).</p>
<label className="block text-sm">Email
<input type="email" required autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} className={input} />
</label>
{error && <p role="alert" className="text-sm text-[var(--r-danger,#b42318)]">{error}</p>}
<button type="submit" disabled={busy} className={primary}>{busy ? 'Please wait…' : 'Send me a code'}</button>
<button type="button" onClick={() => { setStep('code'); setMessage(''); }} className="block w-full text-center text-sm text-[var(--r-sub)] underline hover:text-[var(--r-text)]">I already have a code</button>
</form>
) : (
<form onSubmit={reset} className="mt-4 space-y-4">
{message && <p role="status" className="text-sm text-[var(--r-sub)]">{message}</p>}
<label className="block text-sm">Email
<input type="email" required value={email} onChange={(e) => setEmail(e.target.value)} className={input} />
</label>
<label className="block text-sm">Code
<input type="text" required autoFocus autoComplete="one-time-code" value={form.code} onChange={(e) => setForm((f) => ({ ...f, code: e.target.value }))} className={`${input} text-center font-mono tracking-widest`} placeholder="ABCD2345" />
</label>
<label className="block text-sm">New password
<input type="password" required minLength={8} autoComplete="new-password" value={form.password} onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))} className={input} />
<span className="mt-1 block text-xs text-[var(--r-sub)]">At least 8 characters.</span>
</label>
<label className="block text-sm">Authenticator code <span className="text-xs text-[var(--r-sub)]">(only if you use two-factor sign-in)</span>
<input type="text" inputMode="numeric" value={form.token} onChange={(e) => setForm((f) => ({ ...f, token: e.target.value }))} className={input} placeholder="123456" />
</label>
{error && <p role="alert" className="text-sm text-[var(--r-danger,#b42318)]">{error}</p>}
<button type="submit" disabled={busy} className={primary}>{busy ? 'Please wait…' : 'Change password'}</button>
</form>
)}
<button type="button" onClick={onBack} className="mt-4 w-full text-center text-sm text-[var(--r-sub)] underline hover:text-[var(--r-text)]">Back to sign in</button>
</div>
);
}