-- LOS INTENTOS FALLIDOS NUNCA SE ESTABAN GUARDANDO (2026-07-29)
--
-- Lo cazo la prueba de carga, y es un bug de seguridad de verdad, no un
-- detalle: el freno anti fuerza bruta era decorativo.
--
-- QUE PASABA. En fiscal.autenticar (y antes en emitir_con_llave) el codigo era:
--
--     insert into fiscal.intentos (...) values (..., false, 'llave invalida');
--     raise exception 'No autorizado';
--
-- En PostgreSQL, `raise exception` aborta la transaccion completa, y con ella
-- se va el insert de la linea anterior. Osea: cada intento fallido se borraba
-- solo. La tabla fiscal.intentos solo guardaba los EXITOSOS.
--
-- Consecuencia: el freno de "20 intentos fallidos en 5 minutos" contaba siempre
-- cero. Alguien podia probar llaves toda la noche sin que nada lo frenara y sin
-- dejar rastro.
--
-- COMO SE ARREGLA. El registro tiene que ocurrir FUERA de la transaccion que se
-- revienta. Se hace desde la Pages Function, que es otra transaccion: cuando
-- recibe el "No autorizado", anota el intento con esta funcion. Solo ocurre en
-- el caso de fallo, que es raro, asi que no cuesta nada en la operacion normal.
--
-- Se descarto meterle una transaccion autonoma con dblink: abrir una conexion
-- nueva por cada intento fallido es justo lo que un atacante querria provocar.
begin;

create or replace function public.fn_registrar_intento(
  p_rif text,
  p_motivo text,
  p_desde text default null
)
returns void
language sql
security definer
set search_path to 'public', 'fiscal'
as $fn$
  insert into fiscal.intentos (rif, entro, motivo, desde)
  values (left(coalesce(p_rif, '?'), 20), false, left(coalesce(p_motivo, 'no autorizado'), 100), p_desde);
$fn$;

revoke execute on function public.fn_registrar_intento(text, text, text) from anon, authenticated, public;
grant execute on function public.fn_registrar_intento(text, text, text) to service_role;

-- Se limpian los inserts que ya no van a ocurrir nunca (los que se revertian
-- solos), para que quede claro leyendo el codigo que el registro del fallo vive
-- afuera y no aca.
create or replace function fiscal.autenticar(p_llave text, p_rif text, p_motivo text)
returns uuid
language plpgsql
security definer
set search_path to 'fiscal', 'public', 'extensions'
as $fn$
declare
  v_emisor uuid;
  v_llave_id uuid;
  v_recientes int;
begin
  select id into v_emisor from fiscal.emisores
   where rif = public.fn_rif_normalizado(p_rif) and activo;
  -- OJO: no se registra el fallo aca. El `raise` de abajo revertiria el insert.
  -- Lo anota la Pages Function con public.fn_registrar_intento.
  if v_emisor is null then
    raise exception 'No autorizado';
  end if;

  -- CAMINO RAPIDO: un vistazo al indice, sin criptografia cara.
  select l.id into v_llave_id
    from fiscal.llaves l
   where l.emisor_id = v_emisor and l.activa
     and l.llave_sha = fiscal.huella_llave(p_llave);

  if v_llave_id is null then
    -- El freno va DESPUES del camino rapido a proposito: un cliente legitimo
    -- con muchas llamadas nunca lo toca, y quien esta probando llaves paga la
    -- consulta antes de que le hagamos bcrypt.
    select count(*) into v_recientes from fiscal.intentos
     where rif = public.fn_rif_normalizado(p_rif) and not entro and en > now() - interval '5 minutes';
    if v_recientes >= 20 then
      raise exception 'Demasiados intentos fallidos. Espera unos minutos.';
    end if;

    select l.id into v_llave_id
      from fiscal.llaves l
     where l.emisor_id = v_emisor and l.activa and l.llave_sha is null
       and l.llave_hash = extensions.crypt(p_llave, l.llave_hash)
     limit 1;

    if v_llave_id is null then
      raise exception 'No autorizado';
    end if;
    update fiscal.llaves set llave_sha = fiscal.huella_llave(p_llave) where id = v_llave_id;
  end if;

  update fiscal.llaves set ultimo_uso = now() where id = v_llave_id;
  return v_emisor;
end;
$fn$;

create or replace function fiscal.emitir_con_llave(
  p_llave text,
  p_rif text,
  p_serie text,
  p_tipo text,
  p_numero_documento bigint,
  p_hash text,
  p_fecha timestamptz,
  p_base_bs numeric,
  p_iva_bs numeric,
  p_total_bs numeric,
  p_documento jsonb,
  p_desde text default null
)
returns table(numero_control bigint, token text, ya_estaba boolean)
language plpgsql
security definer
set search_path to 'fiscal', 'public', 'extensions'
as $fn$
declare v_emisor uuid;
begin
  v_emisor := fiscal.autenticar(p_llave, p_rif, coalesce(p_desde, 'emision'));
  insert into fiscal.intentos (rif, entro, motivo, desde) values (p_rif, true, 'emision', p_desde);

  return query select * from fiscal.recibir_documento(
    p_rif, p_serie, p_tipo, p_numero_documento, p_hash, p_fecha,
    p_base_bs, p_iva_bs, p_total_bs, p_documento
  );
end;
$fn$;

commit;
