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>
120 lines
3.6 KiB
TypeScript
120 lines
3.6 KiB
TypeScript
import { z } from 'zod';
|
|
import { Request, Response, NextFunction } from 'express';
|
|
|
|
/**
|
|
* Schema for creating a new user
|
|
* - email: required, must be valid email format
|
|
* - password: required, minimum 8 characters
|
|
* - name: required (first_name + last_name combined or separate)
|
|
* - role_id: required, must be valid UUID
|
|
* - is_active: optional, defaults to true
|
|
*/
|
|
export const createUserSchema = z.object({
|
|
email: z
|
|
.string({ required_error: 'Email is required' })
|
|
.email('Invalid email format')
|
|
.transform((val) => val.toLowerCase().trim()),
|
|
password: z
|
|
.string({ required_error: 'Password is required' })
|
|
.min(8, 'Password must be at least 8 characters'),
|
|
first_name: z
|
|
.string({ required_error: 'First name is required' })
|
|
.min(1, 'First name cannot be empty')
|
|
.max(100, 'First name cannot exceed 100 characters'),
|
|
last_name: z
|
|
.string({ required_error: 'Last name is required' })
|
|
.min(1, 'Last name cannot be empty')
|
|
.max(100, 'Last name cannot exceed 100 characters'),
|
|
role_id: z
|
|
.number({ required_error: 'Role ID is required' })
|
|
.int('Role ID must be an integer')
|
|
.positive('Role ID must be a positive number'),
|
|
is_active: z.boolean().default(true),
|
|
});
|
|
|
|
/**
|
|
* Schema for updating a user
|
|
* All fields are optional
|
|
* Password has different rules (not allowed in regular update)
|
|
*/
|
|
export const updateUserSchema = z.object({
|
|
email: z
|
|
.string()
|
|
.email('Invalid email format')
|
|
.transform((val) => val.toLowerCase().trim())
|
|
.optional(),
|
|
first_name: z
|
|
.string()
|
|
.min(1, 'First name cannot be empty')
|
|
.max(100, 'First name cannot exceed 100 characters')
|
|
.optional(),
|
|
last_name: z
|
|
.string()
|
|
.min(1, 'Last name cannot be empty')
|
|
.max(100, 'Last name cannot exceed 100 characters')
|
|
.optional(),
|
|
role_id: z
|
|
.number()
|
|
.int('Role ID must be an integer')
|
|
.positive('Role ID must be a positive number')
|
|
.optional(),
|
|
is_active: z.boolean().optional(),
|
|
});
|
|
|
|
/**
|
|
* Schema for changing password
|
|
* - current_password: required
|
|
* - new_password: required, minimum 8 characters
|
|
*/
|
|
export const changePasswordSchema = z.object({
|
|
current_password: z
|
|
.string({ required_error: 'Current password is required' })
|
|
.min(1, 'Current password cannot be empty'),
|
|
new_password: z
|
|
.string({ required_error: 'New password is required' })
|
|
.min(8, 'New password must be at least 8 characters'),
|
|
});
|
|
|
|
/**
|
|
* Type definitions derived from schemas
|
|
*/
|
|
export type CreateUserInput = z.infer<typeof createUserSchema>;
|
|
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
|
export type ChangePasswordInput = z.infer<typeof changePasswordSchema>;
|
|
|
|
/**
|
|
* Generic validation middleware factory
|
|
* Creates a middleware that validates request body against a Zod schema
|
|
* @param schema - Zod schema to validate against
|
|
*/
|
|
export function validate<T extends z.ZodTypeAny>(schema: T) {
|
|
return (req: Request, res: Response, next: NextFunction): void => {
|
|
const result = schema.safeParse(req.body);
|
|
|
|
if (!result.success) {
|
|
const errors = result.error.errors.map((err) => ({
|
|
field: err.path.join('.'),
|
|
message: err.message,
|
|
}));
|
|
|
|
res.status(400).json({
|
|
success: false,
|
|
error: 'Validation failed',
|
|
errors,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Replace body with validated and typed data
|
|
req.body = result.data;
|
|
next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Pre-configured validation middlewares
|
|
*/
|
|
export const validateCreateUser = validate(createUserSchema);
|
|
export const validateUpdateUser = validate(updateUserSchema);
|
|
export const validateChangePassword = validate(changePasswordSchema);
|