millas 0.2.13 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -3
- package/src/admin/Admin.js +107 -1027
- package/src/admin/AdminAuth.js +1 -1
- package/src/admin/ViewContext.js +1 -1
- package/src/admin/handlers/ActionHandler.js +103 -0
- package/src/admin/handlers/ApiHandler.js +113 -0
- package/src/admin/handlers/AuthHandler.js +76 -0
- package/src/admin/handlers/ExportHandler.js +70 -0
- package/src/admin/handlers/InlineHandler.js +71 -0
- package/src/admin/handlers/PageHandler.js +351 -0
- package/src/admin/resources/AdminResource.js +22 -1
- package/src/admin/static/SelectFilter2.js +34 -0
- package/src/admin/static/actions.js +201 -0
- package/src/admin/static/admin.css +7 -0
- package/src/admin/static/change_form.js +585 -0
- package/src/admin/static/core.js +128 -0
- package/src/admin/static/login.js +76 -0
- package/src/admin/static/vendor/bi/bootstrap-icons.min.css +5 -0
- package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff +0 -0
- package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff2 +0 -0
- package/src/admin/static/vendor/jquery.min.js +2 -0
- package/src/admin/views/layouts/base.njk +30 -113
- package/src/admin/views/pages/detail.njk +10 -9
- package/src/admin/views/pages/form.njk +4 -4
- package/src/admin/views/pages/list.njk +11 -193
- package/src/admin/views/pages/login.njk +19 -64
- package/src/admin/views/partials/form-field.njk +1 -1
- package/src/admin/views/partials/form-scripts.njk +4 -478
- package/src/admin/views/partials/form-widget.njk +10 -10
- package/src/ai/AITokenBudget.js +1 -1
- package/src/auth/Auth.js +112 -3
- package/src/auth/AuthMiddleware.js +18 -15
- package/src/auth/Hasher.js +15 -43
- package/src/cli.js +3 -0
- package/src/commands/call.js +190 -0
- package/src/commands/createsuperuser.js +3 -4
- package/src/commands/key.js +97 -0
- package/src/commands/make.js +16 -2
- package/src/commands/new.js +16 -1
- package/src/commands/serve.js +5 -5
- package/src/console/Command.js +337 -0
- package/src/console/CommandLoader.js +165 -0
- package/src/console/index.js +6 -0
- package/src/container/AppInitializer.js +48 -1
- package/src/container/Application.js +3 -1
- package/src/container/HttpServer.js +0 -1
- package/src/container/MillasConfig.js +48 -0
- package/src/controller/Controller.js +13 -11
- package/src/core/docs.js +6 -0
- package/src/core/foundation.js +8 -0
- package/src/core/http.js +20 -10
- package/src/core/validation.js +58 -27
- package/src/docs/Docs.js +268 -0
- package/src/docs/DocsServiceProvider.js +80 -0
- package/src/docs/SchemaInferrer.js +131 -0
- package/src/docs/handlers/ApiHandler.js +305 -0
- package/src/docs/handlers/PageHandler.js +47 -0
- package/src/docs/index.js +13 -0
- package/src/docs/resources/ApiResource.js +402 -0
- package/src/docs/static/docs.css +723 -0
- package/src/docs/static/docs.js +1181 -0
- package/src/encryption/Encrypter.js +381 -0
- package/src/facades/Auth.js +5 -2
- package/src/facades/Crypt.js +166 -0
- package/src/facades/Docs.js +43 -0
- package/src/facades/Mail.js +1 -1
- package/src/http/MillasRequest.js +7 -31
- package/src/http/RequestContext.js +11 -7
- package/src/http/SecurityBootstrap.js +24 -2
- package/src/http/Shape.js +168 -0
- package/src/http/adapters/ExpressAdapter.js +9 -5
- package/src/middleware/CorsMiddleware.js +3 -0
- package/src/middleware/ThrottleMiddleware.js +10 -7
- package/src/orm/model/Model.js +14 -1
- package/src/providers/EncryptionServiceProvider.js +66 -0
- package/src/router/MiddlewareRegistry.js +79 -54
- package/src/router/Route.js +9 -4
- package/src/router/RouteEntry.js +91 -0
- package/src/router/Router.js +71 -1
- package/src/scaffold/maker.js +138 -1
- package/src/scaffold/templates.js +12 -0
- package/src/serializer/Serializer.js +239 -0
- package/src/support/Str.js +1080 -0
- package/src/validation/BaseValidator.js +45 -5
- package/src/validation/Validator.js +67 -61
- package/src/validation/types.js +490 -0
- package/src/middleware/AuthMiddleware.js +0 -46
- package/src/middleware/MiddlewareRegistry.js +0 -106
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Millas Validation — Typed Field Builders
|
|
5
|
+
*
|
|
6
|
+
* Fluent, chainable validators that serve two purposes simultaneously:
|
|
7
|
+
* 1. Runtime validation — run by Validator.validate() when a shape is applied
|
|
8
|
+
* to a route. Failures short-circuit to 422 before the handler runs.
|
|
9
|
+
* 2. Docs generation — read by SchemaInferrer to build the ApiField schema
|
|
10
|
+
* shown in the docs panel. .example() and .describe() are docs-only.
|
|
11
|
+
*
|
|
12
|
+
* All builders extend BaseValidator and are exported from millas/core/validation.
|
|
13
|
+
*
|
|
14
|
+
* ── Usage ─────────────────────────────────────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* const { string, number, boolean, array, email, date, file, object } =
|
|
17
|
+
* require('millas/core/validation');
|
|
18
|
+
*
|
|
19
|
+
* // In a shape:
|
|
20
|
+
* shape({
|
|
21
|
+
* name: string().required().max(200).example('Jane Doe'),
|
|
22
|
+
* email: email().required().example('jane@example.com'),
|
|
23
|
+
* age: number().optional().min(13).max(120).example(25),
|
|
24
|
+
* active: boolean().optional().example(true),
|
|
25
|
+
* tags: array().of(string()).optional().example(['admin', 'user']),
|
|
26
|
+
* role: string().required().oneOf(['admin', 'user', 'guest']),
|
|
27
|
+
* avatar: file().optional().maxSize('5mb').mimeType(['image/jpeg','image/png']),
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* // Inline in body.validate():
|
|
31
|
+
* const data = await body.validate({
|
|
32
|
+
* name: string().required().max(100),
|
|
33
|
+
* email: email().required(),
|
|
34
|
+
* });
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const { BaseValidator, _titleCase } = require('./BaseValidator');
|
|
38
|
+
|
|
39
|
+
// ── StringValidator ────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
class StringValidator extends BaseValidator {
|
|
42
|
+
constructor() {
|
|
43
|
+
super('Must be a string');
|
|
44
|
+
this._type = 'string';
|
|
45
|
+
this._minLen = null;
|
|
46
|
+
this._maxLen = null;
|
|
47
|
+
this._oneOfVals = null;
|
|
48
|
+
this._emailCheck = false;
|
|
49
|
+
this._urlCheck = false;
|
|
50
|
+
this._uuidCheck = false;
|
|
51
|
+
this._phoneCheck = false;
|
|
52
|
+
this._regexCheck = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_checkType(value) {
|
|
56
|
+
if (typeof value !== 'string') return this._typeError || 'Must be a string';
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Minimum character length */
|
|
61
|
+
min(n, msg) {
|
|
62
|
+
this._minLen = n;
|
|
63
|
+
return this._addRule(
|
|
64
|
+
v => typeof v === 'string' && v.length >= n,
|
|
65
|
+
msg || `Must be at least ${n} character${n !== 1 ? 's' : ''}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Maximum character length */
|
|
70
|
+
max(n, msg) {
|
|
71
|
+
this._maxLen = n;
|
|
72
|
+
return this._addRule(
|
|
73
|
+
v => typeof v === 'string' && v.length <= n,
|
|
74
|
+
msg || `Must not exceed ${n} character${n !== 1 ? 's' : ''}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Restrict to a set of allowed string values */
|
|
79
|
+
oneOf(values, msg) {
|
|
80
|
+
this._oneOfVals = values;
|
|
81
|
+
return this._addRule(
|
|
82
|
+
v => values.includes(v),
|
|
83
|
+
msg || `Must be one of: ${values.join(', ')}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Must be a valid email address */
|
|
88
|
+
email(msg) {
|
|
89
|
+
this._emailCheck = true;
|
|
90
|
+
const re = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
|
91
|
+
return this._addRule(
|
|
92
|
+
v => re.test(String(v)),
|
|
93
|
+
msg || 'Must be a valid email address'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Must be a valid http/https URL */
|
|
98
|
+
url(msg) {
|
|
99
|
+
this._urlCheck = true;
|
|
100
|
+
return this._addRule(v => {
|
|
101
|
+
try {
|
|
102
|
+
const u = new URL(String(v));
|
|
103
|
+
return ['http:', 'https:'].includes(u.protocol);
|
|
104
|
+
} catch { return false; }
|
|
105
|
+
}, msg || 'Must be a valid URL');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Must be a valid UUID */
|
|
109
|
+
uuid(msg) {
|
|
110
|
+
this._uuidCheck = true;
|
|
111
|
+
const re = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
112
|
+
return this._addRule(v => re.test(String(v)), msg || 'Must be a valid UUID');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Must match a regex pattern */
|
|
116
|
+
regex(pattern, msg) {
|
|
117
|
+
this._regexCheck = pattern;
|
|
118
|
+
const re = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
119
|
+
return this._addRule(v => re.test(String(v)), msg || 'Format is invalid');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Alias for .regex() — matches the docs API */
|
|
123
|
+
matches(pattern, msg) {
|
|
124
|
+
return this.regex(pattern, msg);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Must be a valid phone number (E.164 or common formats) */
|
|
128
|
+
phone(msg) {
|
|
129
|
+
this._phoneCheck = true;
|
|
130
|
+
return this._addRule(
|
|
131
|
+
v => /^\+?[\d\s\-().]{7,20}$/.test(String(v)),
|
|
132
|
+
msg || 'Must be a valid phone number'
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Value must match a corresponding <field>_confirmation field in the data.
|
|
138
|
+
* Used for password confirmation:
|
|
139
|
+
* password: string().required().min(8).confirmed()
|
|
140
|
+
* // Automatically checks that password_confirmation matches
|
|
141
|
+
*/
|
|
142
|
+
confirmed(msg) {
|
|
143
|
+
this._confirmed = true;
|
|
144
|
+
// The check function receives (value, allData) — we need allData
|
|
145
|
+
// Store a flag; Validator.validate() handles the cross-field check
|
|
146
|
+
// via a custom rule that captures the field name at bind time.
|
|
147
|
+
// We use _addRule with a placeholder that gets replaced in validate().
|
|
148
|
+
this._rules.push({
|
|
149
|
+
_isConfirmed: true,
|
|
150
|
+
message: msg || null,
|
|
151
|
+
});
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Coerce to string after validation */
|
|
156
|
+
_coerce(value) { return String(value); }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── EmailValidator — convenience alias for string().email() ──────────────────
|
|
160
|
+
|
|
161
|
+
class EmailValidator extends StringValidator {
|
|
162
|
+
constructor() {
|
|
163
|
+
super();
|
|
164
|
+
this._type = 'email';
|
|
165
|
+
this.email();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── NumberValidator ────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
class NumberValidator extends BaseValidator {
|
|
172
|
+
constructor() {
|
|
173
|
+
super('Must be a number');
|
|
174
|
+
this._type = 'number';
|
|
175
|
+
this._minVal = null;
|
|
176
|
+
this._maxVal = null;
|
|
177
|
+
this._isInt = false;
|
|
178
|
+
this._isPos = false;
|
|
179
|
+
this._oneOfVals = null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_checkType(value) {
|
|
183
|
+
const n = Number(value);
|
|
184
|
+
if (isNaN(n)) return this._typeError || 'Must be a number';
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Minimum value */
|
|
189
|
+
min(n, msg) {
|
|
190
|
+
this._minVal = n;
|
|
191
|
+
return this._addRule(
|
|
192
|
+
v => Number(v) >= n,
|
|
193
|
+
msg || `Must be at least ${n}`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Maximum value */
|
|
198
|
+
max(n, msg) {
|
|
199
|
+
this._maxVal = n;
|
|
200
|
+
return this._addRule(
|
|
201
|
+
v => Number(v) <= n,
|
|
202
|
+
msg || `Must not exceed ${n}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Must be a whole number */
|
|
207
|
+
integer(msg) {
|
|
208
|
+
this._isInt = true;
|
|
209
|
+
return this._addRule(
|
|
210
|
+
v => Number.isInteger(Number(v)),
|
|
211
|
+
msg || 'Must be a whole number'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Must be greater than zero */
|
|
216
|
+
positive(msg) {
|
|
217
|
+
this._isPos = true;
|
|
218
|
+
return this._addRule(
|
|
219
|
+
v => Number(v) > 0,
|
|
220
|
+
msg || 'Must be a positive number'
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Restrict to allowed numeric values */
|
|
225
|
+
oneOf(values, msg) {
|
|
226
|
+
this._oneOfVals = values;
|
|
227
|
+
return this._addRule(
|
|
228
|
+
v => values.includes(Number(v)),
|
|
229
|
+
msg || `Must be one of: ${values.join(', ')}`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_coerce(value) { return Number(value); }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── BooleanValidator ──────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
class BooleanValidator extends BaseValidator {
|
|
239
|
+
constructor() {
|
|
240
|
+
super('Must be a boolean');
|
|
241
|
+
this._type = 'boolean';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_checkType(value) {
|
|
245
|
+
const acceptable = [true, false, 'true', 'false', '1', '0', 1, 0, 'yes', 'no'];
|
|
246
|
+
if (!acceptable.includes(value)) return this._typeError || 'Must be a boolean';
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_coerce(value) {
|
|
251
|
+
if (value === true || value === 'true' || value === 1 || value === '1' || value === 'yes') return true;
|
|
252
|
+
if (value === false || value === 'false' || value === 0 || value === '0' || value === 'no') return false;
|
|
253
|
+
return Boolean(value);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── ArrayValidator ────────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
class ArrayValidator extends BaseValidator {
|
|
260
|
+
constructor() {
|
|
261
|
+
super('Must be an array');
|
|
262
|
+
this._type = 'array';
|
|
263
|
+
this._itemValidator = null;
|
|
264
|
+
this._minLen = null;
|
|
265
|
+
this._maxLen = null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
_checkType(value) {
|
|
269
|
+
if (!Array.isArray(value)) return this._typeError || 'Must be an array';
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Validate each item in the array with another validator.
|
|
275
|
+
* array().of(string().min(2))
|
|
276
|
+
* array().of(number().positive())
|
|
277
|
+
*/
|
|
278
|
+
of(validator) {
|
|
279
|
+
this._itemValidator = validator;
|
|
280
|
+
return this._addRule((arr, allData) => {
|
|
281
|
+
if (!Array.isArray(arr)) return true; // type check handles this
|
|
282
|
+
for (let i = 0; i < arr.length; i++) {
|
|
283
|
+
const typeErr = validator._checkType(arr[i], `[${i}]`);
|
|
284
|
+
if (typeErr) return false;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}, `Each item ${this._itemValidator?._typeError || 'is invalid'}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Minimum number of items */
|
|
291
|
+
min(n, msg) {
|
|
292
|
+
this._minLen = n;
|
|
293
|
+
return this._addRule(
|
|
294
|
+
arr => Array.isArray(arr) && arr.length >= n,
|
|
295
|
+
msg || `Must have at least ${n} item${n !== 1 ? 's' : ''}`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Maximum number of items */
|
|
300
|
+
max(n, msg) {
|
|
301
|
+
this._maxLen = n;
|
|
302
|
+
return this._addRule(
|
|
303
|
+
arr => Array.isArray(arr) && arr.length <= n,
|
|
304
|
+
msg || `Must have no more than ${n} item${n !== 1 ? 's' : ''}`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── DateValidator ─────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
class DateValidator extends BaseValidator {
|
|
312
|
+
constructor() {
|
|
313
|
+
super('Must be a valid date');
|
|
314
|
+
this._type = 'date';
|
|
315
|
+
this._beforeVal = null;
|
|
316
|
+
this._afterVal = null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
_checkType(value) {
|
|
320
|
+
const d = new Date(value);
|
|
321
|
+
if (isNaN(d.getTime())) return this._typeError || 'Must be a valid date';
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Date must be before this value */
|
|
326
|
+
before(date, msg) {
|
|
327
|
+
this._beforeVal = date;
|
|
328
|
+
const d = new Date(date);
|
|
329
|
+
return this._addRule(
|
|
330
|
+
v => new Date(v) < d,
|
|
331
|
+
msg || `Must be before ${date}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Date must be after this value */
|
|
336
|
+
after(date, msg) {
|
|
337
|
+
this._afterVal = date;
|
|
338
|
+
const d = new Date(date);
|
|
339
|
+
return this._addRule(
|
|
340
|
+
v => new Date(v) > d,
|
|
341
|
+
msg || `Must be after ${date}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Date must be in the past (before now) */
|
|
346
|
+
past(msg) {
|
|
347
|
+
return this._addRule(
|
|
348
|
+
v => new Date(v) < new Date(),
|
|
349
|
+
msg || 'Must be a past date'
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Date must be in the future (after now) */
|
|
354
|
+
future(msg) {
|
|
355
|
+
return this._addRule(
|
|
356
|
+
v => new Date(v) > new Date(),
|
|
357
|
+
msg || 'Must be a future date'
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── ObjectValidator ───────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
class ObjectValidator extends BaseValidator {
|
|
365
|
+
/**
|
|
366
|
+
* @param {object} [schema] — optional nested field schema
|
|
367
|
+
* object({ street: string().required(), city: string().required() })
|
|
368
|
+
*/
|
|
369
|
+
constructor(schema) {
|
|
370
|
+
super('Must be an object');
|
|
371
|
+
this._type = 'object';
|
|
372
|
+
this._schema = schema || null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
_checkType(value) {
|
|
376
|
+
if (typeof value !== 'object' || Array.isArray(value) || value === null) {
|
|
377
|
+
return this._typeError || 'Must be an object';
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Validate nested object fields (fluent alternative to constructor arg) */
|
|
383
|
+
shape(schema) {
|
|
384
|
+
this._schema = schema;
|
|
385
|
+
return this;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── FileValidator ─────────────────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
class FileValidator extends BaseValidator {
|
|
392
|
+
constructor() {
|
|
393
|
+
super('Must be a file');
|
|
394
|
+
this._type = 'file';
|
|
395
|
+
this._maxSizeBytes = null;
|
|
396
|
+
this._mimeTypes = null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
_checkType(value) {
|
|
400
|
+
// Files come through as multer file objects — just check it's present
|
|
401
|
+
if (!value || typeof value !== 'object') return this._typeError || 'Must be a file';
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Maximum file size.
|
|
407
|
+
* Accepts bytes (number) or human string: '5mb', '500kb', '1gb'
|
|
408
|
+
*/
|
|
409
|
+
maxSize(size, msg) {
|
|
410
|
+
const bytes = typeof size === 'number' ? size : _parseSize(size);
|
|
411
|
+
this._maxSizeBytes = bytes;
|
|
412
|
+
return this._addRule(
|
|
413
|
+
v => !v?.size || v.size <= bytes,
|
|
414
|
+
msg || `File must not exceed ${size}`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Allowed MIME types.
|
|
420
|
+
* file().mimeTypes(['image/jpeg', 'image/png'])
|
|
421
|
+
* file().mimeTypes('image/jpeg')
|
|
422
|
+
*/
|
|
423
|
+
mimeTypes(types, msg) {
|
|
424
|
+
const allowed = Array.isArray(types) ? types : [types];
|
|
425
|
+
this._mimeTypes = allowed;
|
|
426
|
+
return this._addRule(
|
|
427
|
+
v => !v?.mimetype || allowed.includes(v.mimetype),
|
|
428
|
+
msg || `Must be one of: ${allowed.join(', ')}`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Alias for backwards compatibility */
|
|
433
|
+
mimeType(types, msg) { return this.mimeTypes(types, msg); }
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Must be an image (jpeg, png, gif, webp, svg).
|
|
437
|
+
* file().image()
|
|
438
|
+
* file().image('Please upload an image')
|
|
439
|
+
*/
|
|
440
|
+
image(msg) {
|
|
441
|
+
const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
|
|
442
|
+
this._mimeTypes = imageTypes;
|
|
443
|
+
return this._addRule(
|
|
444
|
+
v => !v?.mimetype || imageTypes.includes(v.mimetype),
|
|
445
|
+
msg || 'Must be an image (jpeg, png, gif, webp, svg)'
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function _parseSize(str) {
|
|
451
|
+
const s = String(str).toLowerCase().trim();
|
|
452
|
+
const n = parseFloat(s);
|
|
453
|
+
if (s.endsWith('gb')) return n * 1024 * 1024 * 1024;
|
|
454
|
+
if (s.endsWith('mb')) return n * 1024 * 1024;
|
|
455
|
+
if (s.endsWith('kb')) return n * 1024;
|
|
456
|
+
return n;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Factory functions ─────────────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
const string = () => new StringValidator();
|
|
462
|
+
const email = () => new EmailValidator();
|
|
463
|
+
const number = () => new NumberValidator();
|
|
464
|
+
const boolean = () => new BooleanValidator();
|
|
465
|
+
const array = () => new ArrayValidator();
|
|
466
|
+
const date = () => new DateValidator();
|
|
467
|
+
const object = (schema) => new ObjectValidator(schema);
|
|
468
|
+
const file = () => new FileValidator();
|
|
469
|
+
|
|
470
|
+
module.exports = {
|
|
471
|
+
// Classes (for instanceof checks in SchemaInferrer)
|
|
472
|
+
BaseValidator,
|
|
473
|
+
StringValidator,
|
|
474
|
+
EmailValidator,
|
|
475
|
+
NumberValidator,
|
|
476
|
+
BooleanValidator,
|
|
477
|
+
ArrayValidator,
|
|
478
|
+
DateValidator,
|
|
479
|
+
ObjectValidator,
|
|
480
|
+
FileValidator,
|
|
481
|
+
// Factory functions
|
|
482
|
+
string,
|
|
483
|
+
email,
|
|
484
|
+
number,
|
|
485
|
+
boolean,
|
|
486
|
+
array,
|
|
487
|
+
date,
|
|
488
|
+
object,
|
|
489
|
+
file,
|
|
490
|
+
};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const Middleware = require('./Middleware');
|
|
4
|
-
const HttpError = require('../errors/HttpError');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* AuthMiddleware
|
|
8
|
-
*
|
|
9
|
-
* Guards routes from unauthenticated access.
|
|
10
|
-
* Full JWT/session implementation unlocked in Phase 7.
|
|
11
|
-
*
|
|
12
|
-
* For now: checks for presence of Authorization header.
|
|
13
|
-
* Replace the verify() method with real token logic in Phase 7.
|
|
14
|
-
*
|
|
15
|
-
* Register:
|
|
16
|
-
* middlewareRegistry.register('auth', AuthMiddleware);
|
|
17
|
-
*
|
|
18
|
-
* Apply:
|
|
19
|
-
* Route.prefix('/api').middleware(['auth']).group(() => { ... });
|
|
20
|
-
*/
|
|
21
|
-
class AuthMiddleware extends Middleware {
|
|
22
|
-
async handle(req, res, next) {
|
|
23
|
-
const header = req.headers['authorization'];
|
|
24
|
-
|
|
25
|
-
if (!header) {
|
|
26
|
-
throw new HttpError(401, 'No authorization token provided');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const [scheme, token] = header.split(' ');
|
|
30
|
-
|
|
31
|
-
if (scheme !== 'Bearer' || !token) {
|
|
32
|
-
throw new HttpError(401, 'Invalid authorization format. Use: Bearer <token>');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Phase 7 will replace this with real JWT verification:
|
|
36
|
-
// const user = await Auth.verifyToken(token);
|
|
37
|
-
// req.user = user;
|
|
38
|
-
|
|
39
|
-
// For now: attach a stub user so downstream handlers don't break
|
|
40
|
-
req.user = { id: null, token, authenticated: false, _stub: true };
|
|
41
|
-
|
|
42
|
-
next();
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
module.exports = AuthMiddleware;
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* MiddlewareRegistry
|
|
5
|
-
*
|
|
6
|
-
* Maps string aliases → Millas middleware classes or instances.
|
|
7
|
-
* Resolution produces adapter-native handler functions via the adapter,
|
|
8
|
-
* so this class has zero knowledge of Express (or any HTTP engine).
|
|
9
|
-
*
|
|
10
|
-
* The adapter is injected at resolution time (not construction time)
|
|
11
|
-
* so the registry can be built before the adapter exists.
|
|
12
|
-
*/
|
|
13
|
-
class MiddlewareRegistry {
|
|
14
|
-
constructor() {
|
|
15
|
-
this._map = {};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Register a middleware alias.
|
|
20
|
-
*
|
|
21
|
-
* registry.register('auth', AuthMiddleware)
|
|
22
|
-
* registry.register('throttle', new ThrottleMiddleware({ max: 60 }))
|
|
23
|
-
*/
|
|
24
|
-
register(alias, handler) {
|
|
25
|
-
this._map[alias] = handler;
|
|
26
|
-
return this;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Resolve a middleware alias or class/instance into an adapter-native handler.
|
|
31
|
-
*
|
|
32
|
-
* @param {string|Function|object} aliasOrFn
|
|
33
|
-
* @param {import('../http/adapters/HttpAdapter')} adapter
|
|
34
|
-
* @param {object|null} container
|
|
35
|
-
* @returns {Function} adapter-native handler
|
|
36
|
-
*/
|
|
37
|
-
resolve(aliasOrFn, adapter, container = null) {
|
|
38
|
-
const Handler = typeof aliasOrFn === 'string'
|
|
39
|
-
? this._map[aliasOrFn]
|
|
40
|
-
: aliasOrFn;
|
|
41
|
-
|
|
42
|
-
if (!Handler) {
|
|
43
|
-
throw new Error(`Middleware "${aliasOrFn}" is not registered.`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return this._wrap(Handler, adapter, container);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Resolve all aliases in a list.
|
|
51
|
-
*/
|
|
52
|
-
resolveAll(list = [], adapter, container = null) {
|
|
53
|
-
return list.map(m => this.resolve(m, adapter, container));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Return a no-op passthrough handler for the given adapter.
|
|
58
|
-
* Used when a middleware alias is missing but should not crash the app.
|
|
59
|
-
*/
|
|
60
|
-
resolvePassthrough(adapter) {
|
|
61
|
-
// Adapter-agnostic: return a function matching the native signature
|
|
62
|
-
// by asking the adapter to wrap a no-op middleware instance.
|
|
63
|
-
return adapter.wrapMiddleware({
|
|
64
|
-
handle: (_ctx, next) => next(),
|
|
65
|
-
}, null);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
has(alias) {
|
|
69
|
-
return Object.prototype.hasOwnProperty.call(this._map, alias);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
all() {
|
|
73
|
-
return { ...this._map };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ── Internal ────────────────────────────────────────────────────────────────
|
|
77
|
-
|
|
78
|
-
_wrap(Handler, adapter, container) {
|
|
79
|
-
// Pre-instantiated Millas middleware object with handle()
|
|
80
|
-
if (
|
|
81
|
-
typeof Handler === 'object' &&
|
|
82
|
-
Handler !== null &&
|
|
83
|
-
typeof Handler.handle === 'function'
|
|
84
|
-
) {
|
|
85
|
-
return adapter.wrapMiddleware(Handler, container);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Millas middleware class (handle on prototype)
|
|
89
|
-
if (
|
|
90
|
-
typeof Handler === 'function' &&
|
|
91
|
-
Handler.prototype &&
|
|
92
|
-
typeof Handler.prototype.handle === 'function'
|
|
93
|
-
) {
|
|
94
|
-
return adapter.wrapMiddleware(new Handler(), container);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Raw adapter-native function — pass through as-is (escape hatch)
|
|
98
|
-
if (typeof Handler === 'function') {
|
|
99
|
-
return Handler;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
throw new Error('Middleware must be a function or a class with handle().');
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
module.exports = MiddlewareRegistry;
|