lieko-express 0.0.7 → 0.0.9
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/lib/cors.js +100 -0
- package/lib/schema.js +418 -0
- package/lib/static.js +247 -0
- package/lieko-express.js +213 -804
- package/package.json +4 -3
package/lib/cors.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module.exports = function cors(userOptions = {}) {
|
|
2
|
+
const defaultOptions = {
|
|
3
|
+
enabled: true,
|
|
4
|
+
origin: "*",
|
|
5
|
+
strictOrigin: false,
|
|
6
|
+
allowPrivateNetwork: false,
|
|
7
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
8
|
+
headers: ["Content-Type", "Authorization"],
|
|
9
|
+
credentials: false,
|
|
10
|
+
maxAge: 86400,
|
|
11
|
+
exposedHeaders: [],
|
|
12
|
+
debug: false
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const opts = { ...defaultOptions, ...userOptions, enabled: true };
|
|
16
|
+
|
|
17
|
+
const _matchOrigin = (origin, allowedOrigin) => {
|
|
18
|
+
if (!origin || !allowedOrigin) return false;
|
|
19
|
+
if (Array.isArray(allowedOrigin)) {
|
|
20
|
+
return allowedOrigin.some(o => _matchOrigin(origin, o));
|
|
21
|
+
}
|
|
22
|
+
if (allowedOrigin === "*") return true;
|
|
23
|
+
if (allowedOrigin.includes("*")) {
|
|
24
|
+
const regex = new RegExp("^" + allowedOrigin
|
|
25
|
+
.replace(/\./g, "\\.")
|
|
26
|
+
.replace(/\*/g, ".*") + "$");
|
|
27
|
+
return regex.test(origin);
|
|
28
|
+
}
|
|
29
|
+
return origin === allowedOrigin;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const _logDebug = (req, finalOpts) => {
|
|
33
|
+
if (!finalOpts.debug) return;
|
|
34
|
+
console.log("\n[CORS DEBUG]");
|
|
35
|
+
console.log("Request:", req.method, req.url);
|
|
36
|
+
console.log("Origin:", req.headers.origin || "none");
|
|
37
|
+
console.log("Applied CORS Policy:");
|
|
38
|
+
console.log(" - Access-Control-Allow-Origin:", finalOpts.origin);
|
|
39
|
+
console.log(" - Methods:", finalOpts.methods.join(", "));
|
|
40
|
+
console.log(" - Headers:", finalOpts.headers.join(", "));
|
|
41
|
+
if (finalOpts.credentials) console.log(" - Credentials: true");
|
|
42
|
+
if (finalOpts.exposedHeaders?.length) console.log(" - Exposed:", finalOpts.exposedHeaders.join(", "));
|
|
43
|
+
console.log(" - Max-Age:", finalOpts.maxAge);
|
|
44
|
+
if (req.method === "OPTIONS") console.log("Preflight handled → 204\n");
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return function corsMiddleware(req, res, next) {
|
|
48
|
+
if (!opts.enabled) return next();
|
|
49
|
+
|
|
50
|
+
const requestOrigin = req.headers.origin || "";
|
|
51
|
+
|
|
52
|
+
let finalOrigin = "*";
|
|
53
|
+
|
|
54
|
+
if (opts.strictOrigin && requestOrigin) {
|
|
55
|
+
const allowed = _matchOrigin(requestOrigin, opts.origin);
|
|
56
|
+
if (!allowed) {
|
|
57
|
+
res.statusCode = 403;
|
|
58
|
+
return res.end(JSON.stringify({
|
|
59
|
+
success: false,
|
|
60
|
+
error: "Origin Forbidden",
|
|
61
|
+
message: `Origin "${requestOrigin}" is not allowed`
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (opts.origin === "*") {
|
|
67
|
+
finalOrigin = "*";
|
|
68
|
+
} else if (Array.isArray(opts.origin)) {
|
|
69
|
+
const match = opts.origin.find(o => _matchOrigin(requestOrigin, o));
|
|
70
|
+
finalOrigin = match || opts.origin[0];
|
|
71
|
+
} else {
|
|
72
|
+
finalOrigin = _matchOrigin(requestOrigin, opts.origin) ? requestOrigin : opts.origin;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_logDebug(req, { ...opts, origin: finalOrigin });
|
|
76
|
+
|
|
77
|
+
res.setHeader("Access-Control-Allow-Origin", finalOrigin);
|
|
78
|
+
|
|
79
|
+
if (opts.credentials) {
|
|
80
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (opts.exposedHeaders?.length) {
|
|
84
|
+
res.setHeader("Access-Control-Expose-Headers", opts.exposedHeaders.join(", "));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (opts.allowPrivateNetwork && req.headers["access-control-request-private-network"] === "true") {
|
|
88
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (req.method === "OPTIONS") {
|
|
92
|
+
res.setHeader("Access-Control-Allow-Methods", opts.methods.join(", "));
|
|
93
|
+
res.setHeader("Access-Control-Allow-Headers", opts.headers.join(", "));
|
|
94
|
+
res.setHeader("Access-Control-Max-Age", opts.maxAge);
|
|
95
|
+
res.statusCode = 204;
|
|
96
|
+
return res.end();
|
|
97
|
+
}
|
|
98
|
+
next();
|
|
99
|
+
};
|
|
100
|
+
};
|
package/lib/schema.js
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
class ValidationError extends Error {
|
|
2
|
+
constructor(errors) {
|
|
3
|
+
super('Validation failed');
|
|
4
|
+
this.name = 'ValidationError';
|
|
5
|
+
this.errors = errors;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class Schema {
|
|
10
|
+
constructor(rules) {
|
|
11
|
+
this.rules = rules;
|
|
12
|
+
this.fields = rules;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
validate(data) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
for (const [field, validators] of Object.entries(this.rules)) {
|
|
18
|
+
const value = data[field];
|
|
19
|
+
for (const validator of validators) {
|
|
20
|
+
const error = validator(value, field, data);
|
|
21
|
+
if (error) {
|
|
22
|
+
errors.push(error);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (errors.length > 0) throw new ValidationError(errors);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const validators = {
|
|
33
|
+
required: (message = 'Field is required') => {
|
|
34
|
+
return (value, field) => {
|
|
35
|
+
if (value === undefined || value === null || value === '') {
|
|
36
|
+
return { field, message, type: 'required' };
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
requiredTrue: (message = 'Field must be true') => {
|
|
43
|
+
return (value, field) => {
|
|
44
|
+
const normalized = value === true || value === 'true' || value === '1' || value === 1;
|
|
45
|
+
if (!normalized) {
|
|
46
|
+
return { field, message, type: 'requiredTrue' };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
optional: () => {
|
|
53
|
+
return () => null;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
string: (message = 'Field must be a string') => {
|
|
57
|
+
return (value, field) => {
|
|
58
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
59
|
+
return { field, message, type: 'string' };
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
number: (message = 'Field must be a number') => {
|
|
66
|
+
return (value, field) => {
|
|
67
|
+
if (value !== undefined && typeof value !== 'number') {
|
|
68
|
+
return { field, message, type: 'number' };
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
boolean: (message = 'Field must be a boolean') => {
|
|
75
|
+
return (value, field) => {
|
|
76
|
+
if (value === undefined || value === null || value === '') return null;
|
|
77
|
+
|
|
78
|
+
const validTrue = ['true', true, 1, '1'];
|
|
79
|
+
const validFalse = ['false', false, 0, '0'];
|
|
80
|
+
|
|
81
|
+
const isValid = validTrue.includes(value) || validFalse.includes(value);
|
|
82
|
+
|
|
83
|
+
if (!isValid) {
|
|
84
|
+
return { field, message, type: 'boolean' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
integer: (message = 'Field must be an integer') => {
|
|
92
|
+
return (value, field) => {
|
|
93
|
+
if (value !== undefined && !Number.isInteger(value)) {
|
|
94
|
+
return { field, message, type: 'integer' };
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
positive: (message = 'Field must be positive') => {
|
|
101
|
+
return (value, field) => {
|
|
102
|
+
if (value !== undefined && value <= 0) {
|
|
103
|
+
return { field, message, type: 'positive' };
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
negative: (message = 'Field must be negative') => {
|
|
110
|
+
return (value, field) => {
|
|
111
|
+
if (value !== undefined && value >= 0) {
|
|
112
|
+
return { field, message, type: 'negative' };
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
email: (message = 'Invalid email format') => {
|
|
119
|
+
return (value, field) => {
|
|
120
|
+
if (value !== undefined && value !== null) {
|
|
121
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
122
|
+
if (!emailRegex.test(value)) {
|
|
123
|
+
return { field, message, type: 'email' };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
min: (minValue, message) => {
|
|
131
|
+
return (value, field) => {
|
|
132
|
+
if (value !== undefined && value !== null) {
|
|
133
|
+
if (typeof value === 'string' && value.length < minValue) {
|
|
134
|
+
return {
|
|
135
|
+
field,
|
|
136
|
+
message: message || `Field must be at least ${minValue} characters`,
|
|
137
|
+
type: 'min'
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (typeof value === 'number' && value < minValue) {
|
|
141
|
+
return {
|
|
142
|
+
field,
|
|
143
|
+
message: message || `Field must be at least ${minValue}`,
|
|
144
|
+
type: 'min'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
max: (maxValue, message) => {
|
|
153
|
+
return (value, field) => {
|
|
154
|
+
if (value !== undefined && value !== null) {
|
|
155
|
+
if (typeof value === 'string' && value.length > maxValue) {
|
|
156
|
+
return {
|
|
157
|
+
field,
|
|
158
|
+
message: message || `Field must be at most ${maxValue} characters`,
|
|
159
|
+
type: 'max'
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (typeof value === 'number' && value > maxValue) {
|
|
163
|
+
return {
|
|
164
|
+
field,
|
|
165
|
+
message: message || `Field must be at most ${maxValue}`,
|
|
166
|
+
type: 'max'
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
length: (n, message) => {
|
|
175
|
+
return (value, field) => {
|
|
176
|
+
if (typeof value === 'string' && value.length !== n) {
|
|
177
|
+
return {
|
|
178
|
+
field,
|
|
179
|
+
message: message || `Field must be exactly ${n} characters`,
|
|
180
|
+
type: 'length'
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
minLength: (minLength, message) => {
|
|
188
|
+
return (value, field) => {
|
|
189
|
+
if (value !== undefined && value !== null && typeof value === 'string') {
|
|
190
|
+
if (value.length < minLength) {
|
|
191
|
+
return {
|
|
192
|
+
field,
|
|
193
|
+
message: message || `Field must be at least ${minLength} characters`,
|
|
194
|
+
type: 'minLength'
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
maxLength: (maxLength, message) => {
|
|
203
|
+
return (value, field) => {
|
|
204
|
+
if (value !== undefined && value !== null && typeof value === 'string') {
|
|
205
|
+
if (value.length > maxLength) {
|
|
206
|
+
return {
|
|
207
|
+
field,
|
|
208
|
+
message: message || `Field must be at most ${maxLength} characters`,
|
|
209
|
+
type: 'maxLength'
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
pattern: (regex, message = 'Invalid format') => {
|
|
218
|
+
return (value, field) => {
|
|
219
|
+
if (value !== undefined && value !== null && typeof value === 'string') {
|
|
220
|
+
if (!regex.test(value)) {
|
|
221
|
+
return { field, message, type: 'pattern' };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
oneOf: (allowedValues, message) => {
|
|
229
|
+
return (value, field) => {
|
|
230
|
+
if (value !== undefined && value !== null) {
|
|
231
|
+
if (!allowedValues.includes(value)) {
|
|
232
|
+
return {
|
|
233
|
+
field,
|
|
234
|
+
message: message || `Field must be one of: ${allowedValues.join(', ')}`,
|
|
235
|
+
type: 'oneOf'
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
notOneOf: (values, message) => {
|
|
244
|
+
return (value, field) => {
|
|
245
|
+
if (values.includes(value)) {
|
|
246
|
+
return {
|
|
247
|
+
field,
|
|
248
|
+
message: message || `Field cannot be one of: ${values.join(', ')}`,
|
|
249
|
+
type: 'notOneOf'
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
custom: (validatorFn, message = 'Validation failed') => {
|
|
257
|
+
return (value, field, data) => {
|
|
258
|
+
const isValid = validatorFn(value, data);
|
|
259
|
+
if (!isValid) {
|
|
260
|
+
return { field, message, type: 'custom' };
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
equal: (expectedValue, message) => {
|
|
267
|
+
return (value, field) => {
|
|
268
|
+
if (value !== expectedValue) {
|
|
269
|
+
return {
|
|
270
|
+
field,
|
|
271
|
+
message: message || `Field must be equal to ${expectedValue}`,
|
|
272
|
+
type: 'equal'
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
mustBeTrue: (message = 'This field must be accepted') => {
|
|
280
|
+
return (value, field) => {
|
|
281
|
+
const normalized = value === true || value === 'true' || value === '1' || value === 1;
|
|
282
|
+
if (!normalized) {
|
|
283
|
+
return { field, message, type: 'mustBeTrue' };
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
mustBeFalse: (message = 'This field must be declined') => {
|
|
290
|
+
return (value, field) => {
|
|
291
|
+
const normalized = value === false || value === 'false' || value === '0' || value === 0;
|
|
292
|
+
if (!normalized) {
|
|
293
|
+
return { field, message, type: 'mustBeFalse' };
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
date: (message = 'Invalid date') => {
|
|
300
|
+
return (value, field) => {
|
|
301
|
+
if (!value) return null;
|
|
302
|
+
const date = new Date(value);
|
|
303
|
+
if (isNaN(date.getTime())) {
|
|
304
|
+
return { field, message, type: 'date' };
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
before: (limit, message) => {
|
|
311
|
+
return (value, field) => {
|
|
312
|
+
if (!value) return null;
|
|
313
|
+
const d1 = new Date(value);
|
|
314
|
+
const d2 = new Date(limit);
|
|
315
|
+
if (isNaN(d1) || d1 >= d2) {
|
|
316
|
+
return {
|
|
317
|
+
field,
|
|
318
|
+
message: message || `Date must be before ${limit}`,
|
|
319
|
+
type: 'before'
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
after: (limit, message) => {
|
|
327
|
+
return (value, field) => {
|
|
328
|
+
if (!value) return null;
|
|
329
|
+
const d1 = new Date(value);
|
|
330
|
+
const d2 = new Date(limit);
|
|
331
|
+
if (isNaN(d1) || d1 <= d2) {
|
|
332
|
+
return {
|
|
333
|
+
field,
|
|
334
|
+
message: message || `Date must be after ${limit}`,
|
|
335
|
+
type: 'after'
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
startsWith: (prefix, message) => {
|
|
343
|
+
return (value, field) => {
|
|
344
|
+
if (typeof value === 'string' && !value.startsWith(prefix)) {
|
|
345
|
+
return {
|
|
346
|
+
field,
|
|
347
|
+
message: message || `Field must start with "${prefix}"`,
|
|
348
|
+
type: 'startsWith'
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
endsWith: (suffix, message) => {
|
|
356
|
+
return (value, field) => {
|
|
357
|
+
if (typeof value === 'string' && !value.endsWith(suffix)) {
|
|
358
|
+
return {
|
|
359
|
+
field,
|
|
360
|
+
message: message || `Field must end with "${suffix}"`,
|
|
361
|
+
type: 'endsWith'
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
function validate(schema) {
|
|
370
|
+
return (req, res, next) => {
|
|
371
|
+
try {
|
|
372
|
+
schema.validate(req.body);
|
|
373
|
+
next();
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (error instanceof ValidationError) {
|
|
376
|
+
return res.status(400).json({
|
|
377
|
+
success: false,
|
|
378
|
+
message: 'Validation failed',
|
|
379
|
+
errors: error.errors
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function validatePartial(schema) {
|
|
388
|
+
const partial = {};
|
|
389
|
+
|
|
390
|
+
for (const field in schema.fields) {
|
|
391
|
+
const rules = schema.fields[field];
|
|
392
|
+
|
|
393
|
+
const filtered = rules.filter(v =>
|
|
394
|
+
v.name !== 'required' &&
|
|
395
|
+
v.name !== 'requiredTrue' &&
|
|
396
|
+
v.name !== 'mustBeTrue'
|
|
397
|
+
);
|
|
398
|
+
partial[field] = [validators.optional(), ...filtered];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return new Schema(partial);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = {
|
|
405
|
+
Schema,
|
|
406
|
+
ValidationError,
|
|
407
|
+
validators,
|
|
408
|
+
validate,
|
|
409
|
+
validatePartial,
|
|
410
|
+
|
|
411
|
+
schema: (...args) => new Schema(...args),
|
|
412
|
+
v: validators,
|
|
413
|
+
validator: validators,
|
|
414
|
+
middleware: {
|
|
415
|
+
validate,
|
|
416
|
+
validatePartial
|
|
417
|
+
}
|
|
418
|
+
};
|