Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ El formato está basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1

> Las entradas entre la 0.6.0 y la 1.4.0 se reconstruyeron a posteriori a partir del historial de git, agrupadas por los saltos de versión que realmente ocurrieron en `package.json`. La 1.1.0 nunca existió: se pasó directamente de la 1.0.0 a la 1.2.0.

## [1.26.3] - 2026-08-23

### Security

- **Rechazar una solicitud de amistad que no existe devolvía la fila entera de la tabla.** El 404 incluía un `dbState` con el `SELECT *` de la amistad —`action_user_id`, el id de la otra persona, las marcas de tiempo— a quien acababa de fallar la comprobación de permisos, y había además un `console.log` de cada intento. Era depuración que se quedó puesta, y convertía un «no puedes» en un volcado de la tabla. Sus dos hermanos, aceptar y cancelar, responden con la frase a secas: este era el único de los tres que se salía del patrón.

### Fixed

- **El hub marcaba como vencida una tarea que vence hoy.** El contador comparaba con `new Date().toISOString()`, que da el día en UTC: en Bogotá, a partir de las siete de la tarde, el servidor ya cree que es mañana. `lib/fechas.ts` documenta ese caso exacto en su comentario y expone `vencida()` desde entonces —la tarjeta del tablero ya la usa—, pero este contador reimplementó la comparación por su cuenta y se trajo el fallo de vuelta. No falla nunca por la mañana, así que es de los que se descartan como manía de uno.

## [1.26.2] - 2026-08-20

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "forge-js",
"type": "module",
"version": "1.26.2",
"version": "1.26.3",
"engines": {
"node": ">=22.12.0"
},
Expand Down
19 changes: 15 additions & 4 deletions src/components/dashboard/Hub.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Logo from '../brand/Logo.astro';
import WorkspaceIcon from '../brand/WorkspaceIcon.astro';
import { mapaPorClave } from '../../lib/issueTypes';
import { nombreDeRol } from '../../lib/roles';
import { vencida } from '../../lib/fechas';
import { espaciosDelHub, tareasPendientes } from '../../lib/consultas';
// src/pages/index.astro
import MainLayout from '../../layouts/MainLayout.astro';
Expand Down Expand Up @@ -73,10 +74,20 @@ for (const wsId of new Set(myTasks.map((t) => t.workspace_id))) {
for (const [clave, tipo] of mapaPorClave(wsId)) tiposPendientes.set(`${wsId}:${clave}`, tipo);
}

// Vencidas: comparando solo la fecha, no la hora. Una entrega «de hoy» no está
// vencida a las nueve de la mañana.
const hoy = new Date().toISOString().slice(0, 10);
const vencidas = pendientes.filter((tarea) => tarea.due_date && String(tarea.due_date).slice(0, 10) < hoy).length;
/**
* Vencidas, con el helper y no a mano.
*
* Aquí había un `new Date().toISOString().slice(0, 10)`, que da el día en
* **UTC**. `lib/fechas.ts` documenta justo este caso en el comentario de
* `hoyLocal()`: en Bogotá, entre las siete de la tarde y medianoche, el
* servidor ya cree que es mañana y marca como vencido lo que vence hoy. Se
* veía como una tarea vencida que no lo estaba, cada tarde.
*
* `vencida()` existía desde entonces y `IssueCard.astro` ya la usa; este
* contador reimplementó la comparación por su cuenta y se trajo el fallo de
* vuelta.
*/
const vencidas = pendientes.filter((tarea) => vencida(tarea.due_date)).length;
---

