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:
Josh Rogers
2026-05-08 22:58:27 -05:00
parent af085cfb90
commit 09bec105f6
27 changed files with 608 additions and 3574 deletions
+21 -7
View File
@@ -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}