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:
0
water-api/src/utils/.gitkeep
Normal file
0
water-api/src/utils/.gitkeep
Normal file
137
water-api/src/utils/jwt.ts
Normal file
137
water-api/src/utils/jwt.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
39
water-api/src/utils/logger.ts
Normal file
39
water-api/src/utils/logger.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import winston from 'winston';
|
||||
|
||||
const { combine, timestamp, printf, colorize, errors } = winston.format;
|
||||
|
||||
const logFormat = printf(({ level, message, timestamp, stack }) => {
|
||||
return `${timestamp} [${level}]: ${stack || message}`;
|
||||
});
|
||||
|
||||
const getLogLevel = (): string => {
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
switch (env) {
|
||||
case 'production':
|
||||
return 'warn';
|
||||
case 'test':
|
||||
return 'error';
|
||||
default:
|
||||
return 'debug';
|
||||
}
|
||||
};
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: getLogLevel(),
|
||||
format: combine(
|
||||
errors({ stack: true }),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' })
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: combine(
|
||||
colorize({ all: true }),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
logFormat
|
||||
),
|
||||
}),
|
||||
],
|
||||
exitOnError: false,
|
||||
});
|
||||
|
||||
export default logger;
|
||||
43
water-api/src/utils/password.ts
Normal file
43
water-api/src/utils/password.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import logger from './logger';
|
||||
|
||||
const SALT_ROUNDS = 12;
|
||||
|
||||
/**
|
||||
* Hash a password using bcrypt
|
||||
* @param password - Plain text password to hash
|
||||
* @returns Hashed password
|
||||
*/
|
||||
export const hashPassword = async (password: string): Promise<string> => {
|
||||
try {
|
||||
const salt = await bcrypt.genSalt(SALT_ROUNDS);
|
||||
const hashedPassword = await bcrypt.hash(password, salt);
|
||||
return hashedPassword;
|
||||
} catch (error) {
|
||||
logger.error('Error hashing password', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
throw new Error('Failed to hash password');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare a plain text password with a hashed password
|
||||
* @param password - Plain text password to compare
|
||||
* @param hash - Hashed password to compare against
|
||||
* @returns True if passwords match, false otherwise
|
||||
*/
|
||||
export const comparePassword = async (
|
||||
password: string,
|
||||
hash: string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const isMatch = await bcrypt.compare(password, hash);
|
||||
return isMatch;
|
||||
} catch (error) {
|
||||
logger.error('Error comparing passwords', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
throw new Error('Failed to compare passwords');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user