<MainLayout title={t('hub.title')}>
Expand Down
21 changes: 13 additions & 8 deletions src/pages/api/friends/reject/[id].ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@ export const POST: APIRoute = async ({ request, params, locals }) => {
AND status = 'pending'
`).run(friendshipId, user.id, user.id, user.id);

console.log('Reject attempt:', { friendshipId, userId: user.id, changes: result.changes });

/**
* El 404 no cuenta nada de la fila.
*
* Aquí se devolvía `dbState` con el `SELECT *` entero de la amistad:
* `action_user_id`, el id de la otra persona y las marcas de tiempo, a
* quien acababa de fallar la comprobación de permisos. Era depuración que
* se quedó puesta —había también un `console.log` de cada intento— y
* convertía un «no puedes» en un volcado de la tabla.
*
* Sus dos hermanos, `accept/[id].ts` y `cancel/[id].ts`, responden con la
* frase a secas. Este era el único de los tres que se salía del patrón.
*/
if (result.changes === 0) {
return new Response(JSON.stringify({
error: 'Not Found or Unauthorized',
friendshipId,
userId: user.id,
dbState: db.prepare('SELECT * FROM friendships WHERE id = ?').get(friendshipId)
}), { status: 404 });
return new Response('Not Found or Unauthorized', { status: 404 });
}

return new Response(JSON.stringify({ success: true }), { status: 200 });
Expand Down
32 changes: 32 additions & 0 deletions tests/e2e/rechazo-sin-fuga.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { test, expect } from '@playwright/test';

/**
* Un 404 no cuenta nada de la fila.
*
* `api/friends/reject/[id].ts` devolvía `dbState` con el `SELECT *` entero de
* la amistad —`action_user_id`, el id de la otra persona, las marcas de
* tiempo— a quien acababa de fallar la comprobación de permisos. Era
* depuración que se quedó puesta, y convertía un «no puedes» en un volcado de
* la tabla. Sus dos hermanos responden con la frase a secas.
*/
test('el rechazo de una solicitud inexistente no filtra la fila', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="username"]', 'jose');
await page.fill('input[name="password"]', process.env.TEST_PASSWORD || 'LocalDevPass123!');
await page.click('button[type="submit"]');
await page.waitForURL('**/');

const res = await page.request.post('/api/friends/reject/no-existe-este-id', {
headers: { Origin: 'http://localhost:4322' },
});
expect(res.status()).toBe(404);

const cuerpo = await res.text();

// Ni el volcado, ni las columnas que llevaba dentro.
for (const filtrado of ['dbState', 'action_user_id', 'user_a_id', 'user_b_id', 'updated_at']) {
expect(cuerpo, `el 404 sigue contando «${filtrado}»`).not.toContain(filtrado);
}
// Y tampoco el id de quien pregunta, que tampoco pintaba nada ahí.
expect(cuerpo).not.toContain('userId');
});
47 changes: 47 additions & 0 deletions tests/vencidas-hub.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, it, expect } from 'vitest';
import { vencida, hoyLocal } from '../src/lib/fechas';

/**
* El contador de vencidas del Hub.
*
* `Hub.astro` comparaba con `new Date().toISOString().slice(0, 10)`, que da el
* día en **UTC**. En una zona con desfase negativo —Bogotá es UTC-5— eso
* significa que a partir de las siete de la tarde el servidor ya cree que es
* mañana, y una entrega de hoy aparece como vencida.
*
* No falla nunca por la mañana, así que es de los que se descartan como «cosa
* mía». Se fija con la hora clavada, que es lo único que lo reproduce.
*/
describe('una entrega de hoy no está vencida por la tarde', () => {
// 22:00 en Bogotá = 03:00 UTC del día siguiente.
const laNoche = new Date('2026-08-21T03:00:00Z');

it('la comparación en UTC daba mañana; la local da hoy', () => {
const enUtc = laNoche.toISOString().slice(0, 10);
const enLocal = hoyLocal(laNoche);

// Si esta prueba corre en UTC-5, los dos días son distintos: ahí estaba el
// fallo. En otra zona no se puede afirmar, pero la comparación de abajo
// vale igual.
if (enUtc !== enLocal) {
expect(enUtc > enLocal, 'UTC va por delante del día local').toBe(true);
}
});

it('una tarea que vence hoy no cuenta como vencida', () => {
const hoy = hoyLocal(laNoche);
expect(vencida(hoy, laNoche)).toBe(false);
});

it('una que venció ayer sí', () => {
const hoy = hoyLocal(laNoche);
const ayer = new Date(`${hoy}T12:00:00Z`);
ayer.setDate(ayer.getDate() - 1);
expect(vencida(ayer.toISOString().slice(0, 10), laNoche)).toBe(true);
});

it('sin fecha de entrega no está vencida', () => {
expect(vencida(null, laNoche)).toBe(false);
expect(vencida('', laNoche)).toBe(false);
});
});