Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions backend/src/middleware/validateRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,41 @@
import { formatZodErrors } from '../schemas.js';

/**
* Validates incoming request payload against Zod schema.
* Enforces explicit validation, type coercion, and uniform error responses.
* Returns error boundaries with structured details for client debugging.
*
* Creates middleware to validate `req.body` against a Zod schema.
* Returns uniform 400 error shape on validation failure:
* { error: "Validation failed", code: "VALIDATION_ERROR", details: [...] }
*
* @param {import('zod').ZodSchema} schema
* @param {Object} options
* @param {number} [options.maxPayloadSize=10485760] - max payload size in bytes (default 10MB)
*/
export function validateBody(schema) {
export function validateBody(schema, options = {}) {
const { maxPayloadSize = 10 * 1024 * 1024 } = options;

return (req, res, next) => {
if (!req.body) {
req.body = {};
}

const payloadSize = JSON.stringify(req.body).length;
if (payloadSize > maxPayloadSize) {
return res.status(413).json({
error: 'Payload too large',
code: 'PAYLOAD_TOO_LARGE',
details: [{ field: 'body', message: `Payload exceeds ${maxPayloadSize} bytes` }],
});
}

const result = schema.safeParse(req.body);
if (!result.success) {
const details = result.error.issues.map((issue) => ({
field: issue.path.join('.'),
field: issue.path.join('.') || 'root',
message: issue.message,
code: issue.code,
}));
return res.status(400).json({
error: 'Validation failed',
Expand All @@ -29,6 +51,9 @@ export function validateBody(schema) {
}

/**
* Validates query string parameters against Zod schema.
* Enforces type coercion and boundary checking.
*
* Creates middleware to validate `req.query` against a Zod schema.
* @param {import('zod').ZodSchema} schema
*/
Expand All @@ -37,8 +62,9 @@ export function validateQuery(schema) {
const result = schema.safeParse(req.query);
if (!result.success) {
const details = result.error.issues.map((issue) => ({
field: issue.path.join('.'),
field: issue.path.join('.') || 'root',
message: issue.message,
code: issue.code,
}));
return res.status(400).json({
error: 'Validation failed',
Expand All @@ -53,6 +79,9 @@ export function validateQuery(schema) {
}

/**
* Validates path parameters against Zod schema.
* Enforces defensive checks and type validation on URL segments.
*
* Creates middleware to validate `req.params` against a Zod schema.
* @param {import('zod').ZodSchema} schema
*/
Expand All @@ -61,8 +90,9 @@ export function validateParams(schema) {
const result = schema.safeParse(req.params);
if (!result.success) {
const details = result.error.issues.map((issue) => ({
field: issue.path.join('.'),
field: issue.path.join('.') || 'root',
message: issue.message,
code: issue.code,
}));
return res.status(400).json({
error: 'Validation failed',
Expand Down
68 changes: 68 additions & 0 deletions backend/src/schemas/validationRegistry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// @ts-check
import { z } from 'zod';

export const validationRegistry = {
request: {
pagination: z.object({
limit: z.coerce.number().int().min(1).max(1000).default(10),
offset: z.coerce.number().int().min(0).default(0),
cursor: z.string().optional(),
}),

identifiers: z.object({
id: z.string().trim().min(1),
userId: z.string().trim().min(1),
email: z.string().email('Invalid email format'),
campaignId: z.string().trim().min(1),
}),

filters: z.object({
status: z.enum(['active', 'inactive', 'archived']).optional(),
category: z.string().optional(),
search: z.string().max(255).optional(),
}),
},

response: {
error: z.object({
error: z.string(),
code: z.string(),
details: z.array(z.object({
field: z.string().optional(),
message: z.string(),
code: z.string().optional(),
})).optional(),
}),

success: z.object({
data: z.unknown(),
meta: z.object({
timestamp: z.string(),
version: z.string().optional(),
}).optional(),
}),
},

auth: {
credentials: z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
}),

token: z.object({
token: z.string().min(1),
expiresAt: z.string().datetime().optional(),
type: z.enum(['Bearer', 'Basic']).default('Bearer'),
}),
},
};

export const createValidationSchema = (registry, path) => {
const keys = path.split('.');
let schema = registry;
for (const key of keys) {
schema = schema[key];
if (!schema) throw new Error(`Schema not found at path: ${path}`);
}
return schema;
};
Loading