Corrección 1 — Delete de proceso restaurado +

This commit is contained in:
2026-05-29 18:45:48 -03:00
parent 0a3c60cd09
commit 2955399ade
19 changed files with 502 additions and 98 deletions

View File

@@ -13,29 +13,45 @@ const AuthContext = createContext<AuthContextValue | null>(null)
const authService = new SupabaseAuthService()
// Lee el usuario almacenado en localStorage SIN hacer llamadas de red.
// Supabase v2 usa la clave sb-{project_ref}-auth-token.
// Permite renderizar la app inmediatamente sin esperar la verificación async de sesión.
function getUserFromStorage(): AuthUser | null {
try {
const key = Object.keys(localStorage).find(
(k) => k.startsWith('sb-') && k.endsWith('-auth-token')
)
if (!key) return null
const stored = JSON.parse(localStorage.getItem(key) ?? 'null')
const u = stored?.user
if (!u?.id) return null
const email = (u.email as string) ?? ''
return {
id: u.id as string,
email,
name: (u.user_metadata?.full_name as string) ?? email,
avatarUrl: u.user_metadata?.avatar_url as string | undefined,
platformRole: email.endsWith('@inquality.com.py') ? 'platform_admin' : 'guest',
}
} catch {
return null
}
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null)
const [loading, setLoading] = useState(true)
// Inicialización sincrónica desde localStorage — 0ms, sin red
const [user, setUser] = useState<AuthUser | null>(getUserFromStorage())
// loading siempre false: la respuesta de localStorage es inmediata
const [loading] = useState(false)
useEffect(() => {
// Timeout de seguridad: si Supabase no responde en 10s, desbloquear UI
const timeout = setTimeout(() => {
setLoading((prev) => {
if (prev) console.error('Auth timeout: la sesión de Supabase no respondió en 10s')
return false
})
}, 10_000)
// Verificar y sincronizar con Supabase en background
// Si la sesión expiró o fue revocada, onAuthStateChange llamará callback(null)
// y el router redirigirá a /login
const unsubscribe = authService.onAuthStateChange((u) => {
clearTimeout(timeout)
setUser(u)
setLoading(false)
})
return () => {
clearTimeout(timeout)
unsubscribe()
}
return () => unsubscribe()
}, [])
const signIn = useCallback(() => authService.signInWithGoogle(), [])

View File

@@ -24,14 +24,19 @@ export class SupabaseAuthService implements AuthService {
onAuthStateChange(callback: (user: AuthUser | null) => void): () => void {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
// Siempre llamar callback — incluso si falla algo interno
try {
if (!session?.user) {
callback(null)
// INITIAL_SESSION null es transitorio: Supabase no pudo verificar la sesión
// (red lenta, cold start, timeout). Si hay sesión en localStorage, no forzar logout.
// Solo cerrar sesión en eventos explícitos (SIGNED_OUT, USER_DELETED, etc).
if (event !== 'INITIAL_SESSION') {
callback(null)
}
return
}
if (event === 'SIGNED_IN') {
// Primer login: upsert del usuario en la tabla users
const email = session.user.email ?? ''
const role = email.endsWith('@inquality.com.py') ? 'platform_admin' : 'guest'
const { error: upsertError } = await supabase.from('users').upsert(
@@ -46,19 +51,14 @@ export class SupabaseAuthService implements AuthService {
if (upsertError) console.error('Error al crear usuario en DB:', upsertError)
}
const user = await this.buildAuthUser(session.user)
// buildAuthUser es sincrónico — sin DB query, usa metadatos de la sesión
const user = this.buildAuthUser(session.user)
callback(user)
} catch (err) {
console.error('Error en onAuthStateChange:', err)
// Garantizar que loading se desbloquea incluso en error
if (session?.user) {
callback({
id: session.user.id,
email: session.user.email ?? '',
name: session.user.user_metadata?.full_name as string ?? session.user.email ?? '',
platformRole: 'guest',
})
} else {
callback(this.buildAuthUser(session.user))
} else if (event !== 'INITIAL_SESSION') {
callback(null)
}
}
@@ -68,20 +68,16 @@ export class SupabaseAuthService implements AuthService {
return () => subscription.unsubscribe()
}
private async buildAuthUser(supabaseUser: { id: string; email?: string; user_metadata?: Record<string, unknown> }): Promise<AuthUser> {
// Sincrónico: deriva el usuario de los metadatos de sesión sin consultar la DB.
// El rol se deriva del dominio del email (misma lógica que el upsert en SIGNED_IN).
private buildAuthUser(supabaseUser: { id: string; email?: string; user_metadata?: Record<string, unknown> }): 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'],
name: supabaseUser.user_metadata?.full_name as string ?? email,
avatarUrl: supabaseUser.user_metadata?.avatar_url as string ?? undefined,
platformRole: email.endsWith('@inquality.com.py') ? 'platform_admin' : 'guest',
}
}
}