mbkbucket 3.6.0 → 3.7.0
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 +1 -1
- package/lib/cli.js +650 -649
- package/lib/public/bucketadmin.css +282 -7
- package/lib/src/cli/auth-helper.js +9 -18
- package/lib/src/config/index.js +0 -2
- package/lib/src/config/validation.js +3 -3
- package/lib/src/controllers/api.controller.js +1 -1
- package/lib/src/controllers/dashboard.controller.js +4 -3
- package/lib/src/controllers/info.controller.js +1 -2
- package/lib/src/controllers/view.controller.js +1 -3
- package/lib/src/middleware/async-handler.js +4 -8
- package/lib/src/middleware/error-handler.js +2 -6
- package/lib/src/middleware/rate-limiter.js +2 -2
- package/lib/src/middleware/security.js +1 -7
- package/lib/src/routes/api.routes.js +4 -5
- package/lib/src/routes/index.js +39 -19
- package/lib/src/services/s3.service.js +10 -19
- package/lib/src/utils/errors.js +3 -11
- package/lib/src/utils/helpers.js +29 -68
- package/package.json +5 -5
|
@@ -68,10 +68,18 @@
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/* Base Layout & Page Header */
|
|
71
|
+
.page-wrapper,
|
|
72
|
+
.page-wrapper *,
|
|
73
|
+
.page-wrapper *::before,
|
|
74
|
+
.page-wrapper *::after {
|
|
75
|
+
box-sizing: border-box;
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
.page-wrapper {
|
|
72
79
|
max-width: 1600px;
|
|
73
80
|
margin: 0 auto;
|
|
74
81
|
width: 100%;
|
|
82
|
+
max-width: 100%;
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
.page-header {
|
|
@@ -1363,57 +1371,324 @@
|
|
|
1363
1371
|
|
|
1364
1372
|
/* Mobile & Tablet Responsiveness */
|
|
1365
1373
|
@media (max-width: 900px) {
|
|
1374
|
+
.page-wrapper {
|
|
1375
|
+
padding: 0;
|
|
1376
|
+
max-width: 100%;
|
|
1377
|
+
overflow-x: hidden;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
.card.file-manager {
|
|
1381
|
+
border-radius: var(--radius-md);
|
|
1382
|
+
width: 100%;
|
|
1383
|
+
max-width: 100%;
|
|
1384
|
+
overflow: hidden;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1366
1387
|
.fm-layout {
|
|
1367
|
-
grid-template-columns:
|
|
1388
|
+
grid-template-columns: 100%;
|
|
1368
1389
|
height: auto;
|
|
1390
|
+
min-height: 0;
|
|
1369
1391
|
max-height: none;
|
|
1392
|
+
width: 100%;
|
|
1393
|
+
max-width: 100%;
|
|
1394
|
+
overflow: hidden;
|
|
1370
1395
|
}
|
|
1371
1396
|
|
|
1372
1397
|
.fm-tree-wrap {
|
|
1398
|
+
display: flex;
|
|
1399
|
+
border-right: none;
|
|
1400
|
+
border-bottom: 1px solid var(--border-color);
|
|
1401
|
+
padding: 10px 14px;
|
|
1402
|
+
background: rgba(244, 247, 243, 0.85);
|
|
1403
|
+
width: 100%;
|
|
1404
|
+
max-width: 100%;
|
|
1405
|
+
box-sizing: border-box;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
.fm-tree-header,
|
|
1409
|
+
.fm-tree {
|
|
1373
1410
|
display: none;
|
|
1374
1411
|
}
|
|
1375
1412
|
|
|
1413
|
+
.fm-storage {
|
|
1414
|
+
margin-top: 0;
|
|
1415
|
+
padding: 8px 12px;
|
|
1416
|
+
background: rgba(255, 255, 255, 0.95);
|
|
1417
|
+
border: 1px solid var(--border-color);
|
|
1418
|
+
border-radius: var(--radius-sm);
|
|
1419
|
+
box-shadow: 0 1px 4px rgba(15, 118, 110, 0.05);
|
|
1420
|
+
width: 100%;
|
|
1421
|
+
max-width: 100%;
|
|
1422
|
+
box-sizing: border-box;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
.fm-main {
|
|
1426
|
+
width: 100%;
|
|
1427
|
+
max-width: 100%;
|
|
1428
|
+
min-width: 0;
|
|
1429
|
+
overflow: hidden;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1376
1432
|
.fm-grid-wrap {
|
|
1377
1433
|
min-height: 400px;
|
|
1434
|
+
padding: 12px;
|
|
1435
|
+
width: 100%;
|
|
1436
|
+
max-width: 100%;
|
|
1437
|
+
box-sizing: border-box;
|
|
1438
|
+
overflow-x: hidden;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
.fm-grid.view-grid {
|
|
1442
|
+
grid-template-columns: repeat(auto-fill, minmax(135px, 1fr));
|
|
1443
|
+
gap: 10px;
|
|
1444
|
+
width: 100%;
|
|
1445
|
+
max-width: 100%;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
.fm-item {
|
|
1449
|
+
-webkit-tap-highlight-color: transparent;
|
|
1450
|
+
width: 100%;
|
|
1451
|
+
max-width: 100%;
|
|
1452
|
+
min-width: 0;
|
|
1453
|
+
box-sizing: border-box;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
.fm-item-details {
|
|
1457
|
+
width: 100%;
|
|
1458
|
+
max-width: 100%;
|
|
1459
|
+
min-width: 0;
|
|
1460
|
+
overflow: hidden;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
.fm-item-name {
|
|
1464
|
+
width: 100%;
|
|
1465
|
+
max-width: 100%;
|
|
1466
|
+
min-width: 0;
|
|
1467
|
+
overflow: hidden;
|
|
1468
|
+
text-overflow: ellipsis;
|
|
1469
|
+
white-space: nowrap;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
.fm-item:active {
|
|
1473
|
+
transform: scale(0.98);
|
|
1474
|
+
background: var(--primary-light-color-ml);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
.fm-item-menu {
|
|
1478
|
+
opacity: 1;
|
|
1479
|
+
background: rgba(255, 255, 255, 0.92);
|
|
1480
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
.fm-star.on {
|
|
1484
|
+
opacity: 1;
|
|
1378
1485
|
}
|
|
1379
1486
|
}
|
|
1380
1487
|
|
|
1381
1488
|
@media (max-width: 640px) {
|
|
1489
|
+
.page-header {
|
|
1490
|
+
padding: 0.75rem 1rem;
|
|
1491
|
+
margin-bottom: 0.75rem;
|
|
1492
|
+
border-radius: var(--radius);
|
|
1493
|
+
width: 100%;
|
|
1494
|
+
max-width: 100%;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1382
1497
|
.page-header-row {
|
|
1383
1498
|
flex-direction: column;
|
|
1384
|
-
align-items:
|
|
1499
|
+
align-items: stretch;
|
|
1500
|
+
gap: 10px;
|
|
1501
|
+
width: 100%;
|
|
1385
1502
|
}
|
|
1386
1503
|
|
|
1387
1504
|
.page-actions {
|
|
1388
1505
|
width: 100%;
|
|
1389
|
-
|
|
1506
|
+
display: flex;
|
|
1507
|
+
justify-content: stretch;
|
|
1508
|
+
gap: 8px;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
.page-actions .btn {
|
|
1512
|
+
flex: 1 1 0;
|
|
1513
|
+
min-width: 0;
|
|
1514
|
+
justify-content: center;
|
|
1515
|
+
padding: 7px 8px;
|
|
1516
|
+
font-size: 12px;
|
|
1390
1517
|
}
|
|
1391
1518
|
|
|
1392
1519
|
.fm-toolbar {
|
|
1393
1520
|
flex-direction: column;
|
|
1394
1521
|
align-items: stretch;
|
|
1522
|
+
gap: 8px;
|
|
1523
|
+
padding: 8px 10px;
|
|
1524
|
+
width: 100%;
|
|
1525
|
+
max-width: 100%;
|
|
1526
|
+
overflow: hidden;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
.fm-breadcrumb {
|
|
1530
|
+
max-width: 100%;
|
|
1531
|
+
overflow-x: auto;
|
|
1532
|
+
white-space: nowrap;
|
|
1533
|
+
padding-bottom: 2px;
|
|
1534
|
+
scrollbar-width: none;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
.fm-breadcrumb::-webkit-scrollbar {
|
|
1538
|
+
display: none;
|
|
1395
1539
|
}
|
|
1396
1540
|
|
|
1397
1541
|
.fm-toolbar-actions {
|
|
1398
1542
|
width: 100%;
|
|
1399
|
-
|
|
1400
|
-
flex-
|
|
1543
|
+
display: flex;
|
|
1544
|
+
flex-direction: column;
|
|
1545
|
+
align-items: stretch;
|
|
1546
|
+
gap: 8px;
|
|
1547
|
+
max-width: 100%;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
.quick-filter-group {
|
|
1551
|
+
width: 100%;
|
|
1552
|
+
display: flex;
|
|
1553
|
+
overflow-x: auto;
|
|
1554
|
+
scrollbar-width: none;
|
|
1555
|
+
max-width: 100%;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
.quick-filter-group::-webkit-scrollbar {
|
|
1559
|
+
display: none;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
.quick-filter-btn {
|
|
1563
|
+
flex: 1 1 0;
|
|
1564
|
+
text-align: center;
|
|
1565
|
+
padding: 5px 6px;
|
|
1566
|
+
font-size: 11.5px;
|
|
1567
|
+
white-space: nowrap;
|
|
1401
1568
|
}
|
|
1402
1569
|
|
|
1403
1570
|
.search-box {
|
|
1404
1571
|
width: 100%;
|
|
1572
|
+
max-width: 100%;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
.search-box input {
|
|
1576
|
+
width: 100%;
|
|
1577
|
+
box-sizing: border-box;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/* Grid Mode on Small Phones */
|
|
1581
|
+
.fm-grid.view-grid {
|
|
1582
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1583
|
+
gap: 8px;
|
|
1584
|
+
width: 100%;
|
|
1585
|
+
max-width: 100%;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
.fm-grid.view-grid .fm-item {
|
|
1589
|
+
padding: 8px;
|
|
1590
|
+
border-radius: var(--radius-md);
|
|
1591
|
+
gap: 6px;
|
|
1592
|
+
width: 100%;
|
|
1593
|
+
max-width: 100%;
|
|
1594
|
+
min-width: 0;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
.fm-grid.view-grid .fm-item-icon {
|
|
1598
|
+
aspect-ratio: 16 / 11;
|
|
1599
|
+
width: 100%;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
.fm-item-name {
|
|
1603
|
+
font-size: 12px;
|
|
1604
|
+
line-height: 1.35;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
.fm-item-meta {
|
|
1608
|
+
font-size: 10px;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
.fm-item-menu {
|
|
1612
|
+
top: 5px;
|
|
1613
|
+
right: 5px;
|
|
1614
|
+
width: 26px;
|
|
1615
|
+
height: 26px;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
.fm-star {
|
|
1619
|
+
top: 5px;
|
|
1620
|
+
left: 5px;
|
|
1621
|
+
width: 22px;
|
|
1622
|
+
height: 22px;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/* List Mode on Mobile */
|
|
1626
|
+
.fm-grid.view-list {
|
|
1627
|
+
gap: 6px;
|
|
1628
|
+
width: 100%;
|
|
1629
|
+
max-width: 100%;
|
|
1405
1630
|
}
|
|
1406
1631
|
|
|
1407
1632
|
.fm-grid.view-list .fm-item {
|
|
1408
|
-
|
|
1633
|
+
display: flex;
|
|
1634
|
+
align-items: center;
|
|
1635
|
+
gap: 8px;
|
|
1636
|
+
padding: 8px 10px;
|
|
1637
|
+
border-radius: var(--radius-md);
|
|
1638
|
+
width: 100%;
|
|
1639
|
+
max-width: 100%;
|
|
1640
|
+
min-width: 0;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
.fm-grid.view-list .fm-item-checkbox {
|
|
1644
|
+
display: flex;
|
|
1645
|
+
align-items: center;
|
|
1646
|
+
justify-content: center;
|
|
1647
|
+
flex-shrink: 0;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
.fm-grid.view-list .fm-item-icon {
|
|
1651
|
+
width: 32px;
|
|
1652
|
+
height: 32px;
|
|
1653
|
+
flex-shrink: 0;
|
|
1654
|
+
border-radius: var(--radius-sm);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
.fm-grid.view-list .fm-item-name {
|
|
1658
|
+
flex: 1 1 auto;
|
|
1659
|
+
min-width: 0;
|
|
1660
|
+
font-size: 12.5px;
|
|
1661
|
+
font-weight: 600;
|
|
1662
|
+
color: var(--dark-color-ml);
|
|
1663
|
+
overflow: hidden;
|
|
1664
|
+
text-overflow: ellipsis;
|
|
1665
|
+
white-space: nowrap;
|
|
1409
1666
|
}
|
|
1410
1667
|
|
|
1411
1668
|
.fm-grid.view-list .fm-item-size,
|
|
1412
1669
|
.fm-grid.view-list .fm-item-date,
|
|
1413
|
-
.fm-grid.view-list .fm-
|
|
1670
|
+
.fm-grid.view-list .fm-star-wrap {
|
|
1414
1671
|
display: none;
|
|
1415
1672
|
}
|
|
1416
1673
|
|
|
1674
|
+
.fm-grid.view-list .fm-item-menu {
|
|
1675
|
+
flex-shrink: 0;
|
|
1676
|
+
width: 28px;
|
|
1677
|
+
height: 28px;
|
|
1678
|
+
display: flex;
|
|
1679
|
+
align-items: center;
|
|
1680
|
+
justify-content: center;
|
|
1681
|
+
opacity: 1;
|
|
1682
|
+
color: var(--gray-color-ml);
|
|
1683
|
+
border-radius: 6px;
|
|
1684
|
+
background: transparent;
|
|
1685
|
+
box-shadow: none;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
.fm-grid.view-list .fm-item-menu:active {
|
|
1689
|
+
background: var(--light-color-ml);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1417
1692
|
.fm-list-header {
|
|
1418
1693
|
display: none;
|
|
1419
1694
|
}
|
|
@@ -56,13 +56,8 @@ export function isLoggedIn() {
|
|
|
56
56
|
return Boolean(token && server_url);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function getStoredServerUrl() {
|
|
64
|
-
return readConfig().server_url || null;
|
|
65
|
-
}
|
|
59
|
+
export const getStoredToken = () => readConfig().token || null;
|
|
60
|
+
export const getStoredServerUrl = () => readConfig().server_url || null;
|
|
66
61
|
|
|
67
62
|
export async function deviceFlowLogin({ server_url, profile_key }) {
|
|
68
63
|
const baseUrl = String(server_url).replace(/\/+$/, '');
|
|
@@ -87,26 +82,21 @@ export async function deviceFlowLogin({ server_url, profile_key }) {
|
|
|
87
82
|
throw err;
|
|
88
83
|
}
|
|
89
84
|
|
|
90
|
-
const verification_url = deviceResp
|
|
91
|
-
const user_code = deviceResp.user_code;
|
|
92
|
-
const device_code = deviceResp.device_code;
|
|
93
|
-
const expires_in = deviceResp.expires_in;
|
|
94
|
-
const interval = deviceResp.interval;
|
|
95
|
-
const profile = deviceResp.profile;
|
|
85
|
+
const { verification_url = '', user_code = '', device_code, expires_in = 900, interval = 5, profile } = deviceResp;
|
|
96
86
|
|
|
97
87
|
console.log('\n┌─────────────────────────────────────────────────────────┐');
|
|
98
88
|
console.log('│ │');
|
|
99
89
|
console.log('│ 🌐 Open this URL in your browser to approve: │');
|
|
100
|
-
console.log(`│ ${
|
|
90
|
+
console.log(`│ ${verification_url.padEnd(53)}│`);
|
|
101
91
|
console.log('│ │');
|
|
102
|
-
console.log(`│ 🔢 Or enter this code: ${
|
|
92
|
+
console.log(`│ 🔢 Or enter this code: ${user_code.padEnd(30)}│`);
|
|
103
93
|
console.log('│ │');
|
|
104
94
|
console.log('└─────────────────────────────────────────────────────────┘\n');
|
|
105
95
|
|
|
106
96
|
if (profile) console.log(` Profile: ${profile.name} (scope: ${profile.scope || 'full'})`);
|
|
107
|
-
console.log(` Waiting for approval (expires in ${Math.floor(
|
|
97
|
+
console.log(` Waiting for approval (expires in ${Math.floor(expires_in / 60)} minutes)...\n`);
|
|
108
98
|
|
|
109
|
-
const pollInterval =
|
|
99
|
+
const pollInterval = interval * 1000;
|
|
110
100
|
const deadline = Date.now() + MAX_POLL_TIME_MS;
|
|
111
101
|
let attempts = 0;
|
|
112
102
|
|
|
@@ -129,7 +119,7 @@ export async function deviceFlowLogin({ server_url, profile_key }) {
|
|
|
129
119
|
console.log(` Token prefix: ${token_prefix}...`);
|
|
130
120
|
return {
|
|
131
121
|
token: pollBody.token,
|
|
132
|
-
token_prefix
|
|
122
|
+
token_prefix,
|
|
133
123
|
username: pollBody.username,
|
|
134
124
|
profile: profile || null,
|
|
135
125
|
};
|
|
@@ -148,3 +138,4 @@ export async function deviceFlowLogin({ server_url, profile_key }) {
|
|
|
148
138
|
|
|
149
139
|
throw new Error('Login timed out. Please try again.');
|
|
150
140
|
}
|
|
141
|
+
|
package/lib/src/config/index.js
CHANGED
|
@@ -14,7 +14,6 @@ dotenv.config();
|
|
|
14
14
|
const debugConfig = createLogger('config');
|
|
15
15
|
const require = createRequire(import.meta.url);
|
|
16
16
|
|
|
17
|
-
// Load mbkbucket package.json
|
|
18
17
|
let packageJson;
|
|
19
18
|
try {
|
|
20
19
|
packageJson = require("mbkbucket/package.json");
|
|
@@ -22,7 +21,6 @@ try {
|
|
|
22
21
|
packageJson = require("../../../package.json");
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
// Parent project version
|
|
26
24
|
let appVersion = "unknown";
|
|
27
25
|
try {
|
|
28
26
|
appVersion = require("../../../../package.json")?.version ?? require(`${process.cwd()}/package.json`)?.version ?? "unknown";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const TRUE_VALUES = new Set(['true', 't', '1', 'yes', 'y', 'on']);
|
|
2
2
|
const FALSE_VALUES = new Set(['false', 'f', '0', 'no', 'n', 'off']);
|
|
3
|
+
const REQUIRED_BUCKET_FIELDS = ['BUCKET_NAME', 'ACCESS_KEY_ID', 'SECRET_ACCESS_KEY', 'ENDPOINT'];
|
|
3
4
|
|
|
4
5
|
export function normalizeBooleanLike(value) {
|
|
5
6
|
if (typeof value === 'boolean') return value;
|
|
@@ -56,14 +57,12 @@ export function parseAndValidateBucketConnection(rawValue) {
|
|
|
56
57
|
throw new Error('BucketConnection must be a non-empty object');
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
const requiredFields = ['BUCKET_NAME', 'ACCESS_KEY_ID', 'SECRET_ACCESS_KEY', 'ENDPOINT'];
|
|
60
|
-
|
|
61
60
|
for (const [bucketName, config] of Object.entries(parsed)) {
|
|
62
61
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
63
62
|
throw new Error(`Bucket '${bucketName}' configuration must be an object, not a string. Remove quotes around the inner JSON object.`);
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
const missingFields =
|
|
65
|
+
const missingFields = REQUIRED_BUCKET_FIELDS.filter(field => !config[field]);
|
|
67
66
|
if (missingFields.length) {
|
|
68
67
|
throw new Error(`Bucket '${bucketName}' is missing required fields: ${missingFields.join(', ')}`);
|
|
69
68
|
}
|
|
@@ -71,3 +70,4 @@ export function parseAndValidateBucketConnection(rawValue) {
|
|
|
71
70
|
|
|
72
71
|
return parsed;
|
|
73
72
|
}
|
|
73
|
+
|
|
@@ -292,7 +292,7 @@ export async function createFolder(req, res) {
|
|
|
292
292
|
const effectivePrefix = resolvePrefix(req, res, prefix);
|
|
293
293
|
if (effectivePrefix === null) return;
|
|
294
294
|
|
|
295
|
-
const cleanName = String(folderName).replace(
|
|
295
|
+
const cleanName = String(folderName).replace(/^\/+|\/+$/g, '');
|
|
296
296
|
const cleanPrefix = effectivePrefix.replace(/\/+$/, '');
|
|
297
297
|
const folderKey = (effectivePrefix && !cleanName.startsWith(effectivePrefix))
|
|
298
298
|
? `${cleanPrefix}/${cleanName}/`
|
|
@@ -15,7 +15,7 @@ export function renderBucketDashboard(req, res) {
|
|
|
15
15
|
|
|
16
16
|
const mainLayoutPath = path.resolve(process.cwd(), "views", "layouts", "main");
|
|
17
17
|
const hasMainLayout = fs.existsSync(`${mainLayoutPath}.handlebars`) || fs.existsSync(mainLayoutPath);
|
|
18
|
-
const portalLayout = (isPortal && hasMainLayout)
|
|
18
|
+
const portalLayout = (isPortal && hasMainLayout) && mainLayoutPath;
|
|
19
19
|
|
|
20
20
|
let selectedBucket;
|
|
21
21
|
let error = req.query.error;
|
|
@@ -33,10 +33,10 @@ export function renderBucketDashboard(req, res) {
|
|
|
33
33
|
isDefault: name === configuredDefaultBucket
|
|
34
34
|
}));
|
|
35
35
|
|
|
36
|
-
renderPage(req, res, bucketfilePath, portalLayout
|
|
36
|
+
renderPage(req, res, bucketfilePath, Boolean(portalLayout), {
|
|
37
37
|
page: "Admin Bucket",
|
|
38
38
|
helpers: commonHandlebarsHelpers,
|
|
39
|
-
|
|
39
|
+
layout: portalLayout || false,
|
|
40
40
|
bucketvar: selectedBucket,
|
|
41
41
|
selectedBucket,
|
|
42
42
|
bucketOptions,
|
|
@@ -46,3 +46,4 @@ export function renderBucketDashboard(req, res) {
|
|
|
46
46
|
availableBuckets: allBuckets
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
+
|
|
@@ -19,10 +19,9 @@ async function getInfoData() {
|
|
|
19
19
|
* Render the mbkbucket info page (Handlebars).
|
|
20
20
|
*/
|
|
21
21
|
export async function infoPage(req, res) {
|
|
22
|
-
const data = await getInfoData();
|
|
23
22
|
renderPage(req, res, "mbkbucket_info.handlebars", false, {
|
|
24
23
|
page: "MBK Bucket Info",
|
|
25
|
-
...
|
|
24
|
+
...(await getInfoData())
|
|
26
25
|
});
|
|
27
26
|
}
|
|
28
27
|
|
|
@@ -60,9 +60,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
|
|
|
60
60
|
return res.status(500).json({ message: 'Failed to fetch file metadata', error: err.message });
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
if (!meta?.exists) {
|
|
64
|
-
return res.status(404).json({ message: 'File not found', key: keyToUse });
|
|
65
|
-
}
|
|
63
|
+
if (!meta?.exists) return res.status(404).json({ message: 'File not found', key: keyToUse });
|
|
66
64
|
|
|
67
65
|
const etag = `"${meta.LastModified?.getTime() || Date.now()}-${meta.ContentLength || 0}"`;
|
|
68
66
|
if (ifNoneMatch === etag) return res.status(304).end();
|
|
@@ -2,17 +2,14 @@
|
|
|
2
2
|
* Wraps an async Express route handler so that any thrown/rejected errors
|
|
3
3
|
* are forwarded to `next(err)` instead of crashing the process.
|
|
4
4
|
*/
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
Promise.resolve(handler(req, res, next)).catch(err => (onError ? onError(err, req, res, next) : next(err)));
|
|
8
|
-
};
|
|
9
|
-
}
|
|
5
|
+
export const wrap = (handler, onError) => (req, res, next) =>
|
|
6
|
+
Promise.resolve(handler(req, res, next)).catch(err => onError ? onError(err, req, res, next) : next(err));
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
9
|
* Combines wrap() with a standard JSON error response for API routes.
|
|
13
10
|
*/
|
|
14
|
-
export
|
|
15
|
-
|
|
11
|
+
export const wrapApi = (handler, fallbackMessage = 'Request failed') =>
|
|
12
|
+
wrap(handler, (err, _req, res) => {
|
|
16
13
|
console.error(`[mbkbucket] ${err.stack || err.message}`);
|
|
17
14
|
res.status(500).json({
|
|
18
15
|
success: false,
|
|
@@ -20,4 +17,3 @@ export function wrapApi(handler, fallbackMessage = 'Request failed') {
|
|
|
20
17
|
error: err.message || fallbackMessage,
|
|
21
18
|
});
|
|
22
19
|
});
|
|
23
|
-
}
|
|
@@ -2,14 +2,11 @@ import { createLogger } from "#logger";
|
|
|
2
2
|
import { isJsonRequest, sendError, sanitizeErrorDetails } from "mbkauthe";
|
|
3
3
|
|
|
4
4
|
const debugServer = createLogger('server');
|
|
5
|
-
|
|
6
5
|
const BASE_ERROR_DATA = { layout: false, pagename: "Home", page: "/" };
|
|
7
6
|
|
|
8
7
|
export function notFoundHandler(req, res, next) {
|
|
9
8
|
const url = req.originalUrl || req.url || '';
|
|
10
|
-
if (!url.startsWith('/mbkbucket') && typeof next === 'function')
|
|
11
|
-
return next();
|
|
12
|
-
}
|
|
9
|
+
if (!url.startsWith('/mbkbucket') && typeof next === 'function') return next();
|
|
13
10
|
|
|
14
11
|
debugServer("Path not found: %s %s", req.method, req.url);
|
|
15
12
|
|
|
@@ -31,7 +28,6 @@ export function notFoundHandler(req, res, next) {
|
|
|
31
28
|
|
|
32
29
|
export function globalErrorHandler(err, req, res, _next) {
|
|
33
30
|
console.error(`[mbkbucket] ${err.stack || err}`);
|
|
34
|
-
|
|
35
31
|
const statusCode = Number(err.status || err.statusCode || 500);
|
|
36
32
|
|
|
37
33
|
if (isJsonRequest(req)) {
|
|
@@ -49,6 +45,6 @@ export function globalErrorHandler(err, req, res, _next) {
|
|
|
49
45
|
code: statusCode,
|
|
50
46
|
error: statusCode >= 500 ? "Internal app Error" : (err.name || "Client Error"),
|
|
51
47
|
message: err.message || "An unexpected error occurred on the app.",
|
|
52
|
-
...(sanitizedDetails
|
|
48
|
+
...(sanitizedDetails && { details: sanitizedDetails }),
|
|
53
49
|
});
|
|
54
50
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import rateLimit from "express-rate-limit";
|
|
2
2
|
|
|
3
3
|
export const generalLimiter = rateLimit({
|
|
4
|
-
windowMs: 2 * 60 * 1000,
|
|
4
|
+
windowMs: 2 * 60 * 1000,
|
|
5
5
|
max: 150,
|
|
6
6
|
standardHeaders: true,
|
|
7
7
|
legacyHeaders: false,
|
|
@@ -11,7 +11,7 @@ export const generalLimiter = rateLimit({
|
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
export const pviewRateLimit = rateLimit({
|
|
14
|
-
windowMs: 60 * 1000,
|
|
14
|
+
windowMs: 60 * 1000,
|
|
15
15
|
max: 10,
|
|
16
16
|
standardHeaders: 'draft-7',
|
|
17
17
|
legacyHeaders: false,
|
|
@@ -4,12 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const BLOCKED_UA_PATTERNS = /curl|wget|python-requests|python-urllib|go-http|scrapy|libwww|java\/|bot|crawl|spider|headless|phantom|selenium|puppeteer|playwright|postman|insomnia|httpie/i;
|
|
7
|
-
|
|
8
|
-
const SUSPICIOUS_HEADERS = [
|
|
9
|
-
'x-devtools-emulate-network-conditions-client-id',
|
|
10
|
-
'x-chromedriver-clientid',
|
|
11
|
-
'x-automated-tool'
|
|
12
|
-
];
|
|
7
|
+
const SUSPICIOUS_HEADERS = ['x-devtools-emulate-network-conditions-client-id', 'x-chromedriver-clientid', 'x-automated-tool'];
|
|
13
8
|
|
|
14
9
|
export function pviewSecurity(req, res, next) {
|
|
15
10
|
const ua = req.headers['user-agent'] || '';
|
|
@@ -22,6 +17,5 @@ export function pviewSecurity(req, res, next) {
|
|
|
22
17
|
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
23
18
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
24
19
|
res.removeHeader('Access-Control-Allow-Origin');
|
|
25
|
-
|
|
26
20
|
next();
|
|
27
21
|
}
|
|
@@ -27,13 +27,12 @@ export function createApiRoutes({
|
|
|
27
27
|
deleteAuthorization,
|
|
28
28
|
} = {}) {
|
|
29
29
|
const apiRouter = express.Router();
|
|
30
|
-
const
|
|
31
|
-
const viewAuth = viewAuthorization
|
|
32
|
-
const uploadAuth = uploadAuthorization
|
|
33
|
-
const deleteAuth = deleteAuthorization
|
|
30
|
+
const defaultAuth = authorization ?? sessRole('superadmin');
|
|
31
|
+
const viewAuth = viewAuthorization ?? defaultAuth;
|
|
32
|
+
const uploadAuth = uploadAuthorization ?? defaultAuth;
|
|
33
|
+
const deleteAuth = deleteAuthorization ?? defaultAuth;
|
|
34
34
|
|
|
35
35
|
apiRouter.use(requireBucketApi);
|
|
36
|
-
|
|
37
36
|
apiRouter.post('/api/cleanup-uploads', deleteAuth, cleanupUploads);
|
|
38
37
|
apiRouter.get('/api/incomplete-uploads', viewAuth, listIncompleteUploads);
|
|
39
38
|
apiRouter.get('/api/files', viewAuth, listFiles);
|