lieko-express 0.0.3 ā 0.0.5
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/README.md +458 -18
- package/lieko-express.d.ts +264 -0
- package/lieko-express.js +488 -126
- package/package.json +2 -1
package/lieko-express.js
CHANGED
|
@@ -422,6 +422,127 @@ class LiekoExpress {
|
|
|
422
422
|
strictTrailingSlash: true,
|
|
423
423
|
allowTrailingSlash: false,
|
|
424
424
|
};
|
|
425
|
+
|
|
426
|
+
this.bodyParserOptions = {
|
|
427
|
+
json: {
|
|
428
|
+
limit: '10mb',
|
|
429
|
+
strict: true
|
|
430
|
+
},
|
|
431
|
+
urlencoded: {
|
|
432
|
+
limit: '10mb',
|
|
433
|
+
extended: true
|
|
434
|
+
},
|
|
435
|
+
multipart: {
|
|
436
|
+
limit: '10mb'
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
this.corsOptions = {
|
|
441
|
+
enabled: false,
|
|
442
|
+
origin: "*",
|
|
443
|
+
strictOrigin: false,
|
|
444
|
+
allowPrivateNetwork: false,
|
|
445
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
446
|
+
headers: ["Content-Type", "Authorization"],
|
|
447
|
+
credentials: false,
|
|
448
|
+
maxAge: 86400,
|
|
449
|
+
exposedHeaders: [],
|
|
450
|
+
debug: false
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
cors(options = {}) {
|
|
455
|
+
this.corsOptions = {
|
|
456
|
+
...this.corsOptions,
|
|
457
|
+
enabled: true,
|
|
458
|
+
...options
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
_matchOrigin(origin, allowedOrigin) {
|
|
463
|
+
if (!origin || !allowedOrigin) return false;
|
|
464
|
+
|
|
465
|
+
if (Array.isArray(allowedOrigin)) {
|
|
466
|
+
return allowedOrigin.some(o => this._matchOrigin(origin, o));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (allowedOrigin === "*") return true;
|
|
470
|
+
|
|
471
|
+
// Wildcard https://*.example.com
|
|
472
|
+
if (allowedOrigin.includes("*")) {
|
|
473
|
+
const regex = new RegExp("^" + allowedOrigin
|
|
474
|
+
.replace(/\./g, "\\.")
|
|
475
|
+
.replace(/\*/g, ".*") + "$");
|
|
476
|
+
|
|
477
|
+
return regex.test(origin);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return origin === allowedOrigin;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
_applyCors(req, res, opts) {
|
|
484
|
+
if (!opts || !opts.enabled) return;
|
|
485
|
+
|
|
486
|
+
const requestOrigin = req.headers.origin || "";
|
|
487
|
+
|
|
488
|
+
let finalOrigin = "*";
|
|
489
|
+
|
|
490
|
+
if (opts.strictOrigin && requestOrigin) {
|
|
491
|
+
const allowed = this._matchOrigin(requestOrigin, opts.origin);
|
|
492
|
+
|
|
493
|
+
if (!allowed) {
|
|
494
|
+
res.statusCode = 403;
|
|
495
|
+
return res.end(JSON.stringify({
|
|
496
|
+
success: false,
|
|
497
|
+
error: "Origin Forbidden",
|
|
498
|
+
message: `Origin "${requestOrigin}" is not allowed`
|
|
499
|
+
}));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (opts.origin === "*") {
|
|
504
|
+
finalOrigin = "*";
|
|
505
|
+
} else if (Array.isArray(opts.origin)) {
|
|
506
|
+
const match = opts.origin.find(o => this._matchOrigin(requestOrigin, o));
|
|
507
|
+
finalOrigin = match || opts.origin[0];
|
|
508
|
+
} else {
|
|
509
|
+
finalOrigin = this._matchOrigin(requestOrigin, opts.origin)
|
|
510
|
+
? requestOrigin
|
|
511
|
+
: opts.origin;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
this._logCorsDebug(req, {
|
|
515
|
+
...opts,
|
|
516
|
+
origin: finalOrigin
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
res.setHeader("Access-Control-Allow-Origin", finalOrigin);
|
|
520
|
+
|
|
521
|
+
if (opts.credentials) {
|
|
522
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (opts.exposedHeaders?.length) {
|
|
526
|
+
res.setHeader("Access-Control-Expose-Headers",
|
|
527
|
+
opts.exposedHeaders.join(", "));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Chrome Private Network Access
|
|
531
|
+
if (
|
|
532
|
+
opts.allowPrivateNetwork &&
|
|
533
|
+
req.headers["access-control-request-private-network"] === "true"
|
|
534
|
+
) {
|
|
535
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (req.method === "OPTIONS") {
|
|
539
|
+
res.setHeader("Access-Control-Allow-Methods", opts.methods.join(", "));
|
|
540
|
+
res.setHeader("Access-Control-Allow-Headers", opts.headers.join(", "));
|
|
541
|
+
res.setHeader("Access-Control-Max-Age", opts.maxAge);
|
|
542
|
+
|
|
543
|
+
res.statusCode = 204;
|
|
544
|
+
return res.end();
|
|
545
|
+
}
|
|
425
546
|
}
|
|
426
547
|
|
|
427
548
|
set(name, value) {
|
|
@@ -451,6 +572,244 @@ class LiekoExpress {
|
|
|
451
572
|
return !this.settings[name];
|
|
452
573
|
}
|
|
453
574
|
|
|
575
|
+
bodyParser(options = {}) {
|
|
576
|
+
if (options.limit) {
|
|
577
|
+
this.bodyParserOptions.json.limit = options.limit;
|
|
578
|
+
this.bodyParserOptions.urlencoded.limit = options.limit;
|
|
579
|
+
}
|
|
580
|
+
if (options.extended !== undefined) {
|
|
581
|
+
this.bodyParserOptions.urlencoded.extended = options.extended;
|
|
582
|
+
}
|
|
583
|
+
if (options.strict !== undefined) {
|
|
584
|
+
this.bodyParserOptions.json.strict = options.strict;
|
|
585
|
+
}
|
|
586
|
+
return this;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
json(options = {}) {
|
|
590
|
+
if (options.limit) {
|
|
591
|
+
this.bodyParserOptions.json.limit = options.limit;
|
|
592
|
+
}
|
|
593
|
+
if (options.strict !== undefined) {
|
|
594
|
+
this.bodyParserOptions.json.strict = options.strict;
|
|
595
|
+
}
|
|
596
|
+
return this;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
urlencoded(options = {}) {
|
|
600
|
+
if (options.limit) {
|
|
601
|
+
this.bodyParserOptions.urlencoded.limit = options.limit;
|
|
602
|
+
}
|
|
603
|
+
if (options.extended !== undefined) {
|
|
604
|
+
this.bodyParserOptions.urlencoded.extended = options.extended;
|
|
605
|
+
}
|
|
606
|
+
return this;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
multipart(options = {}) {
|
|
610
|
+
if (options.limit) {
|
|
611
|
+
this.bodyParserOptions.multipart.limit = options.limit;
|
|
612
|
+
}
|
|
613
|
+
return this;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
_parseLimit(limit) {
|
|
617
|
+
if (typeof limit === 'number') return limit;
|
|
618
|
+
|
|
619
|
+
const match = limit.match(/^(\d+(?:\.\d+)?)(kb|mb|gb)?$/i);
|
|
620
|
+
if (!match) return 1048576; // 1mb par dƩfaut
|
|
621
|
+
|
|
622
|
+
const value = parseFloat(match[1]);
|
|
623
|
+
const unit = (match[2] || 'b').toLowerCase();
|
|
624
|
+
|
|
625
|
+
const multipliers = {
|
|
626
|
+
b: 1,
|
|
627
|
+
kb: 1024,
|
|
628
|
+
mb: 1024 * 1024,
|
|
629
|
+
gb: 1024 * 1024 * 1024
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
return value * multipliers[unit];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async _parseBody(req, routeOptions = null) {
|
|
636
|
+
return new Promise((resolve, reject) => {
|
|
637
|
+
|
|
638
|
+
if (['GET', 'DELETE', 'HEAD'].includes(req.method)) {
|
|
639
|
+
req.body = {};
|
|
640
|
+
req.files = {};
|
|
641
|
+
req._bodySize = 0;
|
|
642
|
+
return resolve();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const contentType = (req.headers['content-type'] || '').toLowerCase();
|
|
646
|
+
const options = routeOptions || this.bodyParserOptions;
|
|
647
|
+
|
|
648
|
+
req.body = {};
|
|
649
|
+
req.files = {};
|
|
650
|
+
|
|
651
|
+
let raw = Buffer.alloc(0);
|
|
652
|
+
let size = 0;
|
|
653
|
+
let limitExceeded = false;
|
|
654
|
+
let errorSent = false;
|
|
655
|
+
|
|
656
|
+
const detectLimit = () => {
|
|
657
|
+
if (contentType.includes('application/json')) {
|
|
658
|
+
return this._parseLimit(options.json.limit);
|
|
659
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
660
|
+
return this._parseLimit(options.urlencoded.limit);
|
|
661
|
+
} else if (contentType.includes('multipart/form-data')) {
|
|
662
|
+
return this._parseLimit(options.multipart.limit);
|
|
663
|
+
} else {
|
|
664
|
+
return this._parseLimit('1mb');
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
const limit = detectLimit();
|
|
669
|
+
const limitLabel =
|
|
670
|
+
contentType.includes('application/json') ? options.json.limit :
|
|
671
|
+
contentType.includes('application/x-www-form-urlencoded') ? options.urlencoded.limit :
|
|
672
|
+
contentType.includes('multipart/form-data') ? options.multipart.limit :
|
|
673
|
+
'1mb';
|
|
674
|
+
|
|
675
|
+
req.on('data', chunk => {
|
|
676
|
+
if (limitExceeded || errorSent) return;
|
|
677
|
+
|
|
678
|
+
size += chunk.length;
|
|
679
|
+
|
|
680
|
+
if (size > limit) {
|
|
681
|
+
limitExceeded = true;
|
|
682
|
+
errorSent = true;
|
|
683
|
+
|
|
684
|
+
req.removeAllListeners('data');
|
|
685
|
+
req.removeAllListeners('end');
|
|
686
|
+
req.removeAllListeners('error');
|
|
687
|
+
|
|
688
|
+
req.on('data', () => { });
|
|
689
|
+
req.on('end', () => { });
|
|
690
|
+
|
|
691
|
+
const error = new Error(`Request body too large. Limit: ${limitLabel}`);
|
|
692
|
+
error.status = 413;
|
|
693
|
+
error.code = 'PAYLOAD_TOO_LARGE';
|
|
694
|
+
return reject(error);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
raw = Buffer.concat([raw, chunk]);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
req.on('end', () => {
|
|
701
|
+
if (limitExceeded) return;
|
|
702
|
+
|
|
703
|
+
req._bodySize = size;
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
|
|
707
|
+
if (contentType.includes('application/json')) {
|
|
708
|
+
const text = raw.toString();
|
|
709
|
+
try {
|
|
710
|
+
req.body = JSON.parse(text);
|
|
711
|
+
|
|
712
|
+
if (options.json.strict && text.trim() && !['[', '{'].includes(text.trim()[0])) {
|
|
713
|
+
return reject(new Error('Strict mode: body must be an object or array'));
|
|
714
|
+
}
|
|
715
|
+
} catch (err) {
|
|
716
|
+
req.body = {};
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
721
|
+
const text = raw.toString();
|
|
722
|
+
const params = new URLSearchParams(text);
|
|
723
|
+
req.body = {};
|
|
724
|
+
|
|
725
|
+
if (options.urlencoded.extended) {
|
|
726
|
+
for (const [key, value] of params) {
|
|
727
|
+
if (key.includes('[')) {
|
|
728
|
+
const match = key.match(/^([^\[]+)\[([^\]]*)\]$/);
|
|
729
|
+
if (match) {
|
|
730
|
+
const [, objKey, subKey] = match;
|
|
731
|
+
if (!req.body[objKey]) req.body[objKey] = {};
|
|
732
|
+
if (subKey) req.body[objKey][subKey] = value;
|
|
733
|
+
else {
|
|
734
|
+
if (!Array.isArray(req.body[objKey])) req.body[objKey] = [];
|
|
735
|
+
req.body[objKey].push(value);
|
|
736
|
+
}
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
req.body[key] = value;
|
|
741
|
+
}
|
|
742
|
+
} else {
|
|
743
|
+
req.body = Object.fromEntries(params);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
else if (contentType.includes('multipart/form-data')) {
|
|
748
|
+
const boundaryMatch = contentType.match(/boundary=([^;]+)/);
|
|
749
|
+
if (!boundaryMatch) return reject(new Error('Missing multipart boundary'));
|
|
750
|
+
|
|
751
|
+
const boundary = '--' + boundaryMatch[1];
|
|
752
|
+
|
|
753
|
+
const text = raw.toString('binary');
|
|
754
|
+
const parts = text.split(boundary).filter(p => p && !p.includes('--'));
|
|
755
|
+
|
|
756
|
+
for (let part of parts) {
|
|
757
|
+
const headerEnd = part.indexOf('\r\n\r\n');
|
|
758
|
+
if (headerEnd === -1) continue;
|
|
759
|
+
|
|
760
|
+
const headers = part.slice(0, headerEnd);
|
|
761
|
+
const body = part.slice(headerEnd + 4).replace(/\r\n$/, '');
|
|
762
|
+
|
|
763
|
+
const nameMatch = headers.match(/name="([^"]+)"/);
|
|
764
|
+
const filenameMatch = headers.match(/filename="([^"]*)"/);
|
|
765
|
+
const contentTypeMatch = headers.match(/Content-Type:\s*([^\r\n]+)/i);
|
|
766
|
+
|
|
767
|
+
const field = nameMatch?.[1];
|
|
768
|
+
if (!field) continue;
|
|
769
|
+
|
|
770
|
+
if (filenameMatch?.[1]) {
|
|
771
|
+
const bin = Buffer.from(body, 'binary');
|
|
772
|
+
|
|
773
|
+
req.files[field] = {
|
|
774
|
+
filename: filenameMatch[1],
|
|
775
|
+
data: bin,
|
|
776
|
+
size: bin.length,
|
|
777
|
+
contentType: contentTypeMatch ? contentTypeMatch[1] : 'application/octet-stream'
|
|
778
|
+
};
|
|
779
|
+
} else {
|
|
780
|
+
req.body[field] = body;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
else {
|
|
786
|
+
const text = raw.toString();
|
|
787
|
+
req.body = text ? { text } : {};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
for (const key in req.body) {
|
|
791
|
+
const value = req.body[key];
|
|
792
|
+
|
|
793
|
+
if (typeof value === 'string' && value.trim() !== '' && !isNaN(value)) {
|
|
794
|
+
req.body[key] = parseFloat(value);
|
|
795
|
+
} else if (value === 'true') {
|
|
796
|
+
req.body[key] = true;
|
|
797
|
+
} else if (value === 'false') {
|
|
798
|
+
req.body[key] = false;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
resolve();
|
|
803
|
+
|
|
804
|
+
} catch (error) {
|
|
805
|
+
reject(error);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
req.on('error', reject);
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
454
813
|
get(path, ...handlers) {
|
|
455
814
|
this._addRoute('GET', path, ...handlers);
|
|
456
815
|
return this;
|
|
@@ -540,24 +899,21 @@ class LiekoExpress {
|
|
|
540
899
|
}
|
|
541
900
|
|
|
542
901
|
_checkMiddleware(handler) {
|
|
543
|
-
const isAsync = handler.constructor
|
|
902
|
+
const isAsync = handler instanceof (async () => { }).constructor;
|
|
544
903
|
|
|
545
|
-
if (isAsync)
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const paramCount = handler.length;
|
|
904
|
+
if (isAsync) return;
|
|
550
905
|
|
|
551
|
-
if (
|
|
906
|
+
if (handler.length < 3) {
|
|
552
907
|
console.warn(`
|
|
553
|
-
ā ļø WARNING:
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
908
|
+
ā ļø WARNING: Middleware executed without a 'next' parameter.
|
|
909
|
+
This middleware may block the request pipeline.
|
|
910
|
+
|
|
911
|
+
Offending middleware:
|
|
912
|
+
${handler.toString().split('\n')[0].substring(0, 120)}...
|
|
913
|
+
|
|
914
|
+
Fix: Add 'next' as third parameter and call it:
|
|
915
|
+
(req, res, next) => { /* your code */ next(); }
|
|
916
|
+
`);
|
|
561
917
|
}
|
|
562
918
|
}
|
|
563
919
|
|
|
@@ -655,7 +1011,8 @@ class LiekoExpress {
|
|
|
655
1011
|
groupChain: [
|
|
656
1012
|
...this.groupStack,
|
|
657
1013
|
...(route.groupChain || [])
|
|
658
|
-
]
|
|
1014
|
+
],
|
|
1015
|
+
bodyParserOptions: router.bodyParserOptions
|
|
659
1016
|
});
|
|
660
1017
|
});
|
|
661
1018
|
}
|
|
@@ -772,7 +1129,7 @@ class LiekoExpress {
|
|
|
772
1129
|
}
|
|
773
1130
|
|
|
774
1131
|
const HTTP_STATUS = {
|
|
775
|
-
// 4xx
|
|
1132
|
+
// 4xx ā CLIENT ERRORS
|
|
776
1133
|
INVALID_REQUEST: 400,
|
|
777
1134
|
VALIDATION_FAILED: 400,
|
|
778
1135
|
NO_TOKEN_PROVIDED: 401,
|
|
@@ -784,7 +1141,7 @@ class LiekoExpress {
|
|
|
784
1141
|
RECORD_EXISTS: 409,
|
|
785
1142
|
TOO_MANY_REQUESTS: 429,
|
|
786
1143
|
|
|
787
|
-
// 5xx
|
|
1144
|
+
// 5xx ā SERVER ERRORS
|
|
788
1145
|
SERVER_ERROR: 500,
|
|
789
1146
|
SERVICE_UNAVAILABLE: 503
|
|
790
1147
|
};
|
|
@@ -848,43 +1205,89 @@ class LiekoExpress {
|
|
|
848
1205
|
|
|
849
1206
|
async _handleRequest(req, res) {
|
|
850
1207
|
this._enhanceRequest(req);
|
|
1208
|
+
|
|
851
1209
|
const url = req.url;
|
|
852
|
-
const
|
|
853
|
-
const pathname =
|
|
1210
|
+
const qIndex = url.indexOf('?');
|
|
1211
|
+
const pathname = qIndex === -1 ? url : url.substring(0, qIndex);
|
|
854
1212
|
|
|
855
1213
|
const query = {};
|
|
856
|
-
if (
|
|
857
|
-
const searchParams = new URLSearchParams(url.substring(
|
|
1214
|
+
if (qIndex !== -1) {
|
|
1215
|
+
const searchParams = new URLSearchParams(url.substring(qIndex + 1));
|
|
858
1216
|
for (const [key, value] of searchParams) query[key] = value;
|
|
859
1217
|
}
|
|
860
|
-
|
|
861
1218
|
req.query = query;
|
|
862
1219
|
req.params = {};
|
|
863
1220
|
|
|
864
1221
|
for (const key in req.query) {
|
|
865
|
-
const
|
|
866
|
-
if (
|
|
867
|
-
if (
|
|
868
|
-
if (/^\d+$/.test(
|
|
869
|
-
if (/^\d+\.\d+$/.test(
|
|
1222
|
+
const v = req.query[key];
|
|
1223
|
+
if (v === 'true') req.query[key] = true;
|
|
1224
|
+
else if (v === 'false') req.query[key] = false;
|
|
1225
|
+
else if (/^\d+$/.test(v)) req.query[key] = parseInt(v);
|
|
1226
|
+
else if (/^\d+\.\d+$/.test(v)) req.query[key] = parseFloat(v);
|
|
870
1227
|
}
|
|
871
1228
|
|
|
872
|
-
await this._parseBody(req);
|
|
873
1229
|
req._startTime = process.hrtime.bigint();
|
|
874
1230
|
this._enhanceResponse(req, res);
|
|
875
1231
|
|
|
876
1232
|
try {
|
|
1233
|
+
|
|
1234
|
+
if (req.method === "OPTIONS" && this.corsOptions.enabled) {
|
|
1235
|
+
this._applyCors(req, res, this.corsOptions);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const route = this._findRoute(req.method, pathname);
|
|
1240
|
+
|
|
1241
|
+
if (route) {
|
|
1242
|
+
|
|
1243
|
+
if (route.cors === false) { }
|
|
1244
|
+
|
|
1245
|
+
else if (route.cors) {
|
|
1246
|
+
const finalCors = {
|
|
1247
|
+
...this.corsOptions,
|
|
1248
|
+
enabled: true,
|
|
1249
|
+
...route.cors
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
this._applyCors(req, res, finalCors);
|
|
1253
|
+
if (req.method === "OPTIONS") return;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
else if (this.corsOptions.enabled) {
|
|
1257
|
+
this._applyCors(req, res, this.corsOptions);
|
|
1258
|
+
if (req.method === "OPTIONS") return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
} else {
|
|
1262
|
+
|
|
1263
|
+
if (this.corsOptions.enabled) {
|
|
1264
|
+
this._applyCors(req, res, this.corsOptions);
|
|
1265
|
+
if (req.method === "OPTIONS") return;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
try {
|
|
1270
|
+
await this._parseBody(req, route ? route.bodyParserOptions : null);
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
if (error.code === 'PAYLOAD_TOO_LARGE') {
|
|
1273
|
+
return res.status(413).json({
|
|
1274
|
+
success: false,
|
|
1275
|
+
error: 'Payload Too Large',
|
|
1276
|
+
message: error.message
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
return await this._runErrorHandlers(error, req, res);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
877
1282
|
for (const mw of this.middlewares) {
|
|
878
1283
|
if (res.headersSent) return;
|
|
879
1284
|
|
|
880
|
-
if (mw.path && !pathname.startsWith(mw.path))
|
|
881
|
-
continue;
|
|
882
|
-
}
|
|
1285
|
+
if (mw.path && !pathname.startsWith(mw.path)) continue;
|
|
883
1286
|
|
|
884
1287
|
await new Promise((resolve, reject) => {
|
|
885
|
-
const next = async (
|
|
886
|
-
if (
|
|
887
|
-
await this._runErrorHandlers(
|
|
1288
|
+
const next = async (err) => {
|
|
1289
|
+
if (err) {
|
|
1290
|
+
await this._runErrorHandlers(err, req, res);
|
|
888
1291
|
return resolve();
|
|
889
1292
|
}
|
|
890
1293
|
resolve();
|
|
@@ -899,14 +1302,9 @@ class LiekoExpress {
|
|
|
899
1302
|
|
|
900
1303
|
if (res.headersSent) return;
|
|
901
1304
|
|
|
902
|
-
const route = this._findRoute(req.method, pathname);
|
|
903
|
-
|
|
904
1305
|
if (!route) {
|
|
905
|
-
if (this.notFoundHandler)
|
|
906
|
-
|
|
907
|
-
} else {
|
|
908
|
-
return res.status(404).json({ error: 'Route not found' });
|
|
909
|
-
}
|
|
1306
|
+
if (this.notFoundHandler) return this.notFoundHandler(req, res);
|
|
1307
|
+
return res.status(404).json({ error: 'Route not found' });
|
|
910
1308
|
}
|
|
911
1309
|
|
|
912
1310
|
req.params = route.params;
|
|
@@ -915,9 +1313,9 @@ class LiekoExpress {
|
|
|
915
1313
|
if (res.headersSent) return;
|
|
916
1314
|
|
|
917
1315
|
await new Promise((resolve, reject) => {
|
|
918
|
-
const next = async (
|
|
919
|
-
if (
|
|
920
|
-
await this._runErrorHandlers(
|
|
1316
|
+
const next = async (err) => {
|
|
1317
|
+
if (err) {
|
|
1318
|
+
await this._runErrorHandlers(err, req, res);
|
|
921
1319
|
return resolve();
|
|
922
1320
|
}
|
|
923
1321
|
resolve();
|
|
@@ -1175,85 +1573,6 @@ class LiekoExpress {
|
|
|
1175
1573
|
};
|
|
1176
1574
|
}
|
|
1177
1575
|
|
|
1178
|
-
async _parseBody(req) {
|
|
1179
|
-
return new Promise((resolve) => {
|
|
1180
|
-
if (['GET', 'DELETE', 'HEAD'].includes(req.method)) {
|
|
1181
|
-
req.body = {};
|
|
1182
|
-
req.files = {};
|
|
1183
|
-
return resolve();
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
const contentType = (req.headers['content-type'] || '').toLowerCase();
|
|
1187
|
-
req.body = {};
|
|
1188
|
-
req.files = {};
|
|
1189
|
-
|
|
1190
|
-
let raw = '';
|
|
1191
|
-
req.on('data', chunk => raw += chunk);
|
|
1192
|
-
req.on('end', () => {
|
|
1193
|
-
|
|
1194
|
-
if (contentType.includes('application/json')) {
|
|
1195
|
-
try {
|
|
1196
|
-
req.body = JSON.parse(raw);
|
|
1197
|
-
} catch {
|
|
1198
|
-
req.body = {};
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
1202
|
-
req.body = Object.fromEntries(new URLSearchParams(raw));
|
|
1203
|
-
}
|
|
1204
|
-
else if (contentType.includes('multipart/form-data')) {
|
|
1205
|
-
const boundaryMatch = contentType.match(/boundary=([^\s;]+)/);
|
|
1206
|
-
if (boundaryMatch) {
|
|
1207
|
-
const boundary = '--' + boundaryMatch[1];
|
|
1208
|
-
const parts = raw.split(boundary).filter(p => p && !p.includes('--'));
|
|
1209
|
-
|
|
1210
|
-
for (let part of parts) {
|
|
1211
|
-
const headerEnd = part.indexOf('\r\n\r\n');
|
|
1212
|
-
if (headerEnd === -1) continue;
|
|
1213
|
-
|
|
1214
|
-
const headers = part.slice(0, headerEnd);
|
|
1215
|
-
const body = part.slice(headerEnd + 4).replace(/\r\n$/, '');
|
|
1216
|
-
|
|
1217
|
-
const nameMatch = headers.match(/name="([^"]+)"/);
|
|
1218
|
-
const filenameMatch = headers.match(/filename="([^"]*)"/);
|
|
1219
|
-
const field = nameMatch?.[1];
|
|
1220
|
-
if (!field) continue;
|
|
1221
|
-
|
|
1222
|
-
if (filenameMatch?.[1]) {
|
|
1223
|
-
req.files[field] = {
|
|
1224
|
-
filename: filenameMatch[1],
|
|
1225
|
-
data: Buffer.from(body, 'binary'),
|
|
1226
|
-
contentType: headers.match(/Content-Type: (.*)/i)?.[1] || 'application/octet-stream'
|
|
1227
|
-
};
|
|
1228
|
-
} else {
|
|
1229
|
-
req.body[field] = body;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
else {
|
|
1235
|
-
req.body = raw ? { text: raw } : {};
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
for (const key in req.body) {
|
|
1239
|
-
const value = req.body[key];
|
|
1240
|
-
|
|
1241
|
-
if (typeof value === 'string' && value.trim() !== '' && !isNaN(value) && !isNaN(parseFloat(value))) {
|
|
1242
|
-
req.body[key] = parseFloat(value);
|
|
1243
|
-
}
|
|
1244
|
-
else if (value === 'true') {
|
|
1245
|
-
req.body[key] = true;
|
|
1246
|
-
}
|
|
1247
|
-
else if (value === 'false') {
|
|
1248
|
-
req.body[key] = false;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
resolve();
|
|
1253
|
-
});
|
|
1254
|
-
});
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
1576
|
async _runMiddleware(handler, req, res) {
|
|
1258
1577
|
return new Promise((resolve, reject) => {
|
|
1259
1578
|
const next = (err) => err ? reject(err) : resolve();
|
|
@@ -1286,20 +1605,63 @@ class LiekoExpress {
|
|
|
1286
1605
|
code >= 300 ? '\x1b[36m' :
|
|
1287
1606
|
'\x1b[32m';
|
|
1288
1607
|
|
|
1608
|
+
const bodySize = req._bodySize || 0;
|
|
1609
|
+
let bodySizeFormatted;
|
|
1610
|
+
|
|
1611
|
+
if (bodySize === 0) {
|
|
1612
|
+
bodySizeFormatted = '0 bytes';
|
|
1613
|
+
} else if (bodySize < 1024) {
|
|
1614
|
+
bodySizeFormatted = `${bodySize} bytes`;
|
|
1615
|
+
} else if (bodySize < 1024 * 1024) {
|
|
1616
|
+
bodySizeFormatted = `${(bodySize / 1024).toFixed(2)} KB`;
|
|
1617
|
+
} else if (bodySize < 1024 * 1024 * 1024) {
|
|
1618
|
+
bodySizeFormatted = `${(bodySize / (1024 * 1024)).toFixed(2)} MB`;
|
|
1619
|
+
} else {
|
|
1620
|
+
bodySizeFormatted = `${(bodySize / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1289
1623
|
console.log(
|
|
1290
1624
|
`\nš¦ DEBUG REQUEST` +
|
|
1291
1625
|
`\nā ${req.method} ${req.originalUrl}` +
|
|
1292
1626
|
`\nā IP: ${req.ip.ipv4}` +
|
|
1293
1627
|
`\nā Status: ${color(res.statusCode)}${res.statusCode}\x1b[0m` +
|
|
1294
1628
|
`\nā Duration: ${timeFormatted}` +
|
|
1629
|
+
`\nā Body Size: ${bodySizeFormatted}` +
|
|
1295
1630
|
`\nā Params: ${JSON.stringify(req.params || {})}` +
|
|
1296
1631
|
`\nā Query: ${JSON.stringify(req.query || {})}` +
|
|
1297
|
-
`\nā Body: ${JSON.stringify(req.body || {})}` +
|
|
1632
|
+
`\nā Body: ${JSON.stringify(req.body || {}).substring(0, 200)}${JSON.stringify(req.body || {}).length > 200 ? '...' : ''}` +
|
|
1298
1633
|
`\nā Files: ${Object.keys(req.files || {}).join(', ')}` +
|
|
1299
1634
|
`\n---------------------------------------------\n`
|
|
1300
1635
|
);
|
|
1301
1636
|
}
|
|
1302
1637
|
|
|
1638
|
+
_logCorsDebug(req, opts) {
|
|
1639
|
+
if (!opts.debug) return;
|
|
1640
|
+
|
|
1641
|
+
console.log("\n[CORS DEBUG]");
|
|
1642
|
+
console.log("Request:", req.method, req.url);
|
|
1643
|
+
console.log("Origin:", req.headers.origin || "none");
|
|
1644
|
+
|
|
1645
|
+
console.log("Applied CORS Policy:");
|
|
1646
|
+
console.log(" - Access-Control-Allow-Origin:", opts.origin);
|
|
1647
|
+
console.log(" - Access-Control-Allow-Methods:", opts.methods.join(", "));
|
|
1648
|
+
console.log(" - Access-Control-Allow-Headers:", opts.headers.join(", "));
|
|
1649
|
+
|
|
1650
|
+
if (opts.credentials) {
|
|
1651
|
+
console.log(" - Access-Control-Allow-Credentials: true");
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
if (opts.exposedHeaders?.length) {
|
|
1655
|
+
console.log(" - Access-Control-Expose-Headers:", opts.exposedHeaders.join(", "));
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
console.log(" - Max-Age:", opts.maxAge);
|
|
1659
|
+
|
|
1660
|
+
if (req.method === "OPTIONS") {
|
|
1661
|
+
console.log("Preflight request handled with status 204\n");
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1303
1665
|
_buildRouteTree() {
|
|
1304
1666
|
const tree = {};
|
|
1305
1667
|
|
|
@@ -1403,4 +1765,4 @@ module.exports.schema = (...args) => new Schema(...args);
|
|
|
1403
1765
|
module.exports.validators = validators;
|
|
1404
1766
|
module.exports.validate = validate;
|
|
1405
1767
|
module.exports.validatePartial = validatePartial;
|
|
1406
|
-
module.exports.ValidationError = ValidationError;
|
|
1768
|
+
module.exports.ValidationError = ValidationError;
|