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,352 @@
import { Response } from 'express';
import { AuthenticatedRequest } from '../middleware/auth.middleware';
import * as userService from '../services/user.service';
import {
CreateUserInput,
UpdateUserInput,
ChangePasswordInput,
} from '../validators/user.validator';
/**
* GET /users
* List all users (admin only)
* Supports filtering by role_id, is_active, and search
* Supports pagination with page, limit, sortBy, sortOrder
*/
export async function getAllUsers(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
// Parse query parameters for filters
const filters: userService.UserFilter = {};
if (req.query.role_id) {
filters.role_id = parseInt(req.query.role_id as string, 10);
}
if (req.query.is_active !== undefined) {
filters.is_active = req.query.is_active === 'true';
}
if (req.query.search) {
filters.search = req.query.search as string;
}
// Parse pagination parameters
const pagination = {
page: parseInt(req.query.page as string, 10) || 1,
limit: parseInt(req.query.limit as string, 10) || 10,
sortBy: (req.query.sortBy as string) || 'created_at',
sortOrder: (req.query.sortOrder as 'asc' | 'desc') || 'desc',
};
const result = await userService.getAll(filters, pagination);
res.status(200).json({
success: true,
message: 'Users retrieved successfully',
data: result.users,
pagination: result.pagination,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to retrieve users';
res.status(500).json({
success: false,
error: message,
});
}
}
/**
* GET /users/:id
* Get a single user by ID (admin or self)
*/
export async function getUserById(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const userId = parseInt(req.params.id, 10);
if (isNaN(userId)) {
res.status(400).json({
success: false,
error: 'Invalid user ID',
});
return;
}
// Check if user is admin or requesting their own data
const requestingUser = req.user;
const isAdmin = requestingUser?.role === 'ADMIN';
const isSelf = requestingUser?.id === userId.toString();
if (!isAdmin && !isSelf) {
res.status(403).json({
success: false,
error: 'Insufficient permissions',
});
return;
}
const user = await userService.getById(userId);
if (!user) {
res.status(404).json({
success: false,
error: 'User not found',
});
return;
}
res.status(200).json({
success: true,
message: 'User retrieved successfully',
data: user,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to retrieve user';
res.status(500).json({
success: false,
error: message,
});
}
}
/**
* POST /users
* Create a new user (admin only)
*/
export async function createUser(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const data = req.body as CreateUserInput;
const user = await userService.create({
email: data.email,
password: data.password,
first_name: data.first_name,
last_name: data.last_name,
role_id: data.role_id,
is_active: data.is_active,
});
res.status(201).json({
success: true,
message: 'User created successfully',
data: user,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to create user';
if (message === 'Email already in use') {
res.status(409).json({
success: false,
error: message,
});
return;
}
res.status(500).json({
success: false,
error: message,
});
}
}
/**
* PUT /users/:id
* Update a user (admin can update all fields, regular users can only update limited fields on self)
*/
export async function updateUser(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const userId = parseInt(req.params.id, 10);
if (isNaN(userId)) {
res.status(400).json({
success: false,
error: 'Invalid user ID',
});
return;
}
const requestingUser = req.user;
const isAdmin = requestingUser?.role === 'ADMIN';
const isSelf = requestingUser?.id === userId.toString();
if (!isAdmin && !isSelf) {
res.status(403).json({
success: false,
error: 'Insufficient permissions',
});
return;
}
const data = req.body as UpdateUserInput;
// Non-admin users can only update their own profile fields (not role_id or is_active)
if (!isAdmin) {
if (data.role_id !== undefined || data.is_active !== undefined) {
res.status(403).json({
success: false,
error: 'You can only update your profile information',
});
return;
}
}
const user = await userService.update(userId, data);
if (!user) {
res.status(404).json({
success: false,
error: 'User not found',
});
return;
}
res.status(200).json({
success: true,
message: 'User updated successfully',
data: user,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to update user';
if (message === 'Email already in use') {
res.status(409).json({
success: false,
error: message,
});
return;
}
res.status(500).json({
success: false,
error: message,
});
}
}
/**
* DELETE /users/:id
* Deactivate a user (soft delete, admin only)
*/
export async function deleteUser(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const userId = parseInt(req.params.id, 10);
if (isNaN(userId)) {
res.status(400).json({
success: false,
error: 'Invalid user ID',
});
return;
}
// Prevent admin from deleting themselves
if (req.user?.id === userId.toString()) {
res.status(400).json({
success: false,
error: 'Cannot deactivate your own account',
});
return;
}
const deleted = await userService.deleteUser(userId);
if (!deleted) {
res.status(404).json({
success: false,
error: 'User not found',
});
return;
}
res.status(200).json({
success: true,
message: 'User deactivated successfully',
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to deactivate user';
res.status(500).json({
success: false,
error: message,
});
}
}
/**
* PUT /users/:id/password
* Change user password (self only)
*/
export async function changePassword(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const userId = parseInt(req.params.id, 10);
if (isNaN(userId)) {
res.status(400).json({
success: false,
error: 'Invalid user ID',
});
return;
}
// Only allow users to change their own password
if (req.user?.id !== userId.toString()) {
res.status(403).json({
success: false,
error: 'You can only change your own password',
});
return;
}
const data = req.body as ChangePasswordInput;
await userService.changePassword(
userId,
data.current_password,
data.new_password
);
res.status(200).json({
success: true,
message: 'Password changed successfully',
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to change password';
if (message === 'Current password is incorrect') {
res.status(401).json({
success: false,
error: message,
});
return;
}
if (message === 'User not found') {
res.status(404).json({
success: false,
error: message,
});
return;
}
res.status(500).json({
success: false,
error: message,
});
}
}