import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' import { getSupabaseAdmin } from '@/lib/supabase' import { formatDate } from '../responses/admin-format' import { AdminNavBar } from '@/components/admin-nav-bar' import { InviteForm } from './invite-form' export const dynamic = 'force-dynamic' interface OrgOption { id: string name: string } interface RoleRow { user_id: string app_role: string org_id: string | null organizations: { name: string } | null } interface UserDisplayRow { userId: string email: string orgName: string role: string createdAt: string } async function getSupabaseSession() { const cookieStore = await cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll() }, setAll(cookiesToSet) { try { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options) ) } catch { // Server Component — no puede mutar cookies, se ignora } }, }, } ) } // auth.users no está expuesto vía PostgREST (no es schema público) — no hay // forma de hacer un .from('auth.users') ni un join SQL directo desde // supabase-js. listUsers() es la API admin soportada para resolver el email; // pagina hasta agotar resultados (tope defensivo de 2000 usuarios). async function fetchAllUsersMap(admin: ReturnType) { const map = new Map() const perPage = 200 for (let page = 1; page <= 10; page++) { const { data, error } = await admin.auth.admin.listUsers({ page, perPage }) if (error || !data) break data.users.forEach((u) => { map.set(u.id, { email: u.email ?? '—', createdAt: u.created_at }) }) if (data.users.length < perPage) break } return map } export default async function AdminUsuariosPage() { const supabase = await getSupabaseSession() // 1. Identificar a quien llama — defensivo, el middleware ya debería haber // redirigido a /login si no hay sesión. const { data: { user } } = await supabase.auth.getUser() if (!user) { return (

Acceso restringido

No se encontró una sesión activa.

← Iniciar sesión
) } // 2. Verificar platform_admin leyendo la fila REAL en user_roles — esta // página usa getSupabaseAdmin() más abajo (bypasa RLS por completo), así // que el gate de autorización tiene que ir acá, no delegarse a las policies. const { data: callerRole } = await supabase .from('user_roles') .select('app_role') .eq('user_id', user.id) .maybeSingle() if (callerRole?.app_role !== 'platform_admin') { return (

Acceso restringido

Esta sección es exclusiva para administradores de plataforma.

← Volver
) } // 3. Datos — admin client para todo a partir de acá. La autorización ya // quedó probada en el paso 2 con una lectura directa a la DB; usar admin // para el resto evita depender de policies/claims del JWT que podrían // haber quedado stale. const admin = getSupabaseAdmin() const [{ data: orgsData, error: orgsError }, { data: rolesData, error: rolesError }] = await Promise.all([ admin.from('organizations').select('id, name').order('name'), admin.from('user_roles').select('user_id, app_role, org_id, organizations(name)'), ]) if (orgsError || rolesError) { return (

Usuarios

No fue posible conectarse a la base de datos.

          {(orgsError ?? rolesError)?.message}
        
) } const organizations = (orgsData ?? []) as OrgOption[] const roles = (rolesData ?? []) as unknown as RoleRow[] const usersMap = await fetchAllUsersMap(admin) const userRows: UserDisplayRow[] = roles.map((r) => { const u = usersMap.get(r.user_id) return { userId: r.user_id, email: u?.email ?? '—', orgName: r.organizations?.name ?? '—', role: r.app_role, createdAt: u?.createdAt ? formatDate(u.createdAt) : '—', } }) return ( <>

Usuarios

{userRows.length} usuarios registrados

Invitar usuario

Usuarios existentes

{userRows.map((row) => ( ))}
Email Organización Rol Fecha de creación
{row.email} {row.orgName} {row.role} {row.createdAt}
{userRows.length === 0 && (

Todavía no hay usuarios invitados.

)}
) }