Files
Gesti-n-Reservas-Naturcalab…/apps/api/src/routes/users.ts
Kilian ee03f0b36d feat: configuración de destinatarios de notificaciones desde la app
Gestiona grupos de email y asignación por escenario en BD
(natur_reservas.notification_groups / notification_scenario_recipients)
en lugar de variables de entorno hardcodeadas. Incluye pantalla de ajustes,
endpoint admin protegido, envío de prueba y niveles reducida/completa.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:12:58 +01:00

117 lines
3.6 KiB
TypeScript

import { Router, type Router as ExpressRouter } from 'express';
import { supabaseAdmin } from '../lib/supabase.js';
import { requireAdmin } from '../middleware/requireAdmin.js';
import type { UserRole } from '@naturcalabacera/shared';
const router: ExpressRouter = Router();
const VALID_ROLES: UserRole[] = ['admin', 'internal_staff', 'external_availability_viewer'];
// GET /api/users — listar todos los perfiles
router.get('/', requireAdmin, async (_req, res) => {
const { data, error } = await supabaseAdmin
.from('user_profiles')
.select('id, email, role, display_name, created_at')
.order('created_at', { ascending: false });
if (error) {
return res.status(500).json({ error: error.message });
}
return res.json({ users: data ?? [] });
});
// POST /api/users/invite — invitar un usuario nuevo
router.post('/invite', requireAdmin, async (req, res) => {
const { email, role, display_name } = req.body as {
email?: string;
role?: UserRole;
display_name?: string;
};
if (!email || !role) {
return res.status(400).json({ error: 'email y role son requeridos' });
}
if (!VALID_ROLES.includes(role)) {
return res.status(400).json({ error: 'Rol inválido' });
}
// 1. Invitar al usuario por email (envía correo con magic link)
const { data: invited, error: inviteError } = await supabaseAdmin.auth.admin.inviteUserByEmail(email);
if (inviteError || !invited.user) {
return res.status(400).json({ error: inviteError?.message ?? 'Error al invitar usuario' });
}
// 2. Crear el perfil con el rol seleccionado
const { data: profile, error: profileError } = await supabaseAdmin
.from('user_profiles')
.insert({
id: invited.user.id,
email,
role,
display_name: display_name ?? null,
})
.select()
.single();
if (profileError) {
// Best-effort cleanup: eliminar el usuario auth si el perfil falló
await supabaseAdmin.auth.admin.deleteUser(invited.user.id).catch(() => {});
return res.status(500).json({ error: profileError.message });
}
return res.status(201).json({ user: profile });
});
// PATCH /api/users/:id — actualizar rol o display_name
router.patch('/:id', requireAdmin, async (req, res) => {
const { id } = req.params;
const { role, display_name } = req.body as {
role?: UserRole;
display_name?: string;
};
if (role !== undefined && !VALID_ROLES.includes(role)) {
return res.status(400).json({ error: 'Rol inválido' });
}
const updates: Record<string, unknown> = {};
if (role !== undefined) updates.role = role;
if (display_name !== undefined) updates.display_name = display_name;
if (Object.keys(updates).length === 0) {
return res.status(400).json({ error: 'Nada que actualizar' });
}
const { data, error } = await supabaseAdmin
.from('user_profiles')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) {
return res.status(500).json({ error: error.message });
}
return res.json({ user: data });
});
// DELETE /api/users/:id — eliminar usuario auth + perfil
router.delete('/:id', requireAdmin, async (req, res) => {
const id = req.params.id as string;
const callerId = res.locals.callerId as string;
if (id === callerId) {
return res.status(400).json({ error: 'No puedes eliminar tu propia cuenta' });
}
// ON DELETE CASCADE en user_profiles.id elimina el perfil al eliminar el usuario auth.
const { error } = await supabaseAdmin.auth.admin.deleteUser(id);
if (error) {
return res.status(500).json({ error: error.message });
}
return res.json({ ok: true });
});
export { router as usersRouter };