Passwordless sign-in: 6-digit email codes, feature-flagged on SendGrid key

/api/auth/email/start issues a 15-min code (60s resend guard, 6 tries);
verify creates the account passwordless (sponsor cookie first-touch) and
mints the session. UI swaps the password cards for the code flow when
config.emailAuth is on; dev mode returns the code inline. Password flow
remains until the key lands in the volume (data/sendgrid.key) or
SENDGRID_KEY env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-04 13:38:11 -05:00
parent 7bb1a78ca3
commit 08f7a408e3
5 changed files with 143 additions and 3 deletions
+29
View File
@@ -120,6 +120,35 @@
finally { btn.disabled = false; }
};
// passwordless (feature-flagged on config.emailAuth): code replaces passwords
(async () => {
const cfg = await IAP.getConfig();
if (!cfg.emailAuth) return;
$('passCards').hidden = true;
$('magicCard').hidden = false;
const start = busy($('mcSendBtn'), async () => {
const r = await api('/api/auth/email/start', { email: $('mcEmail').value });
$('mcCodeRow').hidden = false;
$('mcVerifyBtn').hidden = false;
$('mcSendBtn').hidden = true;
$('mcResend').hidden = false;
if (r.devCode) { $('mcCode').value = r.devCode; IAP.status('Dev mode: code filled in for you.', 'ok'); }
else IAP.status('Code sent. Check your inbox (and spam, the first time).', 'ok');
$('mcCode').focus();
});
$('mcSendBtn').addEventListener('click', start);
$('mcResend').addEventListener('click', busy($('mcResend'), async () => {
const r = await api('/api/auth/email/start', { email: $('mcEmail').value });
if (r.devCode) $('mcCode').value = r.devCode;
IAP.status('Fresh code sent.', 'ok');
}));
$('mcVerifyBtn').addEventListener('click', busy($('mcVerifyBtn'), async () => {
await api('/api/auth/email/verify', { email: $('mcEmail').value, code: $('mcCode').value });
IAP.status('You are in.', 'ok');
await render();
}));
})();
$('signupBtn').addEventListener('click', busy($('signupBtn'), async () => {
await api('/api/signup', { email: $('suEmail').value, password: $('suPass').value });
IAP.status('Welcome aboard. You are in.', 'ok');