Add Google SSO
- Migration: profiles.username is now nullable -- Google's OAuth redirect can't collect a username up front the way the email/password signup form does, so a first-time Google sign-in's profile is created with no username. - supabase/config.toml: [auth.external.google] enabled, credentials via env() substitution (SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID/_SECRET in .env.local, which the CLI auto-loads). skip_nonce_check is on, which Supabase's own docs call out as required for local sign-in. - LoginScreen: "Continue with Google" alongside the existing email/password form. - CompleteProfileScreen: one-time gate for a signed-in user with no username yet (i.e. first Google sign-in) -- same hard-gate spirit as email verification, nothing else is usable until a username is set. - App.tsx now checks profiles.username after establishing a session and routes to CompleteProfileScreen before AppShell when it's missing. RLS test suite re-run clean (23/23) after the schema change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
+23
@@ -1,5 +1,6 @@
|
||||
import type { Session } from '@supabase/supabase-js'
|
||||
import { useEffect, useState } from 'react'
|
||||
import CompleteProfileScreen from './components/auth/CompleteProfileScreen'
|
||||
import LoginScreen from './components/auth/LoginScreen'
|
||||
import AppShell from './components/layout/AppShell'
|
||||
import { supabase } from './data/supabaseClient'
|
||||
@@ -7,6 +8,8 @@ import { supabase } from './data/supabaseClient'
|
||||
export default function App() {
|
||||
// undefined = still checking for an existing session; null = signed out.
|
||||
const [session, setSession] = useState<Session | null | undefined>(undefined)
|
||||
// undefined = haven't checked yet; null = has one; true = needs to set one.
|
||||
const [needsUsername, setNeedsUsername] = useState<boolean | null | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data }) => setSession(data.session))
|
||||
@@ -16,6 +19,18 @@ export default function App() {
|
||||
return () => subscription.unsubscribe()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Nothing to check while signed out — the render logic below never
|
||||
// reads needsUsername in that case anyway.
|
||||
if (!session) return
|
||||
supabase
|
||||
.from('profiles')
|
||||
.select('username')
|
||||
.eq('id', session.user.id)
|
||||
.single()
|
||||
.then(({ data }) => setNeedsUsername(!data?.username))
|
||||
}, [session])
|
||||
|
||||
if (session === undefined) {
|
||||
return <div className="flex h-screen items-center justify-center text-sm text-slate-400">Loading…</div>
|
||||
}
|
||||
@@ -24,5 +39,13 @@ export default function App() {
|
||||
return <LoginScreen />
|
||||
}
|
||||
|
||||
if (needsUsername === undefined) {
|
||||
return <div className="flex h-screen items-center justify-center text-sm text-slate-400">Loading…</div>
|
||||
}
|
||||
|
||||
if (needsUsername) {
|
||||
return <CompleteProfileScreen onDone={() => setNeedsUsername(false)} />
|
||||
}
|
||||
|
||||
return <AppShell />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user