From f2b3010ac959bd5db273f71e90705b2c8e2a5963 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Fri, 10 Oct 2025 17:23:37 -0600 Subject: [PATCH] =?UTF-8?q?Ahora=20se=20obtienen=20claims=20de=20las=20tab?= =?UTF-8?q?las=20en=20el=20esquema=20public,=20en=20vez=20de=20la=20inform?= =?UTF-8?q?aci=C3=B3n=20de=20sesion=20del=20usuario,=20que=20se=20obtiene?= =?UTF-8?q?=20de=20la=20tabla=20auth.users?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit En supabase.tsx se sustituy贸 la manera de obtener los claims del usuario, utilizando ahora un rpc de una funci贸n en supabase. --- .vscode/launch.json | 23 ++++-- src/auth/supabase.tsx | 104 ++++++++++-------------- src/routes/_authenticated.tsx | 3 +- src/routes/_authenticated/dashboard.tsx | 2 +- src/routes/_authenticated/planes.tsx | 1 + src/routes/_authenticated/usuarios.tsx | 16 ++-- 6 files changed, 70 insertions(+), 79 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index af8dc9d..30266e4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,12 +4,19 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { - "type": "msedge", - "request": "launch", - "name": "Launch Edge against localhost", - "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}" - } - ] + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}" + }, + { + "type": "msedge", + "request": "launch", + "name": "Launch Edge against localhost", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}" + } +] } \ No newline at end of file diff --git a/src/auth/supabase.tsx b/src/auth/supabase.tsx index 7d735a0..4ba367d 100644 --- a/src/auth/supabase.tsx +++ b/src/auth/supabase.tsx @@ -15,13 +15,7 @@ export interface SupabaseAuthState { isLoading: boolean } -type Role = - | 'lci' - | 'vicerrectoria' - | 'director_facultad' // 馃憟 NEW - | 'secretario_academico' - | 'jefe_carrera' - | 'planeacion' +type Role = string; type UserClaims = { claims_admin: boolean @@ -45,24 +39,25 @@ export function SupabaseAuthProvider({ children }: { children: React.ReactNode } const [isLoading, setIsLoading] = useState(true) useEffect(() => { + // Funci贸n para manejar la sesi贸n + const handleSession = async (session: Session | null) => { + const u = session?.user ?? null + setUser(u) + setIsAuthenticated(!!u) + setClaims(await buildClaims(session)) + setIsLoading(false) + } + // Carga inicial - supabase.auth.getSession().then(async ({ data: { session } }) => { - const u = session?.user ?? null - setUser(u) - setIsAuthenticated(!!u) - setClaims(await buildClaims(session)) - setIsLoading(false) + supabase.auth.getSession().then(({ data: { session } }) => { + handleSession(session) }) - + // Suscripci贸n a cambios de sesi贸n - const { data: { subscription } } = supabase.auth.onAuthStateChange(async (_event, session) => { - const u = session?.user ?? null - setUser(u) - setIsAuthenticated(!!u) - setClaims(await buildClaims(session)) - setIsLoading(false) + const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { + handleSession(session) }) - + return () => subscription.unsubscribe() }, []) @@ -99,49 +94,38 @@ export function useSupabaseAuth() { * Helpers * ===================== */ -// Unifica extracci贸n de metadatos y resuelve facultad_color si hay facultad_id +// Obtiene los claims del usuario desde la base de datos a partir de una funci贸n en la BDD async function buildClaims(session: Session | null): Promise { - const u = session?.user - if (!u) return null - - const app = (u.app_metadata ?? {}) as Partial & { role?: Role } - const meta = (u.user_metadata ?? {}) as Partial - - // Mezcla segura: app_metadata > user_metadata (para campos de claims) - const base: Partial = { - claims_admin: !!(app.claims_admin ?? (meta as any).claims_admin), - role: (app.role as Role | undefined) ?? ('lci' as Role), - facultad_id: app.facultad_id ?? meta.facultad_id ?? null, - carrera_id: app.carrera_id ?? meta.carrera_id ?? null, - clave: (meta.clave as string) ?? '', - nombre: (meta.nombre as string) ?? '', - apellidos: (meta.apellidos as string) ?? '', - title: (meta.title as string) ?? '', - avatar: (meta.avatar as string) ?? null, + // Validar sesi贸n + if (!session || !session.user) { + console.warn('No session or user found'); + return null; } + const u = session.user; - let facultad_color: string | null = null - if (base.facultad_id) { - // Lee color desde public.facultades - const { data, error } = await supabase - .from('facultades') - .select('color') - .eq('id', base.facultad_id) - .maybeSingle() - if (!error && data) facultad_color = (data as any)?.color ?? null - } + try{ + const result = await supabase.rpc('obtener_claims_usuario', { + p_user_id: u.id, + }); + + const data: UserClaims[] | null = result.data; + const error = result.error; - return { - claims_admin: !!base.claims_admin, - role: (base.role ?? 'lci') as Role, - clave: base.clave ?? '', - nombre: base.nombre ?? '', - apellidos: base.apellidos ?? '', - title: base.title ?? '', - avatar: base.avatar ?? null, - facultad_id: (base.facultad_id as string | null) ?? null, - carrera_id: (base.carrera_id as string | null) ?? null, - facultad_color, // 馃帹 + if (error) { + console.error('Error al obtener la informaci贸n:', error); + throw new Error('Error al obtener la informaci贸n del usuario'); + } + + console.log(data); + if (!data || data.length === 0) { + console.warn('No se encontr贸 informaci贸n para el usuario'); + return null; + } + + return data[0]; + } catch (e) { + console.error('Error inesperado:', e); + return null; } } diff --git a/src/routes/_authenticated.tsx b/src/routes/_authenticated.tsx index f0838fb..0306b26 100644 --- a/src/routes/_authenticated.tsx +++ b/src/routes/_authenticated.tsx @@ -73,7 +73,6 @@ function useUserDisplay() { avatar: claims?.avatar ?? null, initials: getInitials([nombre, apellidos].filter(Boolean).join(" ")), role, - isAdmin: Boolean(claims?.claims_admin), } } @@ -150,7 +149,7 @@ function Layout() { function Sidebar({ onNavigate }: { onNavigate?: () => void }) { const { claims } = useSupabaseAuth() - const isAdmin = Boolean(claims?.claims_admin) + const isAdmin = claims?.role === 'lci' || claims?.role === 'vicerrectoria' const canSeeCarreras = ["jefe_carrera", 'vicerrectoria', 'secretario_academico', 'lci'].includes(claims?.role ?? '') diff --git a/src/routes/_authenticated/dashboard.tsx b/src/routes/_authenticated/dashboard.tsx index 8a19e2b..abc0d85 100644 --- a/src/routes/_authenticated/dashboard.tsx +++ b/src/routes/_authenticated/dashboard.tsx @@ -175,7 +175,7 @@ function RouteComponent() { const name = auth.user?.user_metadata?.nombre || auth.user?.email?.split('@')[0] || '隆Hola!' - const isAdmin = !!auth.claims?.claims_admin + const isAdmin = auth.claims?.role === 'lci' || auth.claims?.role === 'vicerrectoria' const role = auth.claims?.role as 'lci' | 'vicerrectoria' | 'secretario_academico' | 'jefe_carrera' | 'planeacion' | undefined const navigate = useNavigate({ from: Route.fullPath }) diff --git a/src/routes/_authenticated/planes.tsx b/src/routes/_authenticated/planes.tsx index f1ad984..1073570 100644 --- a/src/routes/_authenticated/planes.tsx +++ b/src/routes/_authenticated/planes.tsx @@ -45,6 +45,7 @@ export const Route = createFileRoute("/_authenticated/planes")({ `) .order("fecha_creacion", { ascending: false }) .limit(100) + console.log({ data, error }) if (error) throw new Error(error.message) return (data ?? []) as PlanRow[] }, diff --git a/src/routes/_authenticated/usuarios.tsx b/src/routes/_authenticated/usuarios.tsx index 4e4eae8..4a377a8 100644 --- a/src/routes/_authenticated/usuarios.tsx +++ b/src/routes/_authenticated/usuarios.tsx @@ -20,7 +20,7 @@ import { CarreraCombobox, FacultadCombobox } from "@/components/users/procedenci import { toast } from "sonner" /* -------------------- Tipos -------------------- */ -type AdminUser = { +type User = { id: string email: string | null created_at: string @@ -69,11 +69,11 @@ const usersKeys = { list: () => [...usersKeys.root, "list"] as const, } -async function fetchUsers(): Promise { +async function fetchUsers(): Promise { // 鈿狅笍 Dev only: service role en cliente const admin = new SupabaseClient(import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY) const { data } = await admin.auth.admin.listUsers() - return (data?.users ?? []) as AdminUser[] + return (data?.users ?? []) as User[] } const usersOptions = () => @@ -96,7 +96,7 @@ function RouteComponent() { const { data } = useSuspenseQuery(usersOptions()) const [q, setQ] = useState("") - const [editing, setEditing] = useState(null) + const [editing, setEditing] = useState(null) const [form, setForm] = useState<{ role?: Role claims_admin?: boolean @@ -167,7 +167,7 @@ function RouteComponent() { }) const toggleBan = useMutation({ - mutationFn: async (u: AdminUser) => { + mutationFn: async (u: User) => { const banned = !!u.banned_until && new Date(u.banned_until) > new Date() const payload = banned ? { banned_until: null } : { banned_until: new Date(Date.now() + 100 * 365 * 24 * 60 * 60 * 1000).toISOString() } const { error } = await supabase.auth.admin.updateUserById(u.id, payload as any) @@ -228,7 +228,7 @@ function RouteComponent() { }) const saveUser = useMutation({ - mutationFn: async ({ u, f }: { u: AdminUser; f: typeof form }) => { + mutationFn: async ({ u, f }: { u: User; f: typeof form }) => { // 1) Actualiza metadatos (tu Edge Function; placeholder aqu铆) // await fetch('/functions/update-user', { method: 'POST', body: JSON.stringify({ id: u.id, ...f }) }) // Simula 茅xito: @@ -251,7 +251,7 @@ function RouteComponent() { onError: (e: any) => toast.error(e?.message || "No se pudo guardar"), }) - if (!auth.claims?.claims_admin) { + if (auth.claims?.role !== "lci" && auth.claims?.role !== "vicerrectoria") { return
No tienes permisos para administrar usuarios.
} @@ -267,7 +267,7 @@ function RouteComponent() { }) }, [q, data]) - function openEdit(u: AdminUser) { + function openEdit(u: User) { setEditing(u) setForm({ role: u.app_metadata?.role,