Migrar backend a PostgreSQL + Node.js/Express con nuevas funcionalidades

Backend (water-api/):
- Crear API REST completa con Express + TypeScript
- Implementar autenticación JWT con refresh tokens
- CRUD completo para: projects, concentrators, meters, gateways, devices, users, roles
- Agregar validación con Zod para todas las entidades
- Implementar webhooks para The Things Stack (LoRaWAN)
- Agregar endpoint de lecturas con filtros y resumen de consumo
- Implementar carga masiva de medidores via Excel (.xlsx)

Frontend:
- Crear cliente HTTP con manejo automático de JWT y refresh
- Actualizar todas las APIs para usar nuevo backend
- Agregar sistema de autenticación real (login, logout, me)
- Agregar selector de tipo (LORA, LoRaWAN, Grandes) en concentradores y medidores
- Agregar campo Meter ID en medidores
- Crear modal de carga masiva para medidores
- Agregar página de consumo con gráficas y filtros
- Corregir carga de proyectos independiente de datos existentes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Exteban08
2026-01-23 10:13:26 +00:00
parent 2b5735d78d
commit c81a18987f
92 changed files with 14088 additions and 1866 deletions

View File

@@ -0,0 +1,84 @@
import { Request, Response, NextFunction } from 'express';
import { verifyAccessToken } from '../utils/jwt';
/**
* Extended Request interface with authenticated user
*/
export interface AuthenticatedRequest extends Request {
user?: {
id: string;
email: string;
role: string;
};
}
/**
* Middleware to authenticate JWT access tokens
* Extracts Bearer token from Authorization header, verifies it,
* and attaches the decoded user to the request object
*/
export function authenticateToken(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void {
const authHeader = req.headers.authorization;
if (!authHeader) {
res.status(401).json({ error: 'Authorization header missing' });
return;
}
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
res.status(401).json({ error: 'Invalid authorization header format' });
return;
}
const token = parts[1];
try {
const decoded = verifyAccessToken(token);
if (!decoded) {
res.status(401).json({ error: 'Invalid or expired token' });
return;
}
req.user = {
id: decoded.id,
email: decoded.email,
role: decoded.role,
};
next();
} catch (error) {
res.status(401).json({ error: 'Invalid or expired token' });
}
}
/**
* Middleware factory for role-based access control
* Checks if the authenticated user has one of the required roles
* @param roles - Array of allowed roles
*/
export function requireRole(...roles: string[]) {
return (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void => {
if (!req.user) {
res.status(401).json({ error: 'Authentication required' });
return;
}
if (!roles.includes(req.user.role)) {
res.status(403).json({ error: 'Insufficient permissions' });
return;
}
next();
};
}