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:...)
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
Publicar un comentario