lieko-express 0.0.3 → 0.0.4
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.js +488 -126
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -311,7 +311,7 @@ app.use((req, res, next) => {
|
|
|
311
311
|
next();
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
-
// CORS middleware
|
|
314
|
+
// CORS middleware (native, but you can use app.cors())
|
|
315
315
|
app.use((req, res, next) => {
|
|
316
316
|
res.set('Access-Control-Allow-Origin', '*');
|
|
317
317
|
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
|
@@ -382,6 +382,380 @@ app.use(async (req, res, next) => {
|
|
|
382
382
|
});
|
|
383
383
|
```
|
|
384
384
|
|
|
385
|
+
# **CORS**
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
Lieko-Express includes a **fully built-in, high-performance CORS engine**, with:
|
|
389
|
+
|
|
390
|
+
* Global CORS (`app.cors()`)
|
|
391
|
+
* Route-level CORS (`app.get("/test", { cors: {...} }, handler)`—coming soon if you want it)
|
|
392
|
+
* Wildcard origins (`https://*.example.com`)
|
|
393
|
+
* Multiple allowed origins
|
|
394
|
+
* Strict mode (reject unknown origins)
|
|
395
|
+
* Credential support
|
|
396
|
+
* Private Network Access (Chrome PNA)
|
|
397
|
+
* Debug logging
|
|
398
|
+
* Full OPTIONS preflight support
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
# **Enable CORS Globally**
|
|
403
|
+
|
|
404
|
+
```js
|
|
405
|
+
app.cors();
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
This enables default permissive CORS:
|
|
409
|
+
|
|
410
|
+
| Option | Default |
|
|
411
|
+
| --------------------- | ----------------------------- |
|
|
412
|
+
| `origin` | `"*"` |
|
|
413
|
+
| `methods` | all standard methods |
|
|
414
|
+
| `headers` | `Content-Type, Authorization` |
|
|
415
|
+
| `credentials` | false |
|
|
416
|
+
| `maxAge` | 86400 (24h) |
|
|
417
|
+
| `exposedHeaders` | none |
|
|
418
|
+
| `strictOrigin` | false |
|
|
419
|
+
| `allowPrivateNetwork` | false |
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
# **Custom Global CORS Options**
|
|
424
|
+
|
|
425
|
+
```js
|
|
426
|
+
app.cors({
|
|
427
|
+
origin: ["https://example.com", "https://api.example.com"],
|
|
428
|
+
methods: ["GET", "POST"],
|
|
429
|
+
headers: ["Content-Type", "Authorization", "X-Custom"],
|
|
430
|
+
credentials: true,
|
|
431
|
+
maxAge: 3600,
|
|
432
|
+
exposedHeaders: ["X-RateLimit-Remaining"],
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
# **Allow All Origins (Standard Mode)**
|
|
439
|
+
|
|
440
|
+
```js
|
|
441
|
+
app.cors({
|
|
442
|
+
origin: "*"
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
⚠️ If you also set `credentials: true`, browsers will **reject** `*`.
|
|
447
|
+
Lieko will still output `*` because that's the correct spec behavior.
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
# **Strict Origin Mode**
|
|
452
|
+
|
|
453
|
+
Strict mode ensures the incoming `Origin` header **must match exactly** one of your allowed origins.
|
|
454
|
+
|
|
455
|
+
```js
|
|
456
|
+
app.cors({
|
|
457
|
+
origin: ["https://myapp.com"],
|
|
458
|
+
strictOrigin: true
|
|
459
|
+
});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
If the request comes from an unauthorized origin:
|
|
463
|
+
|
|
464
|
+
```json
|
|
465
|
+
HTTP 403
|
|
466
|
+
{
|
|
467
|
+
"success": false,
|
|
468
|
+
"error": "Origin Forbidden",
|
|
469
|
+
"message": "Origin \"https://evil.com\" is not allowed"
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
# **Wildcard Origins**
|
|
476
|
+
|
|
477
|
+
Lieko-Express supports Express-style wildcard patterns:
|
|
478
|
+
|
|
479
|
+
### Allow any subdomain
|
|
480
|
+
|
|
481
|
+
```js
|
|
482
|
+
app.cors({
|
|
483
|
+
origin: "https://*.example.com"
|
|
484
|
+
});
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Matches:
|
|
488
|
+
|
|
489
|
+
* `https://api.example.com`
|
|
490
|
+
* `https://dashboard.example.com`
|
|
491
|
+
|
|
492
|
+
Does not match:
|
|
493
|
+
|
|
494
|
+
* `http://example.com`
|
|
495
|
+
* `https://example.net`
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
# **Allow Private Network Access (Chrome PNA)**
|
|
500
|
+
|
|
501
|
+
```js
|
|
502
|
+
app.cors({
|
|
503
|
+
allowPrivateNetwork: true
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
When Chrome sends:
|
|
508
|
+
|
|
509
|
+
```
|
|
510
|
+
Access-Control-Request-Private-Network: true
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Lieko automatically responds:
|
|
514
|
+
|
|
515
|
+
```
|
|
516
|
+
Access-Control-Allow-Private-Network: true
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
# Route-Level CORS
|
|
520
|
+
|
|
521
|
+
Lieko-Express allows each route to override or extend the global CORS settings using a `cors` property in the route options.
|
|
522
|
+
This gives you fine-grained control over cross-origin behavior on a per-endpoint basis.
|
|
523
|
+
|
|
524
|
+
Route-level CORS:
|
|
525
|
+
|
|
526
|
+
* Overrides global CORS
|
|
527
|
+
* Supports wildcards (`https://*.example.com`)
|
|
528
|
+
* Supports strict mode
|
|
529
|
+
* Sends its own preflight response
|
|
530
|
+
* Automatically merges with the global configuration
|
|
531
|
+
* Does **not** affect other routes
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Basic Example
|
|
536
|
+
|
|
537
|
+
```js
|
|
538
|
+
app.get("/public", {
|
|
539
|
+
cors: { origin: "*" }
|
|
540
|
+
}, (req, res) => {
|
|
541
|
+
res.ok("Public endpoint with open CORS");
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
This route allows requests from **any origin**, even if global CORS is configured differently.
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## 🔒 Restrict a Sensitive Route
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
app.post("/admin/login", {
|
|
553
|
+
cors: {
|
|
554
|
+
origin: "https://dashboard.myapp.com",
|
|
555
|
+
credentials: true,
|
|
556
|
+
strictOrigin: true
|
|
557
|
+
}
|
|
558
|
+
}, (req, res) => {
|
|
559
|
+
res.ok("Login OK");
|
|
560
|
+
});
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Result:
|
|
564
|
+
|
|
565
|
+
* Only `https://dashboard.myapp.com` is allowed
|
|
566
|
+
* Cookies / sessions allowed
|
|
567
|
+
* Any other origin receives:
|
|
568
|
+
|
|
569
|
+
```json
|
|
570
|
+
{
|
|
571
|
+
"success": false,
|
|
572
|
+
"error": "Origin Forbidden",
|
|
573
|
+
"message": "Origin \"https://evil.com\" is not allowed"
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Status: **403 Forbidden**
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## ✨ Wildcard Origins on a Route
|
|
582
|
+
|
|
583
|
+
```js
|
|
584
|
+
app.get("/api/data", {
|
|
585
|
+
cors: {
|
|
586
|
+
origin: "https://*.example.com"
|
|
587
|
+
}
|
|
588
|
+
}, (req, res) => {
|
|
589
|
+
res.ok({ data: 123 });
|
|
590
|
+
});
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Matches:
|
|
594
|
+
|
|
595
|
+
* `https://api.example.com`
|
|
596
|
+
* `https://dashboard.example.com`
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
## ⚙️ Full Route-Level CORS Example
|
|
601
|
+
|
|
602
|
+
```js
|
|
603
|
+
app.get("/user/profile", {
|
|
604
|
+
cors: {
|
|
605
|
+
origin: [
|
|
606
|
+
"https://app.example.com",
|
|
607
|
+
"https://*.trusted.dev"
|
|
608
|
+
],
|
|
609
|
+
methods: ["GET", "PATCH"],
|
|
610
|
+
headers: ["Content-Type", "Authorization"],
|
|
611
|
+
exposedHeaders: ["X-User-Id"],
|
|
612
|
+
credentials: true,
|
|
613
|
+
maxAge: 600,
|
|
614
|
+
strictOrigin: true,
|
|
615
|
+
allowPrivateNetwork: true,
|
|
616
|
+
debug: true
|
|
617
|
+
}
|
|
618
|
+
}, (req, res) => {
|
|
619
|
+
res.ok({ profile: "OK" });
|
|
620
|
+
});
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## 📌 How Route-Level CORS Works Internally
|
|
626
|
+
|
|
627
|
+
When a request hits a route:
|
|
628
|
+
|
|
629
|
+
1. If global CORS is enabled → apply it.
|
|
630
|
+
2. If the route defines `cors` → merge with global config.
|
|
631
|
+
3. Route CORS **overrides** global CORS.
|
|
632
|
+
4. If request is `OPTIONS` (preflight) → return automatic CORS response.
|
|
633
|
+
5. Otherwise → run route handler.
|
|
634
|
+
|
|
635
|
+
This ensures predictable behavior.
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## 📘 Route-Level CORS Options
|
|
640
|
+
|
|
641
|
+
| Option | Type | Description | |
|
|
642
|
+
| --------------------- | ---------- | ---------------------------------------- | ---------------------------------------- |
|
|
643
|
+
| `origin` | `string | string[]` | Allowed origins (`*`, domain, wildcard). |
|
|
644
|
+
| `methods` | `string[]` | Allowed HTTP methods. | |
|
|
645
|
+
| `headers` | `string[]` | Allowed request headers. | |
|
|
646
|
+
| `exposedHeaders` | `string[]` | Response headers exposed to the browser. | |
|
|
647
|
+
| `credentials` | `boolean` | Allow cookies/sessions. | |
|
|
648
|
+
| `maxAge` | `number` | Preflight cache lifetime (seconds). | |
|
|
649
|
+
| `strictOrigin` | `boolean` | Reject non-matching origins with 403. | |
|
|
650
|
+
| `allowPrivateNetwork` | `boolean` | Enables Chrome Private Network Access. | |
|
|
651
|
+
| `debug` | `boolean` | Logs detailed CORS decisions. | |
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## 🧩 Interaction with Global CORS
|
|
656
|
+
|
|
657
|
+
| Feature | Global | Route |
|
|
658
|
+
| ------------------ | ------ | ------------- |
|
|
659
|
+
| Origin | ✔ | ✔ (overrides) |
|
|
660
|
+
| Wildcards | ✔ | ✔ |
|
|
661
|
+
| Credentials | ✔ | ✔ |
|
|
662
|
+
| Strict Origin | ✔ | ✔ |
|
|
663
|
+
| Private Network | ✔ | ✔ |
|
|
664
|
+
| Debug Logging | ✔ | ✔ |
|
|
665
|
+
| Preflight Handling | ✔ | ✔ |
|
|
666
|
+
| Overrides Global | ❌ | ✔ |
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
# **Enable CORS Debug Logging**
|
|
672
|
+
|
|
673
|
+
```js
|
|
674
|
+
app.cors({
|
|
675
|
+
debug: true
|
|
676
|
+
});
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Console output example:
|
|
680
|
+
|
|
681
|
+
```
|
|
682
|
+
[CORS DEBUG]
|
|
683
|
+
Request: GET /users
|
|
684
|
+
Origin: https://app.example.com
|
|
685
|
+
Applied CORS Policy:
|
|
686
|
+
- Access-Control-Allow-Origin: https://app.example.com
|
|
687
|
+
- Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
|
|
688
|
+
- Access-Control-Allow-Headers: Content-Type, Authorization
|
|
689
|
+
- Max-Age: 86400
|
|
690
|
+
Simple request handled normally
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
695
|
+
# **Preflight Handling (OPTIONS)**
|
|
696
|
+
|
|
697
|
+
Lieko-Express automatically handles it:
|
|
698
|
+
|
|
699
|
+
### Request:
|
|
700
|
+
|
|
701
|
+
```
|
|
702
|
+
OPTIONS /login
|
|
703
|
+
Access-Control-Request-Method: POST
|
|
704
|
+
Access-Control-Request-Headers: Content-Type
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
### Lieko Response:
|
|
708
|
+
|
|
709
|
+
```
|
|
710
|
+
204 No Content
|
|
711
|
+
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
|
|
712
|
+
Access-Control-Allow-Headers: Content-Type, Authorization
|
|
713
|
+
Access-Control-Max-Age: 86400
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
---
|
|
717
|
+
|
|
718
|
+
# **Credentials Support**
|
|
719
|
+
|
|
720
|
+
```js
|
|
721
|
+
app.cors({
|
|
722
|
+
credentials: true,
|
|
723
|
+
origin: "https://myapp.com"
|
|
724
|
+
});
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
Response contains:
|
|
728
|
+
|
|
729
|
+
```
|
|
730
|
+
Access-Control-Allow-Credentials: true
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
# **Disable CORS**
|
|
737
|
+
|
|
738
|
+
Just don’t call `app.cors()`.
|
|
739
|
+
CORS stays fully disabled.
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
# **Default CORS Configuration**
|
|
744
|
+
|
|
745
|
+
```js
|
|
746
|
+
this.corsOptions = {
|
|
747
|
+
enabled: false,
|
|
748
|
+
origin: "*",
|
|
749
|
+
strictOrigin: false,
|
|
750
|
+
allowPrivateNetwork: false,
|
|
751
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
752
|
+
headers: ["Content-Type", "Authorization"],
|
|
753
|
+
credentials: false,
|
|
754
|
+
maxAge: 86400,
|
|
755
|
+
exposedHeaders: [],
|
|
756
|
+
debug: false
|
|
757
|
+
};
|
|
758
|
+
```
|
|
385
759
|
|
|
386
760
|
# 🔍 Request Object
|
|
387
761
|
|
|
@@ -514,15 +888,83 @@ res.paginated(items, page, limit, total);
|
|
|
514
888
|
* Error code → HTTP mapping
|
|
515
889
|
* String errors also supported (`res.error("Invalid user")`)
|
|
516
890
|
|
|
517
|
-
|
|
891
|
+
---
|
|
892
|
+
# Body Parsing (JSON, URL-encoded, Multipart) — 100% Native, Zero Dependencies
|
|
893
|
+
|
|
894
|
+
Lieko Express ships with a **fast, secure, and fully native body parser** — no external packages, not even `body-parser`.
|
|
895
|
+
|
|
896
|
+
As soon as you create your app, body parsing is **automatically enabled**:
|
|
897
|
+
|
|
898
|
+
```js
|
|
899
|
+
const app = Lieko(); // Ready to handle JSON, form-data, files, etc.
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
### Default Limits
|
|
903
|
+
|
|
904
|
+
| Content-Type | Default Limit | Bytes |
|
|
905
|
+
|---------------------------------------|---------------|-------------------|
|
|
906
|
+
| `application/json` | **1mb** | ~1,048,576 bytes |
|
|
907
|
+
| `application/x-www-form-urlencoded` | **1mb** | ~1,048,576 bytes |
|
|
908
|
+
| `multipart/form-data` (file uploads) | **10mb** | ~10,485,760 bytes |
|
|
909
|
+
|
|
910
|
+
That’s already **10× more generous** than Express’s default 100kb!
|
|
911
|
+
|
|
912
|
+
### Change Limits — Three Super-Simple Ways
|
|
913
|
+
|
|
914
|
+
#### 1. Per content-type (most common)
|
|
915
|
+
|
|
916
|
+
```js
|
|
917
|
+
app.json({ limit: '100mb' }); // JSON payloads only
|
|
918
|
+
app.urlencoded({ limit: '50mb' }); // Classic HTML forms
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
#### 2. Global limit (JSON + urlencoded at once)
|
|
922
|
+
|
|
923
|
+
```js
|
|
924
|
+
app.bodyParser({ limit: '50mb' });
|
|
925
|
+
// Applies 50mb to both JSON and urlencoded bodies
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
#### 3. File upload limit (multipart/form-data)
|
|
929
|
+
|
|
930
|
+
```js
|
|
931
|
+
app.multipart({ limit: '500mb' }); // For very large file uploads
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
You can freely combine them:
|
|
935
|
+
|
|
936
|
+
```js
|
|
937
|
+
const app = Lieko();
|
|
938
|
+
|
|
939
|
+
app.json({ limit: '10mb' }); // Small, fast JSON APIs
|
|
940
|
+
app.urlencoded({ limit: '5mb' });
|
|
941
|
+
app.multipart({ limit: '1gb' }); // Allow huge file uploads
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
### Real-world Example
|
|
945
|
+
|
|
946
|
+
```js
|
|
947
|
+
const Lieko = require('lieko-express');
|
|
948
|
+
const app = Lieko();
|
|
949
|
+
|
|
950
|
+
// Accept reasonably large JSON payloads
|
|
951
|
+
app.json({ limit: '25mb' });
|
|
952
|
+
|
|
953
|
+
// Accept classic forms
|
|
954
|
+
app.urlencoded({ limit: '10mb' });
|
|
518
955
|
|
|
519
|
-
|
|
956
|
+
// Allow big file uploads (photos, videos, etc.)
|
|
957
|
+
app.multipart({ limit: '500mb' });
|
|
520
958
|
|
|
521
|
-
|
|
959
|
+
app.post('/upload', (req, res) => {
|
|
960
|
+
console.log('JSON body size :', Buffer.byteLength(JSON.stringify(req.body)), 'bytes');
|
|
961
|
+
console.log('Uploaded files :', Object.keys(req.files || {}));
|
|
522
962
|
|
|
523
|
-
|
|
963
|
+
res.ok({ received: true, files: req.files });
|
|
964
|
+
});
|
|
524
965
|
|
|
525
|
-
|
|
966
|
+
app.listen(3000);
|
|
967
|
+
```
|
|
526
968
|
|
|
527
969
|
Uploads end up in:
|
|
528
970
|
|
|
@@ -546,6 +988,12 @@ Query & body fields are **auto converted**:
|
|
|
546
988
|
"null" → null
|
|
547
989
|
```
|
|
548
990
|
|
|
991
|
+
|
|
992
|
+
**No `app.use(express.json())`, no `app.use(express.urlencoded())`, no extra dependencies** — everything works out of the box, blazing fast, and fully configurable in one line.
|
|
993
|
+
|
|
994
|
+
That’s the Lieko philosophy: **less boilerplate, more power**.
|
|
995
|
+
|
|
996
|
+
|
|
549
997
|
### Response Methods
|
|
550
998
|
|
|
551
999
|
```javascript
|
|
@@ -1577,6 +2025,7 @@ DEBUG REQUEST
|
|
|
1577
2025
|
→ Params: {"id":"42"}
|
|
1578
2026
|
→ Query: {"page":2,"active":true}
|
|
1579
2027
|
→ Body: {}
|
|
2028
|
+
→ Body Size: 0 bytes
|
|
1580
2029
|
→ Files:
|
|
1581
2030
|
---------------------------------------------
|
|
1582
2031
|
```
|
|
@@ -1649,9 +2098,9 @@ You can configure:
|
|
|
1649
2098
|
```js
|
|
1650
2099
|
app.set('trust proxy', value);
|
|
1651
2100
|
app.set('debug', boolean);
|
|
1652
|
-
app.set('x-powered-by',
|
|
1653
|
-
app.set('strictTrailingSlash',
|
|
1654
|
-
app.set('allowTrailingSlash',
|
|
2101
|
+
app.set('x-powered-by', boolean);
|
|
2102
|
+
app.set('strictTrailingSlash', boolean);
|
|
2103
|
+
app.set('allowTrailingSlash', boolean);
|
|
1655
2104
|
```
|
|
1656
2105
|
|
|
1657
2106
|
# 🌐 Trust Proxy & IP Parsing
|
|
@@ -1761,15 +2210,6 @@ function myPlugin(app) {
|
|
|
1761
2210
|
|
|
1762
2211
|
myPlugin(app);
|
|
1763
2212
|
```
|
|
1764
|
-
|
|
1765
|
-
## 🚀 Deploying Lieko Express
|
|
1766
|
-
|
|
1767
|
-
### Node.js (PM2)
|
|
1768
|
-
|
|
1769
|
-
```bash
|
|
1770
|
-
pm2 start app.js
|
|
1771
|
-
```
|
|
1772
|
-
|
|
1773
2213
|
## 📦 API Reference
|
|
1774
2214
|
|
|
1775
2215
|
### `Lieko()`
|
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;
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lieko-express",
|
|
3
|
+
"version": "0.0.4",
|
|
3
4
|
"repository": {
|
|
4
5
|
"type": "git",
|
|
5
6
|
"url": "https://github.com/eiwSrvt/lieko-express"
|
|
6
7
|
},
|
|
7
8
|
"homepage": "https://github.com/eiwSrvt/lieko-express",
|
|
8
|
-
"version": "0.0.3",
|
|
9
9
|
"description": "Lieko-express — A Modern, Minimal, REST API Framework for Node.js",
|
|
10
10
|
"main": "lieko-express.js",
|
|
11
11
|
"scripts": {
|