Agrego carpeta del source sql de la Base de Datos

This commit is contained in:
Pablo
2026-08-22 19:10:49 -03:00
parent 11e36bd6c2
commit 88d724fbcf
94 changed files with 7820 additions and 0 deletions
@@ -0,0 +1,69 @@
-- ============================================================================
-- fc_cambiar_propia_contrasena
-- ============================================================================
-- PROPÓSITO
-- Permite a un admin o superadmin cambiar su propia contraseña,
-- confirmando primero la contraseña actual. Autoservicio: no recibe el id
-- de ningún otro usuario, solo opera sobre el dueño del token.
--
-- PARÁMETROS
-- p_token UUID sesión del actor (también identifica de quién
-- es la contraseña a cambiar).
-- p_password_actual TEXT contraseña actual en texto plano, para
-- confirmar identidad antes de cambiarla.
-- p_password_nueva TEXT contraseña nueva en texto plano (mínimo 8
-- caracteres; se valida también en el cliente,
-- pero el servidor es la última línea).
--
-- AUTORIZACIÓN
-- Requiere permiso 'cambiar_propia_contrasena'.
--
-- ERRORES (RAISE EXCEPTION)
-- - 'La contraseña actual es incorrecta.'
-- - 'La contraseña nueva debe tener al menos 8 caracteres.'
--
-- RETORNA
-- void.
--
-- EFECTOS SECUNDARIOS
-- UPDATE de password_hash en public.usuarios, fila del propio actor.
-- fecha_modificacion no se toca (no es un cambio de "perfil").
-- ============================================================================
CREATE OR REPLACE FUNCTION public.fc_cambiar_propia_contrasena(
p_token UUID,
p_password_actual TEXT,
p_password_nueva TEXT
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO extensions, public
AS $function$
DECLARE
v_user_id UUID;
v_stored_password_hash TEXT;
BEGIN
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'cambiar_propia_contrasena');
IF p_password_nueva IS NULL OR length(p_password_nueva) < 8 THEN
RAISE EXCEPTION 'La contraseña nueva debe tener al menos 8 caracteres.';
END IF;
SELECT usuario_id INTO v_user_id FROM internal.sesiones WHERE token = p_token;
SELECT password_hash INTO v_stored_password_hash
FROM public.usuarios
WHERE id = v_user_id
FOR UPDATE;
IF v_stored_password_hash IS NULL
OR v_stored_password_hash <> crypt(p_password_actual, v_stored_password_hash) THEN
RAISE EXCEPTION 'La contraseña actual es incorrecta.';
END IF;
UPDATE public.usuarios
SET password_hash = internal.create_password_hash(p_password_nueva)
WHERE id = v_user_id;
END;
$function$;
@@ -0,0 +1,59 @@
-- ============================================================================
-- fc_eliminar_usuario
-- ============================================================================
-- PROPÓSITO
-- Elimina un usuario (DELETE real). Si tiene registros asociados (pagos,
-- historial, etc.) y un FK lo impide, atrapa la violación y devuelve un
-- mensaje amigable al frontend.
--
-- PARÁMETROS
-- p_dni TEXT DNI del usuario a eliminar.
-- p_token UUID sesión del actor.
--
-- AUTORIZACIÓN
-- Requiere permiso 'eliminar_usuarios'.
--
-- ERRORES (RAISE EXCEPTION)
-- - 'No se puede eliminar el usuario X porque tiene registros asociados
-- (pagos, historial, etc). Primero debe archivar o borrar esos datos.'
-- - 'No existe un usuario con el DNI X'
--
-- RETORNA
-- BOOL TRUE en caso de éxito.
--
-- EFECTOS SECUNDARIOS
-- DELETE de la fila en public.usuarios. Las eliminaciones en cascada
-- dependen de los FKs definidos en las tablas hijas.
-- ============================================================================
CREATE OR REPLACE FUNCTION public.fc_eliminar_usuario(p_dni text, p_token uuid)
RETURNS bool
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO public
AS $function$
DECLARE
v_id_eliminado UUID;
BEGIN
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'eliminar_usuarios');
BEGIN
DELETE FROM public.usuarios
WHERE dni = p_dni
RETURNING id INTO v_id_eliminado;
EXCEPTION
WHEN foreign_key_violation THEN
-- Este mensaje es mucho más amigable que el error nativo de Postgres
RAISE EXCEPTION 'No se puede eliminar el usuario % porque tiene registros asociados (pagos, historial, etc). Primero debe archivar o borrar esos datos.', p_dni;
END;
-- 3. Validar si encontró al usuario
IF v_id_eliminado IS NULL THEN
RAISE EXCEPTION 'No existe un usuario con el DNI %', p_dni;
END IF;
RETURN TRUE;
END;
$function$;
@@ -0,0 +1,143 @@
-- ============================================================================
-- fc_insertar_usuario
-- ============================================================================
-- PROPÓSITO
-- Crea un usuario nuevo. Si el rol es 'admin' o 'superadmin', exige un
-- permiso adicional sobre el del actor. Hashea la contraseña recibida en
-- texto plano antes de persistir.
--
-- PARÁMETROS
-- p_datos JSONB con el payload del usuario. Claves:
-- dni TEXT (obligatorio)
-- nombre TEXT (obligatorio)
-- apellido TEXT (opcional)
-- mail TEXT (opcional)
-- telefono TEXT (opcional)
-- peso DECIMAL (opcional)
-- altura INTEGER (opcional)
-- fuerza_max DECIMAL (opcional)
-- sexo TEXT (opcional, default 'Hombre')
-- rol TEXT (obligatorio) p.ej. 'cliente', 'admin', 'superadmin'.
-- password TEXT (obligatorio si rol != 'cliente'; opcional si
-- rol = 'cliente') en texto plano; se hashea
-- internamente.
-- isActive BOOLEAN (obligatorio).
-- tipo_cuota UUID (opcional) plan asociado. Si se pasa debe existir
-- en tipos_cuota.
-- p_token UUID sesión del actor.
--
-- AUTORIZACIÓN
-- Requiere permiso 'modificar_usuarios'. Si rol = 'superadmin' exige además
-- 'agregar_superadmin'. Si rol = 'admin' exige además 'agregar_admin'.
--
-- ERRORES (RAISE EXCEPTION)
-- - 'El tipo_cuota X no existe.'
-- - 'La contraseña es obligatoria' (si rol != 'cliente' y no se mandó)
-- - 'Error al generar el hash de la contraseña'
--
-- RETORNA
-- void.
--
-- EFECTOS SECUNDARIOS
-- INSERT en public.usuarios con password_hash calculado. Si rol = 'cliente'
-- y no se mandó password, password_hash se inserta NULL (los clientes no
-- inician sesión en este panel, usan el bot de WhatsApp).
-- ============================================================================
CREATE OR REPLACE FUNCTION public.fc_insertar_usuario(p_datos jsonb, p_token uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO extensions, public
AS $function$
DECLARE
v_password_hash TEXT;
v_rol TEXT;
-- Campos del nuevo usuario
new_dni TEXT;
new_nombre TEXT;
new_apellido TEXT;
new_mail TEXT;
new_telefono TEXT;
new_peso DECIMAL(5,2);
new_altura INTEGER;
new_fuerza_max DECIMAL(10,2);
new_sexo TEXT;
new_rol TEXT;
new_password TEXT;
new_isactive BOOL;
new_tipo_cuota UUID;
BEGIN
-- 1. Validar el token de sesión y obtener el rol del usuario que llama.
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'modificar_usuarios');
-- 2. Validar que no esté intentando crear un admin o superadmin si no puede
new_rol := p_datos->>'rol';
IF new_rol = 'superadmin' THEN
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'agregar_superadmin');
ELSIF new_rol = 'admin' THEN
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'agregar_admin');
END IF;
-- 2.5 Extraer campos del JSONB
new_dni := p_datos->>'dni';
new_nombre := p_datos->>'nombre';
new_apellido := p_datos->>'apellido';
new_mail := p_datos->>'mail';
new_telefono := p_datos->>'telefono';
new_peso := (p_datos->>'peso')::DECIMAL;
new_altura := (p_datos->>'altura')::INTEGER;
new_fuerza_max := (p_datos->>'fuerza_max')::DECIMAL;
new_sexo := COALESCE(p_datos->>'sexo', 'Hombre');
new_password := p_datos->>'password';
new_isactive := (p_datos->>'isActive')::BOOLEAN;
-- CORRECCIÓN 1: Validar que exista la key Y que NO sea null antes de buscar en la BD
IF p_datos ? 'tipo_cuota' AND (p_datos->>'tipo_cuota') IS NOT NULL THEN
new_tipo_cuota := (p_datos->>'tipo_cuota')::UUID;
-- Validar que exista el tipo_cuota en la tabla padre
IF NOT EXISTS (
SELECT 1 FROM tipos_cuota
WHERE id = new_tipo_cuota
) THEN
RAISE EXCEPTION 'El tipo_cuota % no existe.', new_tipo_cuota;
END IF;
ELSE
-- Si no viene o es null, asignamos NULL explícitamente
new_tipo_cuota := NULL;
END IF;
-- La contraseña es obligatoria salvo para clientes: no necesitan loguearse
-- en este panel (usan el bot de WhatsApp). Si es cliente y no la mandan,
-- password_hash queda NULL.
IF new_password IS NULL AND new_rol <> 'cliente' THEN
RAISE EXCEPTION 'La contraseña es obligatoria';
END IF;
-- 3. Hashear la contraseña solo si vino (clientes pueden no traerla)
IF new_password IS NOT NULL THEN
SELECT internal.create_password_hash(new_password) INTO v_password_hash;
IF v_password_hash IS NULL THEN
RAISE EXCEPTION 'Error al generar el hash de la contraseña';
END IF;
ELSE
v_password_hash := NULL;
END IF;
-- 4. Insertar el nuevo usuario
-- CORRECCIÓN 2: Agregué 'tipo_cuota' y 'new_tipo_cuota' al INSERT
INSERT INTO public.usuarios (
dni, nombre, apellido, mail, telefono, peso, altura, fuerza_max,
sexo, rol, password_hash, isactive, tipo_cuota
) VALUES (
new_dni, new_nombre, new_apellido, new_mail, new_telefono, new_peso, new_altura, new_fuerza_max,
new_sexo, new_rol, v_password_hash, new_isactive, new_tipo_cuota
);
END;
$function$;
@@ -0,0 +1,41 @@
-- ============================================================================
-- fc_modificar_estado_usuario
-- ============================================================================
-- PROPÓSITO
-- Activa o desactiva un usuario (toggle de isactive). Pensado para "dar
-- de baja" sin perder el registro.
--
-- PARÁMETROS
-- p_token UUID sesión del actor.
-- p_id UUID ID del usuario.
-- p_estado BOOLEAN nuevo valor de isactive.
--
-- AUTORIZACIÓN
-- Requiere permiso 'desactivar_usuarios'. (El mismo permiso cubre
-- activación y desactivación.)
--
-- ERRORES (RAISE EXCEPTION)
-- Ninguno propio. Si p_id no existe, el UPDATE no afecta filas y la
-- función igualmente termina sin error.
--
-- RETORNA
-- void.
--
-- EFECTOS SECUNDARIOS
-- UPDATE de isactive en public.usuarios. NO toca fecha_modificacion.
-- ============================================================================
CREATE OR REPLACE FUNCTION public.fc_modificar_estado_usuario(p_token uuid, p_id uuid, p_estado boolean)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO public
AS $function$
BEGIN
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'desactivar_usuarios');
update public.usuarios
set isactive = p_estado
where id = p_id;
END;
$function$;
@@ -0,0 +1,44 @@
-- ============================================================================
-- fc_modificar_rol_usuario
-- ============================================================================
-- PROPÓSITO
-- Cambia el rol de un usuario y actualiza su fecha_modificacion.
-- NO valida que p_nuevo_rol sea un valor permitido; el constraint de
-- tabla es la última línea de defensa.
--
-- PARÁMETROS
-- p_uid UUID ID del usuario a modificar.
-- p_nuevo_rol TEXT nuevo valor de rol.
-- p_token UUID sesión del actor.
--
-- AUTORIZACIÓN
-- Requiere permiso 'modificar_rol_usuarios'.
--
-- ERRORES (RAISE EXCEPTION)
-- Ninguno propio (sólo los del validador de permiso o constraint de tabla).
-- Si p_uid no existe, no hace nada (no rebota).
--
-- RETORNA
-- void.
--
-- EFECTOS SECUNDARIOS
-- UPDATE en public.usuarios: setea rol y fecha_modificacion = now().
-- ============================================================================
CREATE OR REPLACE FUNCTION public.fc_modificar_rol_usuario(p_uid uuid, p_nuevo_rol text, p_token uuid)
RETURNS VOID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO public
AS $function$
DECLARE
BEGIN
-- Exigir el permiso específico para esta acción
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'modificar_rol_usuarios');
UPDATE public.usuarios
SET rol = p_nuevo_rol,
fecha_modificacion = now()
WHERE id = p_uid;
END;
$function$;
@@ -0,0 +1,141 @@
-- ============================================================================
-- fc_modificar_usuario
-- ============================================================================
-- PROPÓSITO
-- Modifica los datos de un usuario existente. Semántica PATCH: sólo se
-- actualizan las claves presentes en p_datos. Identifica al usuario por
-- id; si no viene, usa dni como fallback.
--
-- PARÁMETROS
-- p_datos JSONB con los campos a actualizar. Para identificar al usuario:
-- id UUID (preferido)
-- dni TEXT (fallback si no se pasa id)
-- Campos editables (todos opcionales):
-- nombre, apellido, peso, altura, mail, telefono, fuerza_max, sexo,
-- tipo_cuota, rol, password.
-- Caso especial - rol:
-- Si se pasa 'rol' y es distinto al actual, se delega a
-- fc_modificar_rol_usuario (que valida un permiso adicional). Tras la
-- delegación, la key 'rol' se elimina del UPDATE principal para evitar
-- escribir dos veces el mismo campo.
-- Caso especial - tipo_cuota:
-- Si se pasa la key con valor NULL, el plan se desasocia (queda NULL).
-- Si no se pasa la key, no se toca.
-- Caso especial - password:
-- Opcional; solo superadmin puede setearla (permiso dedicado). Si se
-- omite o viene vacía, no se toca la contraseña existente.
-- p_token UUID sesión del actor.
--
-- AUTORIZACIÓN
-- Requiere permiso 'modificar_usuarios'. Si se cambia el rol, además
-- requiere 'modificar_rol_usuarios' (impuesto por la función delegada).
-- Si se pasa 'password' no vacía, además requiere
-- 'resetear_contrasena_usuario'.
--
-- ERRORES (RAISE EXCEPTION)
-- - 'Se requiere un ID o un DNI para identificar al usuario.'
-- - 'Usuario no encontrado con los datos proporcionados.'
-- - 'El tipo_cuota X no existe.'
-- - 'Error al generar el hash de la contraseña'
--
-- RETORNA
-- void.
--
-- EFECTOS SECUNDARIOS
-- - UPDATE en public.usuarios con fecha_modificacion = now().
-- - Si hubo cambio de rol, además UPDATE adicional vía
-- fc_modificar_rol_usuario.
-- - Si vino 'password' no vacía, además actualiza password_hash.
-- ============================================================================
CREATE OR REPLACE FUNCTION public.fc_modificar_usuario(p_datos jsonb, p_token uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO extensions, public
AS $function$
DECLARE
v_isActive_new BOOLEAN := NULL;
v_rol_actual TEXT;
v_uid UUID;
v_password_hash TEXT;
BEGIN
-- Validar token
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'modificar_usuarios');
-- Evaluamos primero si el JSON trae un ID válido
IF p_datos ? 'id' AND p_datos->>'id' IS NOT NULL THEN
SELECT u.id, u.rol INTO v_uid, v_rol_actual
FROM public.usuarios u
WHERE u.id = (p_datos->>'id')::UUID
FOR UPDATE;
-- Si no hay ID, intentamos usar el DNI (fallback)
ELSIF p_datos ? 'dni' AND p_datos->>'dni' IS NOT NULL THEN
SELECT u.id, u.rol INTO v_uid, v_rol_actual
FROM public.usuarios u
WHERE u.dni = p_datos->>'dni'
FOR UPDATE;
-- Si no mandaron ni ID ni DNI, no sabemos a quién modificar
ELSE
RAISE EXCEPTION 'Se requiere un ID o un DNI para identificar al usuario.';
END IF;
-- Si hicimos la búsqueda pero no encontramos coincidencias
IF NOT FOUND THEN
RAISE EXCEPTION 'Usuario no encontrado con los datos proporcionados.';
END IF;
-- Validar existencia del tipo_cuota SOLO si viene en el JSON y NO es null
IF p_datos ? 'tipo_cuota' AND (p_datos->>'tipo_cuota') IS NOT NULL THEN
IF NOT EXISTS (
SELECT 1 FROM tipos_cuota
WHERE id = (p_datos->>'tipo_cuota')::UUID
) THEN
RAISE EXCEPTION 'El tipo_cuota % no existe.', p_datos->>'tipo_cuota';
END IF;
END IF;
-- Validar si viene un rol en el JSON y si es distinto al que ya está en la DB
IF p_datos ? 'rol' AND (p_datos->>'rol') IS DISTINCT FROM v_rol_actual THEN
-- Delegamos la responsabilidad a la nueva función
PERFORM public.fc_modificar_rol_usuario(v_uid, p_datos->>'rol', p_token);
-- Si llega hasta acá (no tiró excepción), eliminamos la key 'rol' del json
-- para que el UPDATE de abajo no lo sobreescriba innecesariamente.
p_datos := p_datos - 'rol';
END IF;
-- Password opcional: solo si viene la key Y no es null/vacía, se re-hashea
-- y se agrega al UPDATE. Acción sensible -> permiso dedicado, no alcanza
-- con 'modificar_usuarios' (mismo criterio que ya se usa para 'rol').
IF p_datos ? 'password' AND COALESCE(p_datos->>'password', '') <> '' THEN
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'resetear_contrasena_usuario');
v_password_hash := internal.create_password_hash(p_datos->>'password');
IF v_password_hash IS NULL THEN
RAISE EXCEPTION 'Error al generar el hash de la contraseña';
END IF;
ELSE
v_password_hash := NULL;
END IF;
UPDATE public.usuarios
SET
nombre = CASE WHEN p_datos ? 'nombre' THEN p_datos->>'nombre' ELSE nombre END,
apellido = CASE WHEN p_datos ? 'apellido' THEN p_datos->>'apellido' ELSE apellido END,
peso = CASE WHEN p_datos ? 'peso' THEN (p_datos->>'peso')::DECIMAL ELSE peso END,
altura = CASE WHEN p_datos ? 'altura' THEN (p_datos->>'altura')::DECIMAL ELSE altura END,
mail = CASE WHEN p_datos ? 'mail' THEN p_datos->>'mail' ELSE mail END,
telefono = CASE WHEN p_datos ? 'telefono' THEN p_datos->>'telefono' ELSE telefono END,
fuerza_max= CASE WHEN p_datos ? 'fuerza_max' THEN (p_datos->>'fuerza_max')::DECIMAL ELSE fuerza_max END,
sexo = CASE WHEN p_datos ? 'sexo' THEN p_datos->>'sexo' ELSE sexo END,
-- Esto ahora permite cambiar el tipo de cuota a NULL si mandas null, o cambiarlo a otro ID
tipo_cuota= CASE WHEN p_datos ? 'tipo_cuota' THEN (p_datos->>'tipo_cuota')::UUID ELSE tipo_cuota END,
password_hash = CASE WHEN v_password_hash IS NOT NULL THEN v_password_hash ELSE password_hash END,
fecha_modificacion = now()
WHERE id = v_uid;
END;
$function$;
@@ -0,0 +1,65 @@
-- ============================================================================
-- fc_obtener_usuario_por_token
-- ============================================================================
-- PROPÓSITO
-- Devuelve los datos del usuario asociado a una sesión vigente identificada
-- por p_user_token. El segundo parámetro (p_token) es la sesión del actor
-- que realiza la consulta y se usa sólo para autorizar.
--
-- PARÁMETROS
-- p_token UUID sesión del actor que llama (autorización).
-- p_user_token UUID token de la sesión del usuario que se quiere consultar.
-- La sesión debe estar vigente (expires_at > NOW()).
--
-- AUTORIZACIÓN
-- Requiere permiso 'ver_usuarios'.
--
-- ERRORES (RAISE EXCEPTION)
-- Ninguno propio (sólo los del validador de permiso).
--
-- RETORNA
-- JSONB objeto único con los datos del usuario (id, dni, nombre, apellido,
-- mail, telefono, peso, altura, sexo, fuerza_max, isActive, rol,
-- tipo_cuota, fecha_creacion, fecha_modificacion). Si p_user_token no
-- corresponde a una sesión vigente, retorna NULL.
--
-- EFECTOS SECUNDARIOS
-- Ninguno (STABLE).
-- ============================================================================
CREATE OR REPLACE FUNCTION public.fc_obtener_usuario_por_token(p_token UUID, p_user_token UUID)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
STABLE
AS $$
DECLARE
BEGIN
PERFORM internal.validate_permission(p_token, 'ver_usuarios');
RETURN (
SELECT jsonb_build_object(
'id', u.id,
'dni', u.dni,
'nombre', u.nombre,
'mail', u.mail,
'peso', u.peso,
'sexo', u.sexo,
'altura', u.altura,
'apellido', u.apellido,
'fecha_creacion', u.fecha_creacion,
'fecha_modificacion', u.fecha_modificacion,
'fuerza_max', u.fuerza_max,
'isActive', u.isactive,
'rol', u.rol,
'telefono', u.telefono,
'tipo_cuota', u.tipo_cuota
)
FROM public.usuarios u
JOIN internal.sesiones s ON s.usuario_id = u.id
AND s.token = p_user_token
AND s.expires_at > NOW()
);
END;
$$ LANGUAGE plpgsql;
@@ -0,0 +1,79 @@
-- ============================================================================
-- fc_obtener_usuarios
-- ============================================================================
-- PROPÓSITO
-- Devuelve un lote paginado de usuarios. Opcionalmente filtra por DNI.
--
-- PARÁMETROS
-- p_token UUID sesión del actor.
-- p_dni TEXT (opcional, default NULL) si se pasa, sólo retorna el
-- usuario con ese DNI. NULL = todos.
-- p_pagina INT (default 1) página, base 1.
-- p_cantidad INT (default 50) ítems por página.
--
-- AUTORIZACIÓN
-- Requiere permiso 'ver_usuarios'.
--
-- ERRORES (RAISE EXCEPTION)
-- Ninguno propio (sólo los del validador de permiso).
--
-- RETORNA
-- JSONB array (puede ser vacío). Cada ítem: id, dni, nombre, apellido,
-- peso, altura, fecha_creacion, fecha_modificacion, telefono, fuerza_max,
-- sexo, tipo_cuota, mail, rol, isActive. Ordenado por nombre, apellido.
--
-- EFECTOS SECUNDARIOS
-- Ninguno (STABLE).
-- ============================================================================
CREATE OR REPLACE FUNCTION fc_obtener_usuarios(
p_token UUID,
p_dni TEXT DEFAULT NULL,
p_pagina INT DEFAULT 1, -- Página actual (empieza en 1)
p_cantidad INT DEFAULT 50 -- Cuántos traer (default 30)
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
STABLE
AS $$
DECLARE
v_offset INT;
BEGIN
-- 1. Validar permisos
PERFORM internal.validate_permission(p_token, 'ver_usuarios');
-- 2. Calcular desde dónde empezar a leer (OFFSET)
-- Si página es 1, offset es 0. Si es 2, offset es 20, etc.
v_offset := (p_pagina - 1) * p_cantidad;
-- 3. Consulta corregida: Paginación dentro, JSON fuera
RETURN (
SELECT COALESCE(jsonb_agg(t.usuario_json), '[]'::jsonb)
FROM (
SELECT jsonb_build_object(
'id', u.id,
'dni', u.dni,
'nombre', u.nombre,
'apellido', u.apellido,
'peso', u.peso,
'altura', u.altura,
'fecha_creacion', u.fecha_creacion,
'fecha_modificacion', u.fecha_modificacion,
'telefono', u.telefono,
'fuerza_max', u.fuerza_max,
'sexo', u.sexo,
'tipo_cuota', u.tipo_cuota,
'mail', u.mail,
'rol', u.rol,
'isActive', u.isactive
) AS usuario_json
FROM public.usuarios u
WHERE (p_dni IS NULL OR u.dni = p_dni)
ORDER BY u.nombre, u.apellido -- Vital para que el OFFSET sea consistente
LIMIT p_cantidad
OFFSET v_offset
) t -- 't' es la subconsulta con solo 50 filas
);
END;
$$ LANGUAGE plpgsql;
@@ -0,0 +1,66 @@
-- ============================================================================
-- fc_resetear_contrasena_usuario
-- ============================================================================
-- PROPÓSITO
-- Permite al superadmin fijar una contraseña nueva para cualquier usuario
-- no-cliente (admin o superadmin), sin pedir ni validar la contraseña
-- actual del usuario objetivo. Reset administrativo sin restricción.
--
-- PARÁMETROS
-- p_token UUID sesión del actor (debe ser superadmin).
-- p_usuario_id UUID id del usuario objetivo (admin o superadmin).
-- p_password_nueva TEXT contraseña nueva en texto plano (mínimo 8
-- caracteres).
--
-- AUTORIZACIÓN
-- Requiere permiso 'resetear_contrasena_usuario' (solo superadmin).
--
-- ERRORES (RAISE EXCEPTION)
-- - 'La contraseña nueva debe tener al menos 8 caracteres.'
-- - 'Usuario no encontrado.'
-- - 'No se puede resetear la contraseña de un cliente: no inician sesión en este panel.'
--
-- RETORNA
-- void.
--
-- EFECTOS SECUNDARIOS
-- UPDATE de password_hash en public.usuarios, fila de p_usuario_id.
-- ============================================================================
CREATE OR REPLACE FUNCTION public.fc_resetear_contrasena_usuario(
p_token UUID,
p_usuario_id UUID,
p_password_nueva TEXT
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO extensions, public
AS $function$
DECLARE
v_rol_objetivo TEXT;
BEGIN
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'resetear_contrasena_usuario');
IF p_password_nueva IS NULL OR length(p_password_nueva) < 8 THEN
RAISE EXCEPTION 'La contraseña nueva debe tener al menos 8 caracteres.';
END IF;
SELECT rol INTO v_rol_objetivo
FROM public.usuarios
WHERE id = p_usuario_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Usuario no encontrado.';
END IF;
IF v_rol_objetivo = 'cliente' THEN
RAISE EXCEPTION 'No se puede resetear la contraseña de un cliente: no inician sesión en este panel.';
END IF;
UPDATE public.usuarios
SET password_hash = internal.create_password_hash(p_password_nueva)
WHERE id = p_usuario_id;
END;
$function$;