From 5878e37b781594594463ace17bad75d76c2e6b1a Mon Sep 17 00:00:00 2001 From: razyd Date: Fri, 9 Oct 2026 15:59:10 +0200 Subject: [PATCH 1/5] refactor(auth): type the remaining any - user-repository.ts: replace 6 'catch (error: any)' with 'catch (error)' and type assertions for .code and .message access - user-repository.ts: replace 'as any' with explicit type 'Error & { code?: string }' for the duplicate entry error - user-controllers.ts: replace 'catch (error: any)' with 'catch (error)' and type assertion for .message access - Pattern used: (error as { code?: string }).code and (error as Error).message --- backend/controllers/auth/user-controllers.ts | 6 ++-- backend/repositories/auth/user-repository.ts | 31 ++++++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/backend/controllers/auth/user-controllers.ts b/backend/controllers/auth/user-controllers.ts index d790ab2..4ee441b 100644 --- a/backend/controllers/auth/user-controllers.ts +++ b/backend/controllers/auth/user-controllers.ts @@ -143,10 +143,10 @@ export const resetUserPassword = async (req: Request, res: Response): Promise( @@ -397,19 +399,22 @@ export class UserRepository { // Devolver el usuario completo actualizado const updatedUser = await this.getById(id) return updatedUser - } catch (error: any) { + } catch (error) { await dbConnection.rollback() logger.error({ err: error }, 'Error en update') + const message = (error as Error).message + const code = (error as { code?: string }).code + if ( - error.message.includes('no encontrado') || - error.message.includes('no válido') || - error.message.includes('No hay campos') + message.includes('no encontrado') || + message.includes('no válido') || + message.includes('No hay campos') ) { throw error } - if (error.code === 'ER_DUP_ENTRY') { + if (code === 'ER_DUP_ENTRY') { throw new Error('El nombre de usuario o email ya existe', { cause: error }) } @@ -551,7 +556,7 @@ export class UserRepository { } return updatedUser - } catch (error: any) { + } catch (error) { await dbConnection.rollback() logger.error({ err: error }, 'Error en updateProfile') throw error @@ -613,7 +618,7 @@ export class UserRepository { logger.info({ event: 'password_changed', userId }, '[AUTH] password changed') return true - } catch (error: any) { + } catch (error) { await dbConnection.rollback() logger.error({ err: error }, 'Error en updatePassword') throw error @@ -646,7 +651,7 @@ export class UserRepository { logger.info({ event: 'password_reset_by_admin', userId }, '[AUTH] password reset by admin') return true - } catch (error: any) { + } catch (error) { logger.error({ err: error }, 'Error en resetPassword') throw error } From 40af16a6b159a3266d7bc1b18df8f369cf4154ea Mon Sep 17 00:00:00 2001 From: razyd Date: Fri, 9 Oct 2026 16:01:21 +0200 Subject: [PATCH 2/5] refactor(backend): type the remaining any in cron, config and scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - db-config.ts: ssl?: any → ssl?: object - cron-service.ts: results?: any → results?: unknown - checklist-report.ts: replace 9 any with proper types: - Import RowDataPacket from mysql2 - Define interfaces RunRow, StepRow, CommentRow extending RowDataPacket - Use db.query instead of (db as any).query - Replace (d: any) with (d: unknown) in fmt function - Remove all (s: any) and (c: any) from callbacks --- backend/config/db-config.ts | 2 +- backend/scripts/checklist-report.ts | 42 ++++++++++++++++++++------- backend/services/cron/cron-service.ts | 2 +- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/backend/config/db-config.ts b/backend/config/db-config.ts index 51e4fd9..71ca80b 100644 --- a/backend/config/db-config.ts +++ b/backend/config/db-config.ts @@ -26,7 +26,7 @@ interface DBConfig { user: string password: string | undefined database: string - ssl?: any + ssl?: object } // ======================================== diff --git a/backend/scripts/checklist-report.ts b/backend/scripts/checklist-report.ts index a2a2bc0..871927e 100644 --- a/backend/scripts/checklist-report.ts +++ b/backend/scripts/checklist-report.ts @@ -16,16 +16,38 @@ import db from '../config/db.js' import { getTodayMadrid } from '../config/date-utils.js' import { printable } from '../services/text/control-chars.js' +import type { RowDataPacket } from 'mysql2' const CHECKLIST_ID = process.argv[2] ?? 'cl-night-audit' const HOTEL_DATE = process.argv[3] ?? getTodayMadrid() -const fmt = (d: any) => +const fmt = (d: unknown) => d instanceof Date ? d.toISOString().slice(0, 19).replace('T', ' ') : String(d).slice(0, 19) +interface RunRow extends RowDataPacket { + id: number + checklist_id: string + hotel_date: string | Date + started_at: string | Date +} + +interface StepRow extends RowDataPacket { + step_id: string + done: number + done_at: string | Date | null + done_by: string | null +} + +interface CommentRow extends RowDataPacket { + step_id: string + comentario: string + created_at: string | Date + autor: string | null +} + async function main() { // 1. Encontrar el run - const [runs] = await (db as any).query( + const [runs] = await db.query( `SELECT id, checklist_id, hotel_date, started_at FROM checklist_runs WHERE checklist_id = ? AND DATE(hotel_date) = ? AND reset_at IS NULL @@ -43,7 +65,7 @@ async function main() { console.log(`\n=== ${run.checklist_id} | ${HOTEL_DATE} (run_id=${run.id}) ===\n`) // 2. Tareas completadas - const [steps] = await (db as any).query( + const [steps] = await db.query( `SELECT css.step_id, css.done, @@ -56,12 +78,12 @@ async function main() { [run.id] ) - const done = steps.filter((s: any) => s.done === 1) - const pending = steps.filter((s: any) => s.done !== 1) + const done = steps.filter((s) => s.done === 1) + const pending = steps.filter((s) => s.done !== 1) console.log(`Completadas (${done.length}):`) if (done.length) { - done.forEach((s: any) => + done.forEach((s) => console.log(` ✓ ${s.step_id.padEnd(12)} ${fmt(s.done_at)} por: ${s.done_by ?? '—'}`) ) } else { @@ -70,13 +92,13 @@ async function main() { console.log(`\nPendientes (${pending.length}):`) if (pending.length) { - pending.forEach((s: any) => console.log(` · ${s.step_id}`)) + pending.forEach((s) => console.log(` · ${s.step_id}`)) } else { console.log(' (ninguna)') } // 3. Comentarios - const [comments] = await (db as any).query( + const [comments] = await db.query( `SELECT csc.step_id, csc.body AS comentario, @@ -91,7 +113,7 @@ async function main() { console.log(`\nComentarios (${comments.length}):`) if (comments.length) { - comments.forEach((c: any) => + comments.forEach((c) => console.log( ` [${fmt(c.created_at)}] ${c.step_id} | ${printable(c.autor ?? '—')}: ${printable(c.comentario)}` ) @@ -104,6 +126,6 @@ async function main() { main() .then(() => process.exit(0)) .catch((e) => { - console.error(e.message) + console.error((e as Error).message) process.exit(1) }) diff --git a/backend/services/cron/cron-service.ts b/backend/services/cron/cron-service.ts index c4023c1..fd1f19a 100644 --- a/backend/services/cron/cron-service.ts +++ b/backend/services/cron/cron-service.ts @@ -157,7 +157,7 @@ export class CronService { */ static async runNotificationsNow(): Promise<{ success: boolean - results?: any + results?: unknown error?: string duration?: number }> { From 5102cc9b63ec0aa01ab678f4db66d7cb708e6e28 Mon Sep 17 00:00:00 2001 From: razyd Date: Fri, 9 Oct 2026 16:04:58 +0200 Subject: [PATCH 3/5] test: type the remaining any - demo-restriction.test.ts: replace 9 'as any' with 'as unknown as Request[...]' or 'as unknown' for socket, user, headers, and mock access - user-repository-login.test.ts: import RowDataPacket, replace db.query with db.query, replace (user as any) with (user as Record) - checklist.test.ts: import RowDataPacket, replace 7 'const [rows]: any' with 'const [rows] = await db.query', remove (r: any) from callback - employee-requests-repository.test.ts: import RowDataPacket, replace 'const [users]: any' with 'const [users] = await db.query' --- backend/tests/auth/demo-restriction.test.ts | 30 ++++++++++++------- .../tests/auth/user-repository-login.test.ts | 5 ++-- backend/tests/checklist/checklist.test.ts | 17 ++++++----- .../employee-requests-repository.test.ts | 3 +- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/backend/tests/auth/demo-restriction.test.ts b/backend/tests/auth/demo-restriction.test.ts index f440ea6..a65a259 100644 --- a/backend/tests/auth/demo-restriction.test.ts +++ b/backend/tests/auth/demo-restriction.test.ts @@ -25,7 +25,7 @@ function buildReq(overrides: Partial = {}): Request { headers: {}, body: {}, ip: '127.0.0.1', - socket: { remoteAddress: '127.0.0.1' } as any, + socket: { remoteAddress: '127.0.0.1' } as unknown, get: () => undefined, user: undefined, ...overrides, @@ -47,8 +47,13 @@ beforeEach(() => { // The public demo account (users.is_demo) is a real admin: only what is // declared here or on the route is blocked -const DEMO_USER = { id: 'd1', username: 'demo', role: 'admin', isDemo: true } as any -const OWNER = { id: 'a1', username: 'admin', role: 'admin' } as any +const DEMO_USER = { + id: 'd1', + username: 'demo', + role: 'admin', + isDemo: true, +} as unknown as Request['user'] +const OWNER = { id: 'a1', username: 'admin', role: 'admin' } as unknown as Request['user'] describe('demoRestriction — pass-through cases', () => { it('lets through requests without an authenticated user (handled upstream)', () => { @@ -61,7 +66,7 @@ describe('demoRestriction — pass-through cases', () => { it('lets through any non-demo role (admin, recepcionista, etc.)', () => { const req = buildReq({ - user: { id: 'u1', role: 'admin' } as any, + user: { id: 'u1', role: 'admin' } as unknown as Request['user'], method: 'DELETE', originalUrl: '/api/parking/bookings/123', }) @@ -85,8 +90,9 @@ describe('demoRestriction — blocked-attempt log', () => { }, }) denyDemo(req, buildRes(), next) - const logged = (DemoActivityRepository.logActivity as any).mock.calls[0][0] - .body_preview as string + const logged = ( + DemoActivityRepository.logActivity as unknown as { mock: { calls: unknown[][] } } + ).mock.calls[0][0].body_preview as string expect(logged).not.toMatch(/secret/) expect(JSON.parse(logged)).toEqual({ currentPassword: '[REDACTED]', @@ -105,7 +111,7 @@ describe('demoRestriction — demo account (is_demo)', () => { user: DEMO_USER, method: 'POST', originalUrl: '/api/maintenance/1/images', - headers: { 'content-type': contentType } as any, + headers: { 'content-type': contentType } as unknown as Request['headers'], }) const res = buildRes() demoRestriction(req, res, next) @@ -120,7 +126,7 @@ describe('demoRestriction — demo account (is_demo)', () => { user: DEMO_USER, method: 'DELETE', originalUrl: '/api/parking/bookings/ABC', - headers: { 'content-type': 'application/json' } as any, + headers: { 'content-type': 'application/json' } as unknown as Request['headers'], }) const res = buildRes() demoRestriction(req, res, next) @@ -131,7 +137,9 @@ describe('demoRestriction — demo account (is_demo)', () => { const req = buildReq({ user: OWNER, method: 'POST', - headers: { 'content-type': 'multipart/form-data; boundary=x' } as any, + headers: { + 'content-type': 'multipart/form-data; boundary=x', + } as unknown as Request['headers'], }) const res = buildRes() demoRestriction(req, res, next) @@ -190,7 +198,9 @@ describe('blocking a request without a JSON body', () => { user: DEMO_USER, method: 'POST', body: undefined, - headers: { 'content-type': 'multipart/form-data; boundary=x' } as any, + headers: { + 'content-type': 'multipart/form-data; boundary=x', + } as unknown as Request['headers'], }) const res = buildRes() demoRestriction(req, res, next) diff --git a/backend/tests/auth/user-repository-login.test.ts b/backend/tests/auth/user-repository-login.test.ts index a3c14ce..4c14dd2 100644 --- a/backend/tests/auth/user-repository-login.test.ts +++ b/backend/tests/auth/user-repository-login.test.ts @@ -7,6 +7,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest' import bcrypt from 'bcrypt' import db from '../../config/db.js' import { UserRepository } from '../../repositories/auth/user-repository.js' +import type { RowDataPacket } from 'mysql2' const TEST_USERNAME = 'authtest_user_h115' const TEST_PASSWORD = 'CorrectHorseBattery42!' @@ -35,7 +36,7 @@ beforeAll(async () => { ) // Usuario inactivo: reusa el demo si existe y is_active=0 (caso real tras H1-12). - const [rows] = await db.query( + const [rows] = await db.query( `SELECT id FROM users WHERE username = 'demo' AND is_active = 0 LIMIT 1` ) inactiveUserId = rows[0]?.id ?? null @@ -57,7 +58,7 @@ describe('UserRepository.login — integration', () => { expect(user.username).toBe(TEST_USERNAME) expect(user.email).toBe(TEST_EMAIL) expect(user.role).toBe('admin') - expect((user as any).password).toBeUndefined() + expect((user as Record).password).toBeUndefined() }) it('rejects with "Credenciales inválidas" when password is wrong', async () => { diff --git a/backend/tests/checklist/checklist.test.ts b/backend/tests/checklist/checklist.test.ts index 563a3a7..cca4861 100644 --- a/backend/tests/checklist/checklist.test.ts +++ b/backend/tests/checklist/checklist.test.ts @@ -9,6 +9,7 @@ import * as repo from '../../repositories/checklist/checklist-repository.js' import * as service from '../../services/checklist/checklist.service.js' import { getTodayMadrid } from '../../config/date-utils.js' import { createCommentSchema } from '../../validations/checklist/checklist-schemas.js' +import type { RowDataPacket } from 'mysql2' const TEST_CHECKLIST_ID = 'vitest-integration-checklist' const TEST_STEP_ID = 'vitest-step-1' @@ -55,7 +56,7 @@ describe('closeStaleRuns', () => { await repo.closeStaleRuns() - const [rows]: any = await db.query( + const [rows] = await db.query( `SELECT reset_at FROM checklist_runs WHERE checklist_id = ? AND hotel_date = ?`, [TEST_CHECKLIST_ID, today] ) @@ -72,7 +73,7 @@ describe('closeStaleRuns', () => { const affected = await repo.closeStaleRuns() expect(affected).toBeGreaterThanOrEqual(1) - const [rows]: any = await db.query( + const [rows] = await db.query( `SELECT reset_at, reset_reason, reset_by_user_id FROM checklist_runs WHERE checklist_id = ? AND hotel_date = ?`, [TEST_CHECKLIST_ID, STALE_DATE] @@ -123,7 +124,7 @@ describe('getRunState', () => { await service.getRunState(TEST_CHECKLIST_ID) - const [rows]: any = await db.query( + const [rows] = await db.query( `SELECT reset_at FROM checklist_runs WHERE checklist_id = ? AND hotel_date = ?`, [TEST_CHECKLIST_ID, STALE_DATE] ) @@ -141,7 +142,7 @@ describe('toggleStep', () => { await service.toggleStep(TEST_CHECKLIST_ID, TEST_STEP_ID, true, TEST_USER_ID) - const [rows]: any = await db.query( + const [rows] = await db.query( `SELECT css.done, css.done_by_user_id, css.done_at FROM checklist_step_state css JOIN checklist_runs cr ON cr.id = css.run_id @@ -160,7 +161,7 @@ describe('toggleStep', () => { await service.toggleStep(TEST_CHECKLIST_ID, TEST_STEP_ID, true, TEST_USER_ID) await service.toggleStep(TEST_CHECKLIST_ID, TEST_STEP_ID, false, TEST_USER_ID) - const [rows]: any = await db.query( + const [rows] = await db.query( `SELECT css.done, css.done_by_user_id, css.done_at FROM checklist_step_state css JOIN checklist_runs cr ON cr.id = css.run_id @@ -177,7 +178,7 @@ describe('toggleStep', () => { await service.toggleStep(TEST_CHECKLIST_ID, TEST_STEP_ID, true, TEST_USER_ID) - const [rows]: any = await db.query( + const [rows] = await db.query( `SELECT cel.action FROM checklist_event_log cel JOIN checklist_runs cr ON cr.id = cel.run_id WHERE cr.checklist_id = ? AND cel.step_id = ? @@ -193,14 +194,14 @@ describe('toggleStep', () => { await service.toggleStep(TEST_CHECKLIST_ID, TEST_STEP_ID, true, TEST_USER_ID) await service.toggleStep(TEST_CHECKLIST_ID, TEST_STEP_ID, false, TEST_USER_ID) - const [rows]: any = await db.query( + const [rows] = await db.query( `SELECT cel.action FROM checklist_event_log cel JOIN checklist_runs cr ON cr.id = cel.run_id WHERE cr.checklist_id = ? AND cel.step_id = ? ORDER BY cel.at ASC`, [TEST_CHECKLIST_ID, TEST_STEP_ID] ) - const actions = rows.map((r: any) => r.action) + const actions = rows.map((r) => r.action) expect(actions).toEqual(['check', 'uncheck']) }) }) diff --git a/backend/tests/scheduling/employee-requests-repository.test.ts b/backend/tests/scheduling/employee-requests-repository.test.ts index 23aa96c..348a2bb 100644 --- a/backend/tests/scheduling/employee-requests-repository.test.ts +++ b/backend/tests/scheduling/employee-requests-repository.test.ts @@ -5,6 +5,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest' import db from '../../config/db.js' +import type { RowDataPacket } from 'mysql2' // Only run integration tests when DB is available let dbAvailable = false @@ -43,7 +44,7 @@ describe('scheduling_employee_requests — integration', () => { } // Insert a test row and verify findByMonth retrieves it - const [users]: any = await db.query(`SELECT id FROM users LIMIT 1`) + const [users] = await db.query(`SELECT id FROM users LIMIT 1`) if (!users.length) { console.warn('Skipping: no users found in local DB') return From c569a07fce060352c17ca98a88dd2dc0cf4b0092 Mon Sep 17 00:00:00 2001 From: razyd Date: Fri, 9 Oct 2026 16:06:11 +0200 Subject: [PATCH 4/5] build(lint): make no-explicit-any an error - Change '@typescript-eslint/no-explicit-any' from 'warn' to 'error' in eslint.config.js - Remove outdated comment about 341 existing uses (now 0) - All code is now free of explicit any types --- backend/eslint.config.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/eslint.config.js b/backend/eslint.config.js index a83d519..2e79ce2 100644 --- a/backend/eslint.config.js +++ b/backend/eslint.config.js @@ -32,8 +32,7 @@ export default defineConfig( globals: globals.node, }, rules: { - // 341 existing uses: warn until they are typed, so new code is still flagged - '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-explicit-any': 'error', 'prefer-const': ['error', { destructuring: 'all' }], '@typescript-eslint/no-unused-vars': [ 'error', From fef59425ff630bf66b70ccc1166c6b11b16d4321 Mon Sep 17 00:00:00 2001 From: razyd Date: Sun, 11 Oct 2026 11:02:51 +0200 Subject: [PATCH 5/5] docs(todo): drop the remaining backend any, now typed - Remove the "55 uses of any left in the backend" entry. Why: auth, config, cron, scripts and tests are typed and no-explicit-any is an error again; the backend lint reports 0 problems. --- docs/TODO.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/TODO.md b/docs/TODO.md index 473ba85..4e0c805 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -105,11 +105,6 @@ preview. - [ ] **Comprobar si los cron se ejecutan en Render** — corren dentro del proceso del backend; si el plan gratuito lo duerme por inactividad, no se disparan. _No comprobado._ -- [ ] **Backend: quedan 55 usos de `any`** — `no-explicit-any` está como aviso - en `backend/eslint.config.js` para que el lint pase. El 2026-10-04 se - tiparon controllers, services y repositories de los 15 módulos (de 316 - avisos a 55); quedan auth, `config/db.ts`, cron, scripts y tests. Tiparlos - y volver a poner la regla como error. - [ ] **Avisar a los usuarios de que un `admin` puede leer sus mensajes** — `getConversation` y `getMessages` (`backend/controllers/messages/`) dejan leer cualquier conversación al rol `admin`, aunque no participe. Decidir