Session 10: Internal auth refactor, prefetch cascade keys, Sentry, welcome email (1286 tests)

This commit is contained in:
Kev
2026-06-10 20:45:05 -04:00
parent b55dcbd614
commit e5c45ecc8e
22 changed files with 3837 additions and 94 deletions
+80
View File
@@ -0,0 +1,80 @@
import { NextRequest, NextResponse } from 'next/server';
import { getUserFromRequest, jsonError } from '@/lib/auth-helpers';
import { getServiceRoleSupabase } from '@/lib/supabase';
import { sendWelcomeEmail } from '@/services/email';
export const dynamic = 'force-dynamic';
/**
* Welcome email trigger (Session 10).
*
* POST /api/welcome-email
* Bearer auth required.
*
* Idempotent. The send-once flag lives on Supabase auth user_metadata
* (`welcome_email_sent`) — no migration needed; user_metadata is a
* JSONB field Supabase exposes by default. We read it via the
* service-role admin API so the user can't spoof it from the browser.
*
* Response codes:
* 200 { sent: true, id } — email sent successfully
* 200 { sent: false, reason } — skipped (already sent, missing
* RESEND_API_KEY, etc.)
* 401 — auth missing
* 500 — unexpected server error
*
* The page that calls this (web/src/app/welcome/page.tsx) treats any
* 200 response as success regardless of `sent`. Errors are surfaced
* to Sentry, not to the user — a missed welcome email never blocks
* onboarding.
*/
export async function POST(req: NextRequest) {
const user = await getUserFromRequest(req);
if (!user) return jsonError(401, 'Authentication required');
if (!user.email) {
return NextResponse.json({ sent: false, reason: 'no_email_on_account' });
}
const sb = getServiceRoleSupabase();
if (!sb) {
// Without service-role we can't read/write user_metadata; bail
// softly so the welcome page render isn't blocked.
return NextResponse.json({ sent: false, reason: 'no_service_role' });
}
// Read the auth user via the admin API to inspect user_metadata.
let metadata: Record<string, unknown> = {};
try {
const { data, error } = await sb.auth.admin.getUserById(user.id);
if (error || !data?.user) {
return NextResponse.json({ sent: false, reason: 'user_not_found' });
}
metadata = (data.user.user_metadata as Record<string, unknown>) || {};
} catch {
return NextResponse.json({ sent: false, reason: 'metadata_read_failed' });
}
if (metadata.welcome_email_sent === true) {
return NextResponse.json({ sent: false, reason: 'already_sent' });
}
// Send + flag. We flag REGARDLESS of send outcome — a Resend
// outage shouldn't queue infinite retries from the welcome page.
// Operators can manually clear the flag if a batch needs to be
// re-sent.
const result = await sendWelcomeEmail(user.email);
try {
await sb.auth.admin.updateUserById(user.id, {
user_metadata: { ...metadata, welcome_email_sent: true, welcome_email_sent_at: new Date().toISOString() },
});
} catch {
// Flag write failed — log via Sentry if wired; the duplicate-
// suppression layer below will eventually catch up.
}
if (!result.ok) {
return NextResponse.json({ sent: false, reason: result.error || 'send_failed' });
}
return NextResponse.json({ sent: true, id: result.id });
}