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:
9
.vscode/launch.json
vendored
9
.vscode/launch.json
vendored
@@ -4,6 +4,13 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "msedge",
|
||||
"request": "launch",
|
||||
@@ -11,5 +18,5 @@
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
// Validar sesión
|
||||
if (!session || !session.user) {
|
||||
console.warn('No session or user found');
|
||||
return null;
|
||||
}
|
||||
const u = session.user;
|
||||
|
||||
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,
|
||||
try{
|
||||
const result = await supabase.rpc('obtener_claims_usuario', {
|
||||
p_user_id: u.id,
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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
|
||||
console.log(data);
|
||||
if (!data || data.length === 0) {
|
||||
console.warn('No se encontró información para el usuario');
|
||||
return null;
|
||||
}
|
||||
|
||||
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, // 🎨
|
||||
return data[0];
|
||||
} catch (e) {
|
||||
console.error('Error inesperado:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ?? '')
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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[]
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user