Corrección 1 — Delete de proceso restaurado +
This commit is contained in:
@@ -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(), [])
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user