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:
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -4,6 +4,13 @@
|
|||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Chrome against localhost",
|
||||||
|
"url": "http://localhost:3000",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "msedge",
|
"type": "msedge",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
|||||||
@@ -15,13 +15,7 @@ export interface SupabaseAuthState {
|
|||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Role =
|
type Role = string;
|
||||||
| 'lci'
|
|
||||||
| 'vicerrectoria'
|
|
||||||
| 'director_facultad' // 👈 NEW
|
|
||||||
| 'secretario_academico'
|
|
||||||
| 'jefe_carrera'
|
|
||||||
| 'planeacion'
|
|
||||||
|
|
||||||
type UserClaims = {
|
type UserClaims = {
|
||||||
claims_admin: boolean
|
claims_admin: boolean
|
||||||
@@ -45,22 +39,23 @@ export function SupabaseAuthProvider({ children }: { children: React.ReactNode }
|
|||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Carga inicial
|
// Función para manejar la sesión
|
||||||
supabase.auth.getSession().then(async ({ data: { session } }) => {
|
const handleSession = async (session: Session | null) => {
|
||||||
const u = session?.user ?? null
|
const u = session?.user ?? null
|
||||||
setUser(u)
|
setUser(u)
|
||||||
setIsAuthenticated(!!u)
|
setIsAuthenticated(!!u)
|
||||||
setClaims(await buildClaims(session))
|
setClaims(await buildClaims(session))
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga inicial
|
||||||
|
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||||
|
handleSession(session)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Suscripción a cambios de sesión
|
// Suscripción a cambios de sesión
|
||||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (_event, session) => {
|
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||||
const u = session?.user ?? null
|
handleSession(session)
|
||||||
setUser(u)
|
|
||||||
setIsAuthenticated(!!u)
|
|
||||||
setClaims(await buildClaims(session))
|
|
||||||
setIsLoading(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => subscription.unsubscribe()
|
return () => subscription.unsubscribe()
|
||||||
@@ -99,49 +94,38 @@ export function useSupabaseAuth() {
|
|||||||
* Helpers
|
* 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> {
|
async function buildClaims(session: Session | null): Promise<UserClaims | null> {
|
||||||
const u = session?.user
|
// Validar sesión
|
||||||
if (!u) return null
|
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)
|
try{
|
||||||
const base: Partial<UserClaims> = {
|
const result = await supabase.rpc('obtener_claims_usuario', {
|
||||||
claims_admin: !!(app.claims_admin ?? (meta as any).claims_admin),
|
p_user_id: u.id,
|
||||||
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,
|
const data: UserClaims[] | null = result.data;
|
||||||
clave: (meta.clave as string) ?? '',
|
const error = result.error;
|
||||||
nombre: (meta.nombre as string) ?? '',
|
|
||||||
apellidos: (meta.apellidos as string) ?? '',
|
if (error) {
|
||||||
title: (meta.title as string) ?? '',
|
console.error('Error al obtener la información:', error);
|
||||||
avatar: (meta.avatar as string) ?? null,
|
throw new Error('Error al obtener la información del usuario');
|
||||||
}
|
}
|
||||||
|
|
||||||
let facultad_color: string | null = null
|
console.log(data);
|
||||||
if (base.facultad_id) {
|
if (!data || data.length === 0) {
|
||||||
// Lee color desde public.facultades
|
console.warn('No se encontró información para el usuario');
|
||||||
const { data, error } = await supabase
|
return null;
|
||||||
.from('facultades')
|
|
||||||
.select('color')
|
|
||||||
.eq('id', base.facultad_id)
|
|
||||||
.maybeSingle()
|
|
||||||
|
|
||||||
if (!error && data) facultad_color = (data as any)?.color ?? null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return data[0];
|
||||||
claims_admin: !!base.claims_admin,
|
} catch (e) {
|
||||||
role: (base.role ?? 'lci') as Role,
|
console.error('Error inesperado:', e);
|
||||||
clave: base.clave ?? '',
|
return null;
|
||||||
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, // 🎨
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ function useUserDisplay() {
|
|||||||
avatar: claims?.avatar ?? null,
|
avatar: claims?.avatar ?? null,
|
||||||
initials: getInitials([nombre, apellidos].filter(Boolean).join(" ")),
|
initials: getInitials([nombre, apellidos].filter(Boolean).join(" ")),
|
||||||
role,
|
role,
|
||||||
isAdmin: Boolean(claims?.claims_admin),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +149,7 @@ function Layout() {
|
|||||||
|
|
||||||
function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||||
const { claims } = useSupabaseAuth()
|
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 ?? '')
|
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 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 role = auth.claims?.role as 'lci' | 'vicerrectoria' | 'secretario_academico' | 'jefe_carrera' | 'planeacion' | undefined
|
||||||
|
|
||||||
const navigate = useNavigate({ from: Route.fullPath })
|
const navigate = useNavigate({ from: Route.fullPath })
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const Route = createFileRoute("/_authenticated/planes")({
|
|||||||
`)
|
`)
|
||||||
.order("fecha_creacion", { ascending: false })
|
.order("fecha_creacion", { ascending: false })
|
||||||
.limit(100)
|
.limit(100)
|
||||||
|
console.log({ data, error })
|
||||||
if (error) throw new Error(error.message)
|
if (error) throw new Error(error.message)
|
||||||
return (data ?? []) as PlanRow[]
|
return (data ?? []) as PlanRow[]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { CarreraCombobox, FacultadCombobox } from "@/components/users/procedenci
|
|||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
/* -------------------- Tipos -------------------- */
|
/* -------------------- Tipos -------------------- */
|
||||||
type AdminUser = {
|
type User = {
|
||||||
id: string
|
id: string
|
||||||
email: string | null
|
email: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
@@ -69,11 +69,11 @@ const usersKeys = {
|
|||||||
list: () => [...usersKeys.root, "list"] as const,
|
list: () => [...usersKeys.root, "list"] as const,
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchUsers(): Promise<AdminUser[]> {
|
async function fetchUsers(): Promise<User[]> {
|
||||||
// ⚠️ Dev only: service role en cliente
|
// ⚠️ 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 admin = new SupabaseClient(import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY)
|
||||||
const { data } = await admin.auth.admin.listUsers()
|
const { data } = await admin.auth.admin.listUsers()
|
||||||
return (data?.users ?? []) as AdminUser[]
|
return (data?.users ?? []) as User[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const usersOptions = () =>
|
const usersOptions = () =>
|
||||||
@@ -96,7 +96,7 @@ function RouteComponent() {
|
|||||||
const { data } = useSuspenseQuery(usersOptions())
|
const { data } = useSuspenseQuery(usersOptions())
|
||||||
|
|
||||||
const [q, setQ] = useState("")
|
const [q, setQ] = useState("")
|
||||||
const [editing, setEditing] = useState<AdminUser | null>(null)
|
const [editing, setEditing] = useState<User | null>(null)
|
||||||
const [form, setForm] = useState<{
|
const [form, setForm] = useState<{
|
||||||
role?: Role
|
role?: Role
|
||||||
claims_admin?: boolean
|
claims_admin?: boolean
|
||||||
@@ -167,7 +167,7 @@ function RouteComponent() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const toggleBan = useMutation({
|
const toggleBan = useMutation({
|
||||||
mutationFn: async (u: AdminUser) => {
|
mutationFn: async (u: User) => {
|
||||||
const banned = !!u.banned_until && new Date(u.banned_until) > new Date()
|
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 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)
|
const { error } = await supabase.auth.admin.updateUserById(u.id, payload as any)
|
||||||
@@ -228,7 +228,7 @@ function RouteComponent() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const saveUser = useMutation({
|
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í)
|
// 1) Actualiza metadatos (tu Edge Function; placeholder aquí)
|
||||||
// await fetch('/functions/update-user', { method: 'POST', body: JSON.stringify({ id: u.id, ...f }) })
|
// await fetch('/functions/update-user', { method: 'POST', body: JSON.stringify({ id: u.id, ...f }) })
|
||||||
// Simula éxito:
|
// Simula éxito:
|
||||||
@@ -251,7 +251,7 @@ function RouteComponent() {
|
|||||||
onError: (e: any) => toast.error(e?.message || "No se pudo guardar"),
|
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>
|
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])
|
}, [q, data])
|
||||||
|
|
||||||
function openEdit(u: AdminUser) {
|
function openEdit(u: User) {
|
||||||
setEditing(u)
|
setEditing(u)
|
||||||
setForm({
|
setForm({
|
||||||
role: u.app_metadata?.role,
|
role: u.app_metadata?.role,
|
||||||
|
|||||||
Reference in New Issue
Block a user