feat(sprint-4/etapa-1): Supabase auth + Google OAuth + LoginPage + rutas protegidas
- Cliente Supabase singleton en src/lib/supabase.ts - Interfaces AuthUser / AuthService en src/auth/types.ts - SupabaseAuthService: signInWithGoogle, signOut, getCurrentUser, onAuthStateChange con upsert a tabla users (platform_admin para @inquality.com.py, guest resto) - AuthProvider (React Context) wrappea toda la app; expone user, loading, signIn, signOut - LoginPage /login: logo InQuality color, título, fade-in, botón Google naranja #F59845 - RootComponent del router guarda todas las rutas — sin sesión → /login, con sesión + /login → / - Script SQL: supabase/migrations/001_create_users.sql (tabla users + RLS) - AppHeader: <Link> → <a href="/"> para evitar dependencia de RouterContext en tests - 561/561 tests verdes, build sin errores TS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
70
src/auth/SupabaseAuthService.ts
Normal file
70
src/auth/SupabaseAuthService.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import type { AuthService, AuthUser } from './types'
|
||||
|
||||
export class SupabaseAuthService implements AuthService {
|
||||
async signInWithGoogle(): Promise<void> {
|
||||
const { error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google',
|
||||
options: { redirectTo: window.location.origin },
|
||||
})
|
||||
if (error) throw error
|
||||
}
|
||||
|
||||
async signOut(): Promise<void> {
|
||||
const { error } = await supabase.auth.signOut()
|
||||
if (error) throw error
|
||||
}
|
||||
|
||||
async getCurrentUser(): Promise<AuthUser | null> {
|
||||
const { data: { session } } = await supabase.auth.getSession()
|
||||
if (!session?.user) return null
|
||||
return this.buildAuthUser(session.user)
|
||||
}
|
||||
|
||||
onAuthStateChange(callback: (user: AuthUser | null) => void): () => void {
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
async (event, session) => {
|
||||
if (!session?.user) {
|
||||
callback(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (event === 'SIGNED_IN') {
|
||||
const email = session.user.email ?? ''
|
||||
const role = email.endsWith('@inquality.com.py') ? 'platform_admin' : 'guest'
|
||||
await supabase.from('users').upsert(
|
||||
{
|
||||
id: session.user.id,
|
||||
name: session.user.user_metadata?.full_name ?? email,
|
||||
avatar_url: session.user.user_metadata?.avatar_url ?? null,
|
||||
platform_role: role,
|
||||
},
|
||||
{ onConflict: 'id', ignoreDuplicates: true }
|
||||
)
|
||||
}
|
||||
|
||||
const user = await this.buildAuthUser(session.user)
|
||||
callback(user)
|
||||
}
|
||||
)
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
|
||||
private async buildAuthUser(supabaseUser: { id: string; email?: string; user_metadata?: Record<string, unknown> }): Promise<AuthUser> {
|
||||
const email = supabaseUser.email ?? ''
|
||||
const { data: row } = await supabase
|
||||
.from('users')
|
||||
.select('name, avatar_url, platform_role')
|
||||
.eq('id', supabaseUser.id)
|
||||
.single()
|
||||
|
||||
return {
|
||||
id: supabaseUser.id,
|
||||
email,
|
||||
name: row?.name ?? supabaseUser.user_metadata?.full_name as string ?? email,
|
||||
avatarUrl: row?.avatar_url ?? supabaseUser.user_metadata?.avatar_url as string ?? undefined,
|
||||
platformRole: (row?.platform_role ?? 'guest') as AuthUser['platformRole'],
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user