diff --git a/apps/backend/src/controllers/authController.ts b/apps/backend/src/controllers/authController.ts index cdec9c9..7810aaf 100644 --- a/apps/backend/src/controllers/authController.ts +++ b/apps/backend/src/controllers/authController.ts @@ -6,7 +6,7 @@ import { z } from "zod"; import { prisma } from "../../prisma"; import { generateToken } from "../utils/generateToken"; import { UserRole } from "../../generated/prisma/enums"; -import { RegisterSchema, LoginSchema } from "../utils/authSchema"; +import { RegisterSchema, LoginSchema } from "../validations/authSchema"; import crypto from "crypto"; class AuthController { diff --git a/apps/backend/src/controllers/driverController.ts b/apps/backend/src/controllers/driverController.ts index dc579da..cb5abc7 100644 --- a/apps/backend/src/controllers/driverController.ts +++ b/apps/backend/src/controllers/driverController.ts @@ -2,6 +2,11 @@ import express from "express"; import { DriverService } from "../services/driverServices"; import { AuthRequest } from "../types/auth"; import { UserRole } from "../../generated/prisma/enums"; +import { + CreateDriverSchema, + UpdateDriverSchema, +} from "../validations/driverSchema"; +import { ZodError } from "zod"; /** * @swagger @@ -169,9 +174,14 @@ const getDriverById = async (req: AuthRequest, res: express.Response) => { * properties: * name: * type: string + * minLength: 2 + * maxLength: 100 * example: "Mario Hernández" * license_number: * type: string + * minLength: 3 + * maxLength: 50 + * pattern: "^[A-Z0-9-]+$" * example: "TX-DL-001234" * responses: * 201: @@ -181,7 +191,7 @@ const getDriverById = async (req: AuthRequest, res: express.Response) => { * schema: * $ref: '#/components/schemas/Driver' * 400: - * description: Name and License Number are required + * description: Validation error * 401: * description: Not authorized * 403: @@ -200,20 +210,25 @@ const createDriver = async (req: AuthRequest, res: express.Response) => { if (userRole !== UserRole.ADMIN) return res.status(403).json({ error: "Only Admins can create drivers" }); - const { name, license_number } = req.body; - if (!name || !license_number) { - return res - .status(400) - .json({ message: "Name and License Number are required" }); - } + // Validación con Zod + const validatedData = CreateDriverSchema.parse(req.body); - const newDriver = await DriverService.create( - { name, license_number }, - companyId, - ); + const newDriver = await DriverService.create(validatedData, companyId); res.status(201).json(newDriver); } catch (error: any) { + // Manejo de errores de Zod + if (error instanceof ZodError) { + return res.status(400).json({ + error: "Validation failed", + issues: error.issues.map((issue) => ({ + path: issue.path.join("."), + message: issue.message, + })), + }); + } + + // Manejo de errores del servicio if (error.message === "LICENSE_EXISTS") { return res.status(409).json({ message: @@ -227,6 +242,7 @@ const createDriver = async (req: AuthRequest, res: express.Response) => { "This driver exists but is inactive. Please reactivate them instead of creating a new one.", }); } + console.error(error); res.status(500).json({ error: "Error creating driver" }); } @@ -257,9 +273,14 @@ const createDriver = async (req: AuthRequest, res: express.Response) => { * properties: * name: * type: string + * minLength: 2 + * maxLength: 100 * example: "Mario Hernández" * license_number: * type: string + * minLength: 3 + * maxLength: 50 + * pattern: "^[A-Z0-9-]+$" * example: "TX-DL-001234" * is_active: * type: boolean @@ -271,6 +292,8 @@ const createDriver = async (req: AuthRequest, res: express.Response) => { * application/json: * schema: * $ref: '#/components/schemas/Driver' + * 400: + * description: Validation error * 401: * description: Not authorized * 404: @@ -287,25 +310,40 @@ const updateDriver = async (req: AuthRequest, res: express.Response) => { if (!companyId) return res.status(401).json({ error: "Not authorized" }); - const dataToUpdate = req.body; + // Validación con Zod + const validatedData = UpdateDriverSchema.parse(req.body); const updatedDriver = await DriverService.update( id as string, - dataToUpdate, + validatedData, companyId, ); res.status(200).json(updatedDriver); } catch (error: any) { + // Manejo de errores de Zod + if (error instanceof ZodError) { + return res.status(400).json({ + error: "Validation failed", + issues: error.issues.map((issue) => ({ + path: issue.path.join("."), + message: issue.message, + })), + }); + } + + // Manejo de errores del servicio if (error.message === "LICENSE_EXISTS") { return res .status(409) .json({ message: "License number already in use by another driver." }); } + console.error(error); if (error.message.includes("not found")) { return res.status(404).json({ error: "Driver not found" }); } + res.status(500).json({ message: "Error updating driver", error: error?.message, diff --git a/apps/backend/src/controllers/vehicleController.ts b/apps/backend/src/controllers/vehicleController.ts index 522fa5b..280ccae 100644 --- a/apps/backend/src/controllers/vehicleController.ts +++ b/apps/backend/src/controllers/vehicleController.ts @@ -1,6 +1,11 @@ import { Request, Response } from "express"; import { VehicleService } from "../services/vehicleServices"; import { AuthRequest } from "../types/auth"; +import { + CreateVehicleSchema, + UpdateVehicleSchema, +} from "../validations/vehicleSchema"; +import { ZodError } from "zod"; /** * @swagger @@ -55,13 +60,19 @@ import { AuthRequest } from "../types/auth"; * properties: * unit_number: * type: string + * maxLength: 50 * example: "TRUCK-042" * plate: * type: string + * maxLength: 20 + * pattern: "^[A-Z0-9-]+$" * example: "TX-ABC-1234" * is_active: * type: boolean * default: true + * driverId: + * type: string + * format: uuid * responses: * 201: * description: Vehicle created successfully @@ -75,6 +86,8 @@ import { AuthRequest } from "../types/auth"; * example: true * data: * $ref: '#/components/schemas/Vehicle' + * 400: + * description: Validation error * 401: * description: Not authorized * content: @@ -93,11 +106,25 @@ export const createVehicle = async (req: AuthRequest, res: Response) => { const companyId = req.user?.companyId; if (!companyId) return res.status(401).json({ error: "Not authorized" }); - const vehicleData = { ...req.body, companyId }; + // Validación con Zod + const validatedData = CreateVehicleSchema.parse(req.body); + + const vehicleData = { ...validatedData, companyId }; const vehicle = await VehicleService.create(vehicleData); res.status(201).json({ success: true, data: vehicle }); } catch (error: any) { + // Manejo de errores de Zod + if (error instanceof ZodError) { + return res.status(400).json({ + error: "Validation failed", + issues: error.issues.map((issue) => ({ + path: issue.path.join("."), + message: issue.message, + })), + }); + } + console.error(error); res.status(500).json({ success: false, @@ -245,13 +272,19 @@ export const getVehicleById = async (req: AuthRequest, res: Response) => { * properties: * unit_number: * type: string + * maxLength: 50 * example: "TRUCK-042-UPDATED" * plate: * type: string + * maxLength: 20 + * pattern: "^[A-Z0-9-]+$" * example: "TX-NEW-9999" * is_active: * type: boolean * example: true + * driverId: + * type: string + * format: uuid * responses: * 200: * description: Vehicle updated successfully @@ -265,6 +298,8 @@ export const getVehicleById = async (req: AuthRequest, res: Response) => { * example: true * data: * $ref: '#/components/schemas/Vehicle' + * 400: + * description: Validation error * 401: * description: Not authorized * 404: @@ -278,13 +313,27 @@ export const updateVehicle = async (req: AuthRequest, res: Response) => { const companyId = req.user?.companyId; if (!companyId) return res.status(401).json({ error: "Not authorized" }); + // Validación con Zod + const validatedData = UpdateVehicleSchema.parse(req.body); + const updated = await VehicleService.update( String(id), companyId, - req.body, + validatedData, ); res.json({ success: true, data: updated }); } catch (error: any) { + // Manejo de errores de Zod + if (error instanceof ZodError) { + return res.status(400).json({ + error: "Validation failed", + issues: error.issues.map((issue) => ({ + path: issue.path.join("."), + message: issue.message, + })), + }); + } + res.status(500).json({ success: false, error: error.message }); } }; diff --git a/apps/backend/src/utils/authSchema.ts b/apps/backend/src/validations/authSchema.ts similarity index 100% rename from apps/backend/src/utils/authSchema.ts rename to apps/backend/src/validations/authSchema.ts diff --git a/apps/backend/src/validations/driverSchema.ts b/apps/backend/src/validations/driverSchema.ts new file mode 100644 index 0000000..a70ecc8 --- /dev/null +++ b/apps/backend/src/validations/driverSchema.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +// Schema para crear un driver +export const CreateDriverSchema = z.object({ + name: z + .string() + .min(1, "Name is required") + .min(2, "Name must be at least 2 characters") + .max(100, "Name must not exceed 100 characters") + .trim(), + license_number: z + .string() + .min(1, "License number is required") + .min(3, "License number must be at least 3 characters") + .max(50, "License number must not exceed 50 characters") + .trim() + .regex( + /^[A-Z0-9-]+$/i, + "License number can only contain letters, numbers, and hyphens", + ), + is_active: z.boolean().optional().default(true), +}); + +// Schema para actualizar un driver +export const UpdateDriverSchema = z.object({ + name: z + .string() + .min(2, "Name must be at least 2 characters") + .max(100, "Name must not exceed 100 characters") + .trim() + .optional(), + license_number: z + .string() + .min(3, "License number must be at least 3 characters") + .max(50, "License number must not exceed 50 characters") + .trim() + .regex( + /^[A-Z0-9-]+$/i, + "License number can only contain letters, numbers, and hyphens", + ) + .optional(), + is_active: z.boolean().optional(), +}); + +// Exportar tipos inferidos +export type CreateDriverDTO = z.infer; +export type UpdateDriverDTO = z.infer; diff --git a/apps/backend/src/validations/vehicleSchema.ts b/apps/backend/src/validations/vehicleSchema.ts new file mode 100644 index 0000000..b2fd23c --- /dev/null +++ b/apps/backend/src/validations/vehicleSchema.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; + +// Schema para crear un driver +export const CreateVehicleSchema = z.object({ + unit_number: z + .string() + .min(1, "Unit number must not be empty") + .max(50, "Unit number must not exceed 50 characters") + .trim() + .optional(), + plate: z + .string() + .min(1, "Plate must not be empty") + .max(20, "Plate must not exceed 20 characters") + .trim() + .regex( + /^[A-Z0-9-]+$/i, + "Plate can only contain letters, numbers, and hyphens", + ) + .optional(), + is_active: z.boolean().optional().default(true), + driverId: z.uuid("Driver ID must be a valid UUID").optional(), +}); + +// Schema para actualizar un driver +export const UpdateVehicleSchema = z.object({ + unit_number: z + .string() + .min(1, "Unit number must not be empty") + .max(50, "Unit number must not exceed 50 characters") + .trim() + .optional(), + plate: z + .string() + .min(1, "Plate must not be empty") + .max(20, "Plate must not exceed 20 characters") + .trim() + .regex( + /^[A-Z0-9-]+$/i, + "Plate can only contain letters, numbers, and hyphens", + ) + .optional(), + is_active: z.boolean().optional(), + driverId: z.uuid("Driver ID must be a valid UUID").optional(), +}); + +// Exportar tipos inferidos +export type CreateVehicleDTO = z.infer; +export type UpdateVehicleDTO = z.infer;