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:
352
water-api/src/controllers/user.controller.ts
Normal file
352
water-api/src/controllers/user.controller.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user