mbkbucket 1.5.0 → 2.0.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/.env.example +2 -2
- package/README.md +72 -0
- package/index.d.ts +87 -49
- package/index.js +15 -13
- package/lib/config/index.js +9 -89
- package/lib/config/validation.js +86 -0
- package/lib/routes/api.js +139 -45
- package/lib/routes/index.js +70 -1
- package/lib/routes/view.js +71 -18
- package/lib/s3.js +64 -32
- package/package.json +2 -2
- package/public/bucketadmin.css +447 -17
- package/public/bucketadmin.js +272 -82
- package/test/config-validation.test.js +116 -0
- package/views/bucket.handlebars +66 -31
- package/views/bucketadmincontent.handlebars +30 -9
- package/views/bucketportal.handlebars +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { parseAndValidateMbkbucketVar, parseAndValidateBucketConnection, normalizeBooleanLike } from '../lib/config/validation.js';
|
|
4
|
+
|
|
5
|
+
test('parseAndValidateMbkbucketVar applies defaults when empty', () => {
|
|
6
|
+
const cfg = parseAndValidateMbkbucketVar(undefined);
|
|
7
|
+
assert.equal(cfg.p_view_inline, true);
|
|
8
|
+
assert.equal(cfg.publiView_enabled, false);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('parseAndValidateMbkbucketVar normalizes boolean-like strings', () => {
|
|
12
|
+
const cfg = parseAndValidateMbkbucketVar(JSON.stringify({
|
|
13
|
+
p_view_inline: 'false',
|
|
14
|
+
publiView_enabled: 'true'
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
assert.equal(cfg.p_view_inline, false);
|
|
18
|
+
assert.equal(cfg.publiView_enabled, true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('parseAndValidateMbkbucketVar rejects invalid JSON', () => {
|
|
22
|
+
assert.throws(() => parseAndValidateMbkbucketVar('{oops'), /Invalid JSON/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('parseAndValidateMbkbucketVar rejects invalid boolean values', () => {
|
|
26
|
+
assert.throws(() => parseAndValidateMbkbucketVar(JSON.stringify({
|
|
27
|
+
p_view_inline: 'not-bool'
|
|
28
|
+
})), /must be a boolean/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('parseAndValidateMbkbucketVar accepts common boolean aliases', () => {
|
|
32
|
+
const cfg = parseAndValidateMbkbucketVar(JSON.stringify({
|
|
33
|
+
p_view_inline: ' ON ',
|
|
34
|
+
publiView_enabled: '0'
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
assert.equal(cfg.p_view_inline, true);
|
|
38
|
+
assert.equal(cfg.publiView_enabled, false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('parseAndValidateMbkbucketVar rejects non-object values', () => {
|
|
42
|
+
assert.throws(() => parseAndValidateMbkbucketVar('null'), /must be a valid object/);
|
|
43
|
+
assert.throws(() => parseAndValidateMbkbucketVar('[]'), /must be a valid object/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('normalizeBooleanLike returns untouched for unsupported values', () => {
|
|
47
|
+
assert.equal(normalizeBooleanLike('maybe'), 'maybe');
|
|
48
|
+
assert.equal(normalizeBooleanLike(123), 123);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('parseAndValidateBucketConnection accepts multi-bucket config', () => {
|
|
52
|
+
const raw = JSON.stringify({
|
|
53
|
+
R2_Bucket: {
|
|
54
|
+
BUCKET_NAME: 'my-r2-bucket',
|
|
55
|
+
ACCESS_KEY_ID: 'abc',
|
|
56
|
+
SECRET_ACCESS_KEY: 'def',
|
|
57
|
+
ENDPOINT: 'https://example.r2.cloudflarestorage.com'
|
|
58
|
+
},
|
|
59
|
+
S3_Bucket: {
|
|
60
|
+
BUCKET_NAME: 'my-s3-bucket',
|
|
61
|
+
ACCESS_KEY_ID: 'ghi',
|
|
62
|
+
SECRET_ACCESS_KEY: 'jkl',
|
|
63
|
+
ENDPOINT: 'https://s3.ap-southeast-1.amazonaws.com'
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const cfg = parseAndValidateBucketConnection(raw);
|
|
68
|
+
assert.deepEqual(Object.keys(cfg), ['R2_Bucket', 'S3_Bucket']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('parseAndValidateBucketConnection returns null when unset', () => {
|
|
72
|
+
assert.equal(parseAndValidateBucketConnection(undefined), null);
|
|
73
|
+
assert.equal(parseAndValidateBucketConnection(''), null);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('parseAndValidateBucketConnection rejects invalid JSON', () => {
|
|
77
|
+
assert.throws(() => parseAndValidateBucketConnection('{oops'), /not valid JSON/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('parseAndValidateBucketConnection rejects empty object and array', () => {
|
|
81
|
+
assert.throws(() => parseAndValidateBucketConnection('{}'), /non-empty object/);
|
|
82
|
+
assert.throws(() => parseAndValidateBucketConnection('[]'), /non-empty object/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('parseAndValidateBucketConnection rejects missing required fields', () => {
|
|
86
|
+
const raw = JSON.stringify({
|
|
87
|
+
bad: {
|
|
88
|
+
BUCKET_NAME: 'x',
|
|
89
|
+
ACCESS_KEY_ID: 'y',
|
|
90
|
+
ENDPOINT: 'https://example.com'
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
assert.throws(() => parseAndValidateBucketConnection(raw), /missing required fields/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('parseAndValidateBucketConnection rejects quoted inner object shape', () => {
|
|
98
|
+
const raw = JSON.stringify({
|
|
99
|
+
bad: '{"BUCKET_NAME":"x"}'
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert.throws(() => parseAndValidateBucketConnection(raw), /must be an object/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('parseAndValidateBucketConnection rejects empty required fields', () => {
|
|
106
|
+
const raw = JSON.stringify({
|
|
107
|
+
bad: {
|
|
108
|
+
BUCKET_NAME: '',
|
|
109
|
+
ACCESS_KEY_ID: 'abc',
|
|
110
|
+
SECRET_ACCESS_KEY: 'def',
|
|
111
|
+
ENDPOINT: 'https://example.com'
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
assert.throws(() => parseAndValidateBucketConnection(raw), /missing required fields/);
|
|
116
|
+
});
|
package/views/bucket.handlebars
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<title>{{#if page}}{{page}} - {{/if}}Web Portal</title>
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
-
<link href="https://fonts.googleapis.com/css2?family=
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
11
11
|
<script src="/mbkauthe/main.js" defer></script>
|
|
12
12
|
<script src="https://portal.mbktech.org/Assets/Scripts/timeoutlogout.js" defer></script>
|
|
13
13
|
<script src="https://portal.mbktech.org/Assets/Scripts/auto-ids.js" defer></script>
|
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
<link rel="icon" type="image/x-icon" href="/icon.svg">
|
|
16
16
|
<style>
|
|
17
17
|
:root {
|
|
18
|
-
--primary-color-ml: #
|
|
19
|
-
--primary-light-color-ml: #
|
|
20
|
-
--primary-dark-color-ml: #
|
|
21
|
-
--secondary-color-ml: #
|
|
22
|
-
--dark-color-ml: #
|
|
23
|
-
--light-color-ml: #
|
|
24
|
-
--gray-color-ml: #
|
|
25
|
-
--gray-light-color-ml: #
|
|
18
|
+
--primary-color-ml: #0f766e;
|
|
19
|
+
--primary-light-color-ml: #d7f4ee;
|
|
20
|
+
--primary-dark-color-ml: #0b4f4a;
|
|
21
|
+
--secondary-color-ml: #1f8f84;
|
|
22
|
+
--dark-color-ml: #1f2a2a;
|
|
23
|
+
--light-color-ml: #f4f7f3;
|
|
24
|
+
--gray-color-ml: #667c79;
|
|
25
|
+
--gray-light-color-ml: #dce8e3;
|
|
26
26
|
--danger-color-ml: #dc2626;
|
|
27
27
|
--warning-color-ml: #d97706;
|
|
28
28
|
--success-color-ml: #16a34a;
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
--color-accent: var(--primary-color-ml);
|
|
40
40
|
--color-text: var(--dark-color-ml);
|
|
41
41
|
--color-bg: var(--light-color-ml);
|
|
42
|
-
--font-family: "
|
|
43
|
-
--font-family-headings: "
|
|
42
|
+
--font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
|
43
|
+
--font-family-headings: "Sora", "Segoe UI", sans-serif
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
* {
|
|
@@ -50,25 +50,56 @@
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
body {
|
|
53
|
-
font-family:
|
|
54
|
-
background:
|
|
53
|
+
font-family: var(--font-family);
|
|
54
|
+
background:
|
|
55
|
+
radial-gradient(circle at 12% 14%, rgba(16, 185, 129, 0.12), transparent 44%),
|
|
56
|
+
radial-gradient(circle at 86% 8%, rgba(244, 114, 38, 0.12), transparent 40%),
|
|
57
|
+
linear-gradient(160deg, #eef7f2 0, #f8fbf7 52%, #eaf2ef 100%);
|
|
55
58
|
color: var(--dark-color-ml);
|
|
56
59
|
display: flex;
|
|
57
60
|
min-height: 100vh;
|
|
58
61
|
overflow-x: hidden;
|
|
59
62
|
line-height: 1.6;
|
|
60
|
-
font-size: 14px
|
|
63
|
+
font-size: 14px;
|
|
64
|
+
position: relative;
|
|
65
|
+
isolation: isolate
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
body::before,
|
|
69
|
+
body::after {
|
|
70
|
+
content: "";
|
|
71
|
+
position: fixed;
|
|
72
|
+
border-radius: 999px;
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
z-index: -1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
body::before {
|
|
78
|
+
width: 260px;
|
|
79
|
+
height: 260px;
|
|
80
|
+
left: -80px;
|
|
81
|
+
top: 18vh;
|
|
82
|
+
background: radial-gradient(circle, rgba(15, 118, 110, 0.16) 0, rgba(15, 118, 110, 0) 70%)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
body::after {
|
|
86
|
+
width: 300px;
|
|
87
|
+
height: 300px;
|
|
88
|
+
right: -120px;
|
|
89
|
+
bottom: 10vh;
|
|
90
|
+
background: radial-gradient(circle, rgba(251, 146, 60, 0.18) 0, rgba(251, 146, 60, 0) 72%)
|
|
61
91
|
}
|
|
62
92
|
|
|
63
93
|
/* Removed unused sidebar/menu styles to reduce CSS size. */
|
|
64
94
|
|
|
65
95
|
.logo {
|
|
66
|
-
font-
|
|
96
|
+
font-family: var(--font-family-headings);
|
|
97
|
+
font-size: 1.65rem;
|
|
67
98
|
font-weight: 700;
|
|
68
|
-
color:
|
|
99
|
+
color: #0b4f4a;
|
|
69
100
|
display: flex;
|
|
70
101
|
align-items: center;
|
|
71
|
-
gap: .
|
|
102
|
+
gap: .5rem
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
.logo img {
|
|
@@ -84,15 +115,15 @@
|
|
|
84
115
|
}
|
|
85
116
|
|
|
86
117
|
.top-nav {
|
|
87
|
-
background:
|
|
118
|
+
background: rgba(255, 255, 255, .85);
|
|
88
119
|
backdrop-filter: blur(10px);
|
|
89
|
-
padding: .5rem
|
|
120
|
+
padding: .75rem 1.5rem;
|
|
90
121
|
display: flex;
|
|
91
122
|
align-items: center;
|
|
92
123
|
justify-content: flex-end;
|
|
93
124
|
box-shadow: var(--shadow-md-ml);
|
|
94
125
|
z-index: 90;
|
|
95
|
-
border-bottom: 1px solid
|
|
126
|
+
border-bottom: 1px solid rgba(255, 255, 255, .75);
|
|
96
127
|
gap: 1rem
|
|
97
128
|
}
|
|
98
129
|
|
|
@@ -100,7 +131,7 @@
|
|
|
100
131
|
|
|
101
132
|
.content-area {
|
|
102
133
|
flex: 1;
|
|
103
|
-
padding:
|
|
134
|
+
padding: 1.6rem;
|
|
104
135
|
overflow-y: auto
|
|
105
136
|
}
|
|
106
137
|
|
|
@@ -125,13 +156,17 @@
|
|
|
125
156
|
display: flex;
|
|
126
157
|
align-items: center;
|
|
127
158
|
justify-content: space-between;
|
|
128
|
-
margin-bottom:
|
|
129
|
-
padding
|
|
130
|
-
border
|
|
159
|
+
margin-bottom: 1.2rem;
|
|
160
|
+
padding: 1rem 1.2rem;
|
|
161
|
+
border: 1px solid rgba(255, 255, 255, .9);
|
|
162
|
+
border-radius: 1rem;
|
|
163
|
+
background: rgba(255, 255, 255, .74);
|
|
164
|
+
box-shadow: 0 12px 34px rgba(15, 23, 42, .07)
|
|
131
165
|
}
|
|
132
166
|
|
|
133
167
|
.page-title {
|
|
134
|
-
font-
|
|
168
|
+
font-family: var(--font-family-headings);
|
|
169
|
+
font-size: 1.5rem;
|
|
135
170
|
font-weight: 700;
|
|
136
171
|
color: var(--dark-color-ml);
|
|
137
172
|
margin: 0
|
|
@@ -142,10 +177,10 @@
|
|
|
142
177
|
align-items: center;
|
|
143
178
|
color: var(--gray-color-ml);
|
|
144
179
|
font-size: .875rem;
|
|
145
|
-
background:
|
|
146
|
-
padding: .
|
|
147
|
-
border-radius:
|
|
148
|
-
border: 1px solid
|
|
180
|
+
background: linear-gradient(135deg, rgba(15, 118, 110, .08), rgba(251, 146, 60, .08));
|
|
181
|
+
padding: .4rem .85rem;
|
|
182
|
+
border-radius: 999px;
|
|
183
|
+
border: 1px solid rgba(15, 118, 110, .15)
|
|
149
184
|
}
|
|
150
185
|
|
|
151
186
|
.breadcrumb a {
|
|
@@ -204,8 +239,8 @@
|
|
|
204
239
|
<div class="main-content">
|
|
205
240
|
<nav class="top-nav">
|
|
206
241
|
<div class="logo">
|
|
207
|
-
<img onclick="window.location='/'" src="/icon.svg" alt="Logo">
|
|
208
|
-
<span onclick="window.location='/'" style="cursor:pointer;">BKBucket</span>
|
|
242
|
+
<img onclick="window.location='/mbkbucket'" src="/icon.svg" alt="Logo">
|
|
243
|
+
<span onclick="window.location='/mbkbucket'" style="cursor:pointer;">BKBucket</span>
|
|
209
244
|
</div>
|
|
210
245
|
|
|
211
246
|
{{> profilemenu }}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{{!-- File Management System - Bucket Dashboard --}}
|
|
2
|
-
<link rel="stylesheet" href="/mbkbucket/bucketadmin.css
|
|
2
|
+
<link rel="stylesheet" href="/mbkbucket/bucketadmin.css{{mbkbucket_cachebuster}}">
|
|
3
3
|
<div id="bucketAppData" data-app-name="{{APP_NAME}}" hidden></div>
|
|
4
4
|
<!-- File Statistics -->
|
|
5
|
-
<div class="dashboard-card file-stats">
|
|
5
|
+
<div class="dashboard-card file-stats reveal-item reveal-delay-1">
|
|
6
6
|
<div class="card-body">
|
|
7
7
|
<div class="stats-content">
|
|
8
8
|
<div class="stats-icon">
|
|
@@ -12,10 +12,24 @@
|
|
|
12
12
|
<h2 id="totalFilesCount">Loading...</h2>
|
|
13
13
|
<p id="filterInfo">Loading file information...</p>
|
|
14
14
|
</div>
|
|
15
|
-
<div
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
15
|
+
<div class="bucket-switcher-wrap">
|
|
16
|
+
<div class="bucket-switcher" role="group" aria-label="Bucket selection">
|
|
17
|
+
<label for="bucketSelector" class="bucket-switcher-label">Active Bucket</label>
|
|
18
|
+
<div class="bucket-switcher-control">
|
|
19
|
+
<i class="fas fa-database bucket-switcher-icon" aria-hidden="true"></i>
|
|
20
|
+
<select id="bucketSelector" class="form-control-sm bucket-switcher-select" aria-label="Select connected bucket">
|
|
21
|
+
{{#if bucketOptions.length}}
|
|
22
|
+
{{#each bucketOptions}}
|
|
23
|
+
<option value="{{name}}" {{#if isSelected}}selected{{/if}}>{{name}}{{#if isDefault}} (defualt){{/if}}</option>
|
|
24
|
+
{{/each}}
|
|
25
|
+
{{else}}
|
|
26
|
+
<option value="">No buckets configured</option>
|
|
27
|
+
{{/if}}
|
|
28
|
+
</select>
|
|
29
|
+
<i class="fas fa-chevron-down bucket-switcher-caret" aria-hidden="true"></i>
|
|
30
|
+
</div>
|
|
31
|
+
<small id="bucketSwitchStatus" class="bucket-switcher-status" style="display:none;">Switching bucket...</small>
|
|
32
|
+
</div>
|
|
19
33
|
</div>
|
|
20
34
|
</div>
|
|
21
35
|
</div>
|
|
@@ -23,7 +37,7 @@
|
|
|
23
37
|
|
|
24
38
|
{{> bucketadmin_alerts }}
|
|
25
39
|
|
|
26
|
-
<div class="mb-4">
|
|
40
|
+
<div class="mb-4 reveal-item reveal-delay-2">
|
|
27
41
|
<!-- Upload Form -->
|
|
28
42
|
<div class="col-lg-12">
|
|
29
43
|
<div class="dashboard-card h-100">
|
|
@@ -92,7 +106,7 @@
|
|
|
92
106
|
</div>
|
|
93
107
|
|
|
94
108
|
<!-- File List -->
|
|
95
|
-
<div class="dashboard-card">
|
|
109
|
+
<div class="dashboard-card reveal-item reveal-delay-3">
|
|
96
110
|
<div class="card-header file-list-header">
|
|
97
111
|
<h5 class="card-title">
|
|
98
112
|
<div>
|
|
@@ -108,6 +122,13 @@
|
|
|
108
122
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearSearch()" aria-label="Clear search"><i class="fas fa-times"></i></button>
|
|
109
123
|
</div>
|
|
110
124
|
|
|
125
|
+
<div class="quick-filter-group" role="group" aria-label="Quick filters">
|
|
126
|
+
<button type="button" class="btn btn-sm quick-filter-btn active" data-filter="all" onclick="setQuickFilter('all')">All</button>
|
|
127
|
+
<button type="button" class="btn btn-sm quick-filter-btn" data-filter="media" onclick="setQuickFilter('media')">Media</button>
|
|
128
|
+
<button type="button" class="btn btn-sm quick-filter-btn" data-filter="docs" onclick="setQuickFilter('docs')">Docs</button>
|
|
129
|
+
<button type="button" class="btn btn-sm quick-filter-btn" data-filter="code" onclick="setQuickFilter('code')">Code</button>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
111
132
|
<div class="btn-group" role="group" aria-label="Actions">
|
|
112
133
|
<button type="button" class="btn btn-sm btn-success" onclick="createFile()" title="New Text File">
|
|
113
134
|
<i class="fas fa-file-alt"></i> New File
|
|
@@ -148,4 +169,4 @@
|
|
|
148
169
|
<!-- Toast Container -->
|
|
149
170
|
<div id="toast-container" class="toast-container"></div>
|
|
150
171
|
|
|
151
|
-
<script src="/mbkbucket/bucketadmin.js
|
|
172
|
+
<script src="/mbkbucket/bucketadmin.js{{mbkbucket_cachebuster}}"></script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{> bucketadmincontent }}
|