feat(forms): verified-email retro-link of anonymous submissions (Phase 5)
- claimMyAnonymousSubmissions(): exported but safe — no trusted params,
resolves member + email from the caller's session, gated on
email_confirmed_at, idempotent. Links the caller's own prior
anonymous submissions to their member row
- hooked best-effort from joinSpace (member insert now returns id;
links prior anon submissions for the verified email) and
finishOnboarding
- addMember/importMembers deliberately NOT auto-linked: those emails
are admin-asserted, not verified; the locked decision forbids
auto-link there, so the forms.manage linkSubmissionsForMember stays
the admin manual-link path
- also repairs a build break committed in 295d220: finishOnboarding
declared `const admin` twice (Phase 4 added a second). The probe and
the completion update now share one admin client
- docs: API_REFERENCE / ARCHITECTURE / HANDOFF
287/287 tests, build compiles clean (verified Compiled successfully). No deploy.
This commit is contained in:
@@ -417,6 +417,41 @@ export async function getPublicForm(input: unknown) {
|
||||
return { data: form }
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-service retro-link (Phase 5). Links the caller's prior ANONYMOUS
|
||||
* submissions to their own member row, but only when their email is verified
|
||||
* (the locked decision: retro-link to an account only on a verified email).
|
||||
*
|
||||
* Safe to expose: it takes no trusted parameters. It resolves the member and
|
||||
* email strictly from the caller's authenticated session, so a client can
|
||||
* only ever link submissions to their own account, and only their own
|
||||
* verified email. Idempotent (already-linked rows have a non-null member_id
|
||||
* and are excluded). This is the concrete "email-verification" hook; it is
|
||||
* also invoked best-effort from joinSpace and finishOnboarding.
|
||||
*/
|
||||
export async function claimMyAnonymousSubmissions() {
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) return { error: 'Not authenticated' }
|
||||
if (!user.email || !user.email_confirmed_at) return { data: { linked: 0 } }
|
||||
|
||||
const member = await getAuthMember(supabase)
|
||||
if (!member) return { data: { linked: 0 } }
|
||||
|
||||
const admin = createAdminClient()
|
||||
const { data: linked } = await admin
|
||||
.from('form_submissions')
|
||||
.update({ member_id: member.id })
|
||||
.eq('space_id', member.space_id)
|
||||
.is('member_id', null)
|
||||
.eq('submitter_email', user.email.toLowerCase())
|
||||
.select('id')
|
||||
|
||||
return { data: { linked: linked?.length ?? 0 } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Retro-link prior anonymous submissions to a member by matching email.
|
||||
*
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
updateOnboardingStepSchema,
|
||||
uuidSchema,
|
||||
} from '@/lib/validations'
|
||||
import { claimMyAnonymousSubmissions } from './forms'
|
||||
|
||||
const ONBOARDING_ADMIN_ROLES = ['admin', 'board'] as const
|
||||
|
||||
@@ -207,8 +208,7 @@ export async function finishOnboarding() {
|
||||
// onboarding_completed_at is blocked by the self-change trigger (migration
|
||||
// 024) so a member cannot skip required steps via a raw PostgREST call.
|
||||
// Set it via the service client AFTER the required-steps check above, scoped
|
||||
// to this member's own id.
|
||||
const admin = createAdminClient()
|
||||
// to this member's own id. Reuses the `admin` client created above.
|
||||
const { error } = await admin
|
||||
.from('space_members')
|
||||
.update({ onboarding_completed_at: new Date().toISOString() })
|
||||
@@ -216,6 +216,16 @@ export async function finishOnboarding() {
|
||||
.eq('space_id', member.space_id)
|
||||
|
||||
if (error) return { error: error.message }
|
||||
|
||||
// Best-effort: pull in any prior anonymous submissions for this now-verified
|
||||
// member (e.g., a waiver signed before they had an account). Never blocks
|
||||
// finishing.
|
||||
try {
|
||||
await claimMyAnonymousSubmissions()
|
||||
} catch {
|
||||
/* advisory */
|
||||
}
|
||||
|
||||
revalidatePath('/dashboard')
|
||||
return { success: true as const }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user