diff --git a/backend/config/db-config.ts b/backend/config/db-config.ts index 51e4fd91..71ca80bb 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/controllers/auth/user-controllers.ts b/backend/controllers/auth/user-controllers.ts index d790ab28..4ee441b6 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 } diff --git a/backend/scripts/checklist-report.ts b/backend/scripts/checklist-report.ts index a2a2bc0f..871927e9 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 c4023c18..fd1f19a9 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 }> { diff --git a/backend/tests/auth/demo-restriction.test.ts b/backend/tests/auth/demo-restriction.test.ts index f440ea6d..a65a259f 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 a3c14ce1..4c14dd2d 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 563a3a78..cca48613 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 23aa96c2..348a2bb4 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 diff --git a/docs/TODO.md b/docs/TODO.md index 473ba85c..4e0c8052 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