Ahora se obtienen claims de las tablas en el esquema public, en vez de la información de sesion del usuario, que se obtiene de la tabla auth.users

En supabase.tsx se sustituyó la manera de obtener los claims del usuario, utilizando ahora un rpc de una función en supabase.
This commit is contained in:
2025-10-10 17:23:37 -06:00
parent c49c0bbc0a
commit f2b3010ac9
6 changed files with 70 additions and 79 deletions

23
.vscode/launch.json vendored
View File

@@ -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}"
}
]
}

View File

@@ -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,22 +39,23 @@ export function SupabaseAuthProvider({ children }: { children: React.ReactNode }
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
// Carga inicial
supabase.auth.getSession().then(async ({ data: { session } }) => {
// 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(({ 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<UserClaims | null> {
const u = session?.user
if (!u) return null
const app = (u.app_metadata ?? {}) as Partial<UserClaims> & { role?: Role }
const meta = (u.user_metadata ?? {}) as Partial<UserClaims>
// Mezcla segura: app_metadata > user_metadata (para campos de claims)
const base: Partial<UserClaims> = {
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,
});
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, // 🎨
const data: UserClaims[] | null = result.data;
const error = result.error;
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;
}
}

View File

@@ -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 ?? '')

View File

@@ -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 })

View File

@@ -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[]
},

View File

@@ -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<AdminUser[]> {
async function fetchUsers(): Promise<User[]> {
// ⚠️ 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<AdminUser | null>(null)
const [editing, setEditing] = useState<User | null>(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 <div className="p-6 text-sm text-red-600">No tienes permisos para administrar usuarios.</div>
}
@@ -267,7 +267,7 @@ function RouteComponent() {
})
}, [q, data])
function openEdit(u: AdminUser) {
function openEdit(u: User) {
setEditing(u)
setForm({
role: u.app_metadata?.role,