Entrada 6 (# Log in Back y Front)

 #Bitácora de Sesión
Fecha: 19/04/2026
Inicio: [12:00] | Fin: [16:30] || Total: [4 horas y 30 minutos]
Presentes: Matías Benavides Sandoval
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
¿QUÉ HICIMOS HOY?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Se completó el (login backend y frontend) 

1. public/login.html - Interfaz visual con formulario
2. src/frontend/login.ts - Lógica del lado cliente: validación, contador de bloqueo (600s)
3. src/frontend/AuthService.ts - Servicio centralizado para llamadas HTTP (login/logout)
4. src/routes/auth.ts - Definición de endpoints Express 
5. src/controllers/authController.ts - Lógica de negocio: validación de credenciales, generación de token
6. public/css/style.css- Estilos, animaciones y feedback visual

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PROBLEMAS ENCONTRADOS Y CÓMO SE RESOLVIERON
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1. Frontend TypeScript no ejecutaba en navegador (módulos ES6)

Problema: Los archivos compilados estaban como módulos ES6 (import/export), pero el script HTML los cargaba sin contexto de módulo.
Uncaught SyntaxError: Cannot use import statement outside a module

Solución:
- Cambiar `<script src="js/login.js">` a `<script type="module" src="js/login.js">` en public/login.html
- Agregar extensión `.js` explícita en imports: `import { AuthService } from './AuthService.js'`
- Recompilar con `npx tsc --project tsconfigFronted.json`
Resultado: Frontend carga correctamente, módulos ES6 resueltos por navegador 

2. Servidor crash al iniciar (npm run dev)
Problema: "argument handler is required" - archivo src/routes/empleados.ts tenía rutas Express sin handler functions.

Error: argument handler is required
at new Layer (node_modules/express/lib/router/layer.js:...)
at Router.route (node_modules/express/lib/router/index.js:...)
at Object.<anonymous> (src/routes/empleados.ts:...)

Solución: Agregar placeholders `(req: Request, res: Response) => res.json({ message: '...' })` a cada ruta.
Resultado: Servidor inicia sin errores 

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DUDAS Y DIVERGENCIAS DE CRITERIO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

No hubo dudas durante la implementación. La arquitectura y patrones estuvieron claros desde el inicio (ya que es basicamente el mismo que se uso en la primera tarea, entonces no hubo dudas).

Único punto de coordinación: Sebastian debe crear SPs de autenticación (sp_Login, sp_Logout) en paralelo.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AVANCE DEL CÓDIGO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Por ejemplo aca esta el login en TS
/**
* login.ts
* Lógica principal del formulario de login
*
* Este archivo se ejecuta cuando el usuario accede a login.html
* Maneja:
* - Captura del formulario
* - Validación de inputs
* - Llamadas al servicio
* - Mostrar mensajes y bloqueo
*/

import { AuthService } from './AuthService.js';

/**
* Clase LoginManager
* Gestiona toda la interfaz de usuario y lógica del login
*/
class LoginManager {
private authService: AuthService;
private loginForm: HTMLFormElement;
private usernameInput: HTMLInputElement;
private passwordInput: HTMLInputElement;
private messageDiv: HTMLElement;
private loginBtn: HTMLButtonElement;
private loadingSpinner: HTMLElement;
private blockedMessage: HTMLElement;
private blockedTime: HTMLElement;
private blockedCountdown: NodeJS.Timeout | null = null;

constructor() {
    // Inicializar el servicio de autenticación
    this.authService = new AuthService();

    // Obtener referencias a los elementos HTML
    // El "!" al final indica a TypeScript que garantizamos que existen
    this.loginForm = document.getElementById('loginForm') as HTMLFormElement;
    this.usernameInput = document.getElementById('username') as HTMLInputElement;
    this.passwordInput = document.getElementById('password') as HTMLInputElement;
    this.messageDiv = document.getElementById('message') as HTMLElement;
    this.loginBtn = document.getElementById('loginBtn') as HTMLButtonElement;
    this.loadingSpinner = document.getElementById('loadingSpinner') as HTMLElement;
    this.blockedMessage = document.getElementById('blockedMessage') as HTMLElement;
    this.blockedTime = document.getElementById('blockedTime') as HTMLElement;

    // Configurar listeners (escuchadores de eventos)
    this.setupEventListeners();
}

/**
* Configurar todos los listeners de eventos
* Esto se ejecuta cuando se instancia la clase
*/
private setupEventListeners(): void {
    // Cuando se envía el formulario
    this.loginForm.addEventListener('submit', (e) => this.handleSubmit(e));
}

/**
* Manejador del evento "submit" del formulario
* Se ejecuta cuando el usuario presiona "Iniciar Sesión"
*/
private async handleSubmit(e: Event): Promise<void> {
    // Prevenir que la página se recargue (comportamiento por defecto)
    e.preventDefault();

    // Obtener los valores del formulario
    const username = this.usernameInput.value.trim();
    const password = this.passwordInput.value;

    // Validar que los campos no estén vacíos
    if (!username || !password) {
        this.showMessage('Por favor completa todos los campos', 'error');
        return;
    }

    // Deshabilitar botón y mostrar spinner
    this.setLoading(true);

    try {
    // Llamar al servicio de login
    const response = await this.authService.login(username, password);

    // Verificar la respuesta
    if (response.success && response.outResultCode === 0) {
    //  LOGIN EXITOSO
    this.showMessage('¡Bienvenido! Redirigiendo...', 'success');

    // Guardar el token en localStorage para sesiones futuras
    if (response.token) {
        localStorage.setItem('authToken', response.token);
        localStorage.setItem('username', username);
    }

    // Redirigir a la página de inicio después de 1 segundo
    setTimeout(() => {
        window.location.href = '/dashboard.html';
    }, 1000);
    } else if (response.outResultCode === 50003) {
    // CUENTA BLOQUEADA
    this.showBlockedMessage(response.message);
    } else {
    // ERROR DE AUTENTICACIÓN
    this.showMessage(response.message, 'error');
    }
} catch (error) {
    // Manejo de errores inesperados
    console.error('Error inesperado:', error);
    this.showMessage('Ocurrió un error inesperado. Intenta de nuevo.', 'error');
} finally {
    // Siempre reabilitar el botón y ocultar spinner
    this.setLoading(false);
}
}

/**
* Mostrar un mensaje en la interfaz
*
* @param text - Texto del mensaje
* @param type - Tipo de mensaje: 'error', 'success', 'warning'
*/
private showMessage(text: string, type: 'error' | 'success' | 'warning'): void {
    // Limpiar clases previas
    this.messageDiv.className = 'message';

    // Agregar la clase correspondiente al tipo
    this.messageDiv.classList.add(type);

    // Mostrar el mensaje
    this.messageDiv.textContent = text;

    // Si es error, scroll al mensaje para que vea el usuario
    if (type === 'error') {
    this.messageDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    }
}

/**
* Mostrar mensaje de bloqueo por reintentos fallidos
* Incluye un contador regresivo
*
* @param message - Mensaje del servidor
*/
private showBlockedMessage(message: string): void {
    // Mostrar el contenedor de bloqueo
    this.blockedMessage.classList.remove('hidden');

    // Mostrar el mensaje en la consola (para debugging)
    console.warn('Cuenta bloqueada:', message);

    // Iniciar contador regresivo (10 minutos = 600 segundos)
    let remainingSeconds = 600;

    const updateCounter = () => {
    const minutes = Math.floor(remainingSeconds / 60);
    const seconds = remainingSeconds % 60;
    this.blockedTime.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;

    if (remainingSeconds > 0) {
        remainingSeconds--;
        // Actualizar cada segundo
        this.blockedCountdown = setTimeout(updateCounter, 1000);
    } else {
        // Cuando llega a 0, ocultar el bloqueo
        this.blockedMessage.classList.add('hidden');
        this.showMessage(
        'Tu cuenta ha sido desbloqueada. Intenta de nuevo.',
        'success'
        );
    }
    };

    // Iniciar el contador
    updateCounter();
}

/**
* Controlar el estado de carga
* Muestra/oculta el spinner y deshabilita el botón
*
* @param isLoading - true si está cargando, false si terminó
*/
private setLoading(isLoading: boolean): void {
    if (isLoading) {
    // Mostrar spinner y deshabilitar inputs
    this.loadingSpinner.classList.remove('hidden');
    this.loginBtn.disabled = true;
    this.usernameInput.disabled = true;
    this.passwordInput.disabled = true;
    } else {
      // Ocultar spinner y habilitar inputs
    this.loadingSpinner.classList.add('hidden');
    this.loginBtn.disabled = false;
    this.usernameInput.disabled = false;
    this.passwordInput.disabled = false;
    }
}
}

/**
* PUNTO DE ENTRADA
* Se ejecuta cuando el DOM está completamente cargado
*/
document.addEventListener('DOMContentLoaded', () => {
// Crear una instancia de LoginManager, que automáticamente
// configura todos los listeners y prepara la interfaz
new LoginManager();
});


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MORALEJAS / BUENAS PRÁCTICAS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1. Módulos ES6 en navegador requieren `type="module"` - No es suficiente compilar TypeScript a módulos ES6; el HTML debe indicarle al navegador que cargue como módulo.

2. Imports con extensión `.js` en módulos ES6 - Al usar `import` en navegador, la ruta DEBE incluir la extensión `.js` completa.

3. Arquitectura en capas facilita testeo - Separar Service → Route → Controller permite probar cada capa independientemente sin necesidad de interfaz gráfica.

4. Validación dual: frontend + backend - Frontend valida campos vacíos (UX), backend valida credenciales (seguridad).

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRÓXIMA SESIÓN: ¿QUÉ SIGUE?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


1. Crear public/empleados.html con tabla, filtros, botones CRUD
2. Crear src/frontend/empleados.ts con lógica de tabla + modales
3. Crear src/routes/empleados.ts con endpoints (GET, POST, PUT, DELETE)
4. Crear src/controllers/empleadosController.ts con lógica de negocio
5. Esperar SPs de Persona 1 (sp_GetEmpleados, sp_InsertEmpleado, sp_UpdateEmpleado, sp_DeleteEmpleado)



Comentarios

Entradas más populares de este blog

Entrada 16 (Controlador de movimientos)

Entrada 20 (Lógica de insertarMovimientos, conectarla y probarla)

Entrada 21 (Análisis de resultados)