Skip to content

Commit a5b873d

Browse files
authored
Merge pull request #80 from gbengaeben/fix/issue-44-restore-joi-validation
fix(backend): restore strict Joi validation on /api/v1/smart-wallet/* (#44)
2 parents 5e254fb + 4fa77a7 commit a5b873d

5 files changed

Lines changed: 566 additions & 326 deletions

File tree

backend/src/middleware/validateRequestSchema.ts

Lines changed: 89 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
11
/**
2-
* validateRequestSchema — Lightweight Joi-based validation middleware factory
2+
* validateRequestSchema — Lightweight Joi-based validation middleware factory.
33
*
4-
* Extracted from middleware/validation.ts to break a babel-jest CommonJS
5-
* evaluation-order crash. Because this module has zero heavy dependencies
6-
* (no Joi import, no express-validator, no VersionControlUtils), it can
7-
* be required safely by route files that call the factory at module-load
8-
* time (e.g. smartWallet.ts:24).
4+
* This module is intentionally dependency-free (no `joi`, no
5+
* `express-validator`, no `VersionControlUtils`) so it can be `require`d
6+
* by route files that call the factory at module-load time without
7+
* crashing under babel-jest's CommonJS evaluation order (the root cause
8+
* of Issue #44). Routes pass in any Joi schema object with a
9+
* `validate(value)` method; we treat it as a duck-typed `SchemaLike`.
10+
*
11+
* Behaviour:
12+
* * Validates `req.body`, `req.query`, and `req.params` against the
13+
* provided Joi schemas, aggregating every error (not just the first)
14+
* with field/message context.
15+
* * On failure, responds with the standard envelope used elsewhere in
16+
* the codebase:
17+
* { success: false, message: 'Validation failed', errors: [...] }
18+
* * On success, replaces each source with the validated (and unknown-
19+
* stripped) value before calling `next()`.
920
*/
1021

1122
import { Request, Response, NextFunction } from 'express';
1223

13-
/**
14-
* Minimal structural interface for a Joi-like schema object.
15-
* We deliberately avoid importing Joi so this module stays lean
16-
* and does not trigger the babel-jest circular-eval bug.
17-
*/
24+
/** Minimal structural interface for a Joi-like schema object. */
25+
type Detail = { path: Array<string | number>; message: string; type?: string };
26+
27+
/** Minimal structural interface for a Joi-like schema object. */
1828
export interface SchemaLike {
19-
validate(value: unknown): { error?: { details: Array<{ message: string }> } };
29+
validate(
30+
value: unknown,
31+
options?: { stripUnknown?: boolean; abortEarly?: boolean }
32+
): {
33+
error?: { details: Detail[] };
34+
value?: unknown;
35+
};
2036
}
2137

2238
export interface ValidationSchema {
@@ -25,42 +41,77 @@ export interface ValidationSchema {
2541
params?: SchemaLike;
2642
}
2743

44+
interface NormalizedError {
45+
source: 'body' | 'query' | 'params';
46+
field: string;
47+
message: string;
48+
}
49+
50+
const VALIDATE_OPTIONS = {
51+
abortEarly: false,
52+
stripUnknown: true,
53+
} as const;
54+
55+
function safePath(path: Detail['path'] | undefined | null): string {
56+
if (!path || !Array.isArray(path) || path.length === 0) {
57+
return '(root)';
58+
}
59+
return path.join('.');
60+
}
61+
62+
function collect(source: 'body' | 'query' | 'params', schema: SchemaLike, payload: unknown): {
63+
errors: NormalizedError[];
64+
value: unknown;
65+
} {
66+
const result = schema.validate(payload, VALIDATE_OPTIONS);
67+
if (!result.error) {
68+
return { errors: [], value: result.value ?? payload };
69+
}
70+
const errors: NormalizedError[] = result.error.details.map((detail) => ({
71+
source,
72+
field: safePath(detail.path),
73+
message: detail.message,
74+
}));
75+
return { errors, value: result.value ?? payload };
76+
}
77+
2878
/**
29-
* Factory that returns Express middleware which validates
30-
* req.body, req.query, and/or req.params against Joi schemas.
79+
* Factory that returns Express middleware which validates req.body,
80+
* req.query, and/or req.params against Joi schemas.
3181
*/
3282
export function validateRequestSchema(schema: ValidationSchema) {
3383
return (req: Request, res: Response, next: NextFunction): void => {
34-
const errors: string[] = [];
35-
36-
// Validate request body
37-
if (schema.body) {
38-
const { error } = schema.body.validate(req.body);
39-
if (error) {
40-
errors.push(`Body: ${error.details[0].message}`);
41-
}
42-
}
43-
44-
// Validate query parameters
45-
if (schema.query) {
46-
const { error } = schema.query.validate(req.query);
47-
if (error) {
48-
errors.push(`Query: ${error.details[0].message}`);
49-
}
50-
}
84+
const sources: Array<'body' | 'query' | 'params'> = ['body', 'query', 'params'];
85+
const errors: NormalizedError[] = [];
5186

52-
// Validate route parameters
53-
if (schema.params) {
54-
const { error } = schema.params.validate(req.params);
55-
if (error) {
56-
errors.push(`Params: ${error.details[0].message}`);
87+
for (const source of sources) {
88+
const sub = schema[source];
89+
if (!sub) continue;
90+
const outcome = collect(source, sub, req[source]);
91+
if (outcome.errors.length) {
92+
errors.push(...outcome.errors);
5793
}
94+
// Replace the request source with the validated/sanitized value.
95+
// Request.body, Request.query, Request.params are all `any` on
96+
// Express's Request, so we use an explicit switch rather than
97+
// a `Record<string, unknown>` cast (which trips TS2352).
98+
// Replace the request source with the validated/sanitized value.
99+
// Express types `req.query` as `ParsedQs` and `req.params` as
100+
// `ParamsDictionary` (both stricter than `unknown`), so we widen
101+
// locally — the value has already been validated/stripped by Joi
102+
// in collect() above.
103+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
104+
req.query = (source === 'query' ? outcome.value : req.query) as any;
105+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
106+
req.params = (source === 'params' ? outcome.value : req.params) as any;
107+
req.body = source === 'body' ? outcome.value : req.body;
58108
}
59109

60110
if (errors.length > 0) {
61111
res.status(400).json({
62-
error: 'Validation failed',
63-
details: errors,
112+
success: false,
113+
message: 'Validation failed',
114+
errors,
64115
});
65116
return;
66117
}

0 commit comments

Comments
 (0)