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 });
}
+2
View File
@@ -11,6 +11,7 @@ import PushPrompt from '@/components/PushPrompt';
import MFAPrompt from '@/components/MFAPrompt';
import MFAChallenge from '@/components/MFAChallenge';
import CookieConsent from '@/components/CookieConsent';
import SentryInit from '@/components/SentryInit';
import './globals.css';
export const metadata: Metadata = {
@@ -109,6 +110,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<MFAPrompt />
<MFAChallenge />
<CookieConsent />
<SentryInit />
</ParlayProvider>
</ExplainModeProvider>
</AuthProvider>
+20 -2
View File
@@ -2,16 +2,34 @@
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
export default function WelcomePage() {
const router = useRouter();
const { user, loading } = useAuth();
const { user, session, loading } = useAuth();
const welcomeFiredRef = useRef(false);
useEffect(() => {
if (!loading && !user) router.replace('/signup');
}, [loading, user, router]);
// Session 10 — trigger the welcome email exactly once per mount.
// The server-side handler is idempotent via user_metadata
// (welcome_email_sent flag), so re-triggers from a refresh are
// safe; the ref is just a UX optimization to avoid duplicate
// network requests on this page.
useEffect(() => {
if (welcomeFiredRef.current) return;
if (loading || !user || !session) return;
welcomeFiredRef.current = true;
fetch('/api/welcome-email', {
method: 'POST',
headers: { Authorization: `Bearer ${session.access_token}` },
}).catch(() => {
// Fail silent — onboarding must not block on email outages.
});
}, [loading, user, session]);
return (
<section
className="tex-scan"