Collapse migrations, require email at registration
No deployed environments yet, so consolidate the entire migration history into a single Initial migration and tighten the schema accordingly. - User.Email is now non-nullable (required); the partial unique index used to tolerate legacy null emails is gone in favor of a plain unique index. - Both register paths require an email up front. Invite-path registrations must match the invited address (server ignores any mismatched client value); family-code registrations bind whatever the user supplies but stay unconfirmed (EmailConfirmedAt = null) since the family code does not prove email ownership. - Validation order in /register reworked: invite/family-code resolution runs before duplicate-name/email checks so a consumed token surfaces a clean "invitation invalid" error instead of getting masked by the duplicate-email response. - All 14 prior migrations replaced with a single Initial migration. - Test fixtures, builders, and unit tests updated to supply emails. - Login page register form now collects an email field; invite-bound registrations show the invited address as a read-only input. Local dev DBs need to be recreated (drop the yeschef-pgdata volume or the yeschef Postgres database). No production data exists yet.
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
let mode = $state<'login' | 'register'>('login');
|
||||
let name = $state('');
|
||||
let password = $state('');
|
||||
let email = $state('');
|
||||
let familyCode = $state('');
|
||||
let error = $state('');
|
||||
let loading = $state(false);
|
||||
@@ -29,6 +30,7 @@
|
||||
inviteLoading = true;
|
||||
try {
|
||||
invite = await api<InviteLookup>(`/api/auth/invite/${encodeURIComponent(token)}`);
|
||||
email = invite.email;
|
||||
} catch {
|
||||
// Surface a clear message rather than the generic API error — the
|
||||
// recipient probably arrived from a stale or revoked email link.
|
||||
@@ -49,9 +51,9 @@
|
||||
if (mode === 'login') {
|
||||
body = { name, password };
|
||||
} else if (inviteToken) {
|
||||
body = { name, password, inviteToken };
|
||||
body = { name, password, email, inviteToken };
|
||||
} else {
|
||||
body = { name, password, familyCode };
|
||||
body = { name, password, email, familyCode };
|
||||
}
|
||||
|
||||
const res = await api<{ token: string }>(endpoint, {
|
||||
@@ -111,16 +113,28 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if mode === 'register' && !inviteToken}
|
||||
{#if mode === 'register'}
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={familyCode}
|
||||
placeholder="Family code"
|
||||
type="email"
|
||||
bind:value={email}
|
||||
placeholder="Email"
|
||||
required
|
||||
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none"
|
||||
readonly={!!inviteToken}
|
||||
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none {inviteToken ? 'bg-gray-100' : ''}"
|
||||
/>
|
||||
</div>
|
||||
{#if !inviteToken}
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={familyCode}
|
||||
placeholder="Family code"
|
||||
required
|
||||
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
|
||||
Reference in New Issue
Block a user