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

137
water-api/src/utils/jwt.ts Normal file
View File

@@ -0,0 +1,137 @@
import jwt, { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
import config from '../config';
import logger from './logger';
interface TokenPayload {
id: string;
email?: string;
role?: string;
[key: string]: unknown;
}
/**
* Generate an access token
* @param payload - Data to encode in the token
* @returns Signed JWT access token
*/
export const generateAccessToken = (payload: TokenPayload): string => {
const options: SignOptions = {
expiresIn: config.jwt.accessTokenExpiresIn as SignOptions['expiresIn'],
algorithm: 'HS256',
};
try {
const token = jwt.sign(payload, config.jwt.accessTokenSecret, options);
return token;
} catch (error) {
logger.error('Error generating access token', {
error: error instanceof Error ? error.message : 'Unknown error',
});
throw new Error('Failed to generate access token');
}
};
/**
* Generate a refresh token
* @param payload - Data to encode in the token
* @returns Signed JWT refresh token
*/
export const generateRefreshToken = (payload: TokenPayload): string => {
const options: SignOptions = {
expiresIn: config.jwt.refreshTokenExpiresIn as SignOptions['expiresIn'],
algorithm: 'HS256',
};
try {
const token = jwt.sign(payload, config.jwt.refreshTokenSecret, options);
return token;
} catch (error) {
logger.error('Error generating refresh token', {
error: error instanceof Error ? error.message : 'Unknown error',
});
throw new Error('Failed to generate refresh token');
}
};
/**
* Verify an access token
* @param token - JWT access token to verify
* @returns Decoded payload if valid, null if invalid or expired
*/
export const verifyAccessToken = (token: string): JwtPayload | null => {
const options: VerifyOptions = {
algorithms: ['HS256'],
};
try {
const decoded = jwt.verify(
token,
config.jwt.accessTokenSecret,
options
) as JwtPayload;
return decoded;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
logger.debug('Access token expired');
} else if (error instanceof jwt.JsonWebTokenError) {
logger.debug('Invalid access token', {
error: error.message,
});
} else {
logger.error('Error verifying access token', {
error: error instanceof Error ? error.message : 'Unknown error',
});
}
return null;
}
};
/**
* Verify a refresh token
* @param token - JWT refresh token to verify
* @returns Decoded payload if valid, null if invalid or expired
*/
export const verifyRefreshToken = (token: string): JwtPayload | null => {
const options: VerifyOptions = {
algorithms: ['HS256'],
};
try {
const decoded = jwt.verify(
token,
config.jwt.refreshTokenSecret,
options
) as JwtPayload;
return decoded;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
logger.debug('Refresh token expired');
} else if (error instanceof jwt.JsonWebTokenError) {
logger.debug('Invalid refresh token', {
error: error.message,
});
} else {
logger.error('Error verifying refresh token', {
error: error instanceof Error ? error.message : 'Unknown error',
});
}
return null;
}
};
/**
* Decode a token without verification (for debugging)
* @param token - JWT token to decode
* @returns Decoded payload or null
*/
export const decodeToken = (token: string): JwtPayload | null => {
try {
const decoded = jwt.decode(token) as JwtPayload | null;
return decoded;
} catch (error) {
logger.error('Error decoding token', {
error: error instanceof Error ? error.message : 'Unknown error',
});
return null;
}
};