mbkbucket 3.8.2 → 4.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.
Files changed (38) hide show
  1. package/.env.example +15 -14
  2. package/README.md +561 -377
  3. package/index.js +32 -30
  4. package/lib/cli.js +154 -565
  5. package/lib/public/bucketadmin.css +2042 -1708
  6. package/lib/public/bucketadmin.js +1622 -1286
  7. package/lib/public/mbkbucket-helper-demo.html +4 -24
  8. package/lib/public/mbkbucket-helper.css +28 -34
  9. package/lib/public/mbkbucket-helper.js +4 -3
  10. package/lib/src/config/validation.js +86 -73
  11. package/lib/src/controllers/api.controller.js +459 -410
  12. package/lib/src/controllers/dashboard.controller.js +69 -49
  13. package/lib/src/controllers/info.controller.js +34 -3
  14. package/lib/src/controllers/view.controller.js +233 -231
  15. package/lib/src/core/storage/errors.js +62 -0
  16. package/lib/src/core/storage/gdrive-auth.js +193 -0
  17. package/lib/src/core/storage/gdrive-provider.js +656 -0
  18. package/lib/src/core/storage/index.js +14 -0
  19. package/lib/src/core/storage/models.js +80 -0
  20. package/lib/src/core/storage/path-strategy.js +65 -0
  21. package/lib/src/core/storage/s3-provider.js +491 -0
  22. package/lib/src/core/storage/storage-manager.js +203 -0
  23. package/lib/src/core/storage/storage-provider.js +217 -0
  24. package/lib/src/middleware/bucket-resolver.js +37 -37
  25. package/lib/src/routes/api.routes.js +5 -1
  26. package/lib/src/routes/index.js +60 -60
  27. package/lib/src/routes/view.routes.js +28 -25
  28. package/lib/src/services/s3.service.js +42 -594
  29. package/lib/src/services/storage.service.js +213 -0
  30. package/lib/src/utils/errors.js +56 -46
  31. package/package.json +11 -12
  32. package/views/bucket.hbs +47 -71
  33. package/views/bucketadmin_move_modal.hbs +35 -0
  34. package/views/bucketadmin_preview_modal.hbs +108 -40
  35. package/views/bucketadmincontent.hbs +193 -144
  36. package/views/mbkbucket_info.hbs +134 -232
  37. package/.mbkbucket.config.example.json +0 -14
  38. package/lib/src/cli/auth-helper.js +0 -141
@@ -29,33 +29,13 @@
29
29
  body {
30
30
  font-family: var(--font);
31
31
  font-size: 14px;
32
- line-height: 1.6;
32
+ line-height: 1.5;
33
33
  color: var(--text);
34
- background:
35
- radial-gradient(circle at 12% 14%, rgba(16, 185, 129, 0.1), transparent 44%),
36
- radial-gradient(circle at 88% 8%, rgba(15, 118, 110, 0.08), transparent 40%),
37
- linear-gradient(160deg, #eef7f2 0%, #f8fbf7 52%, #eaf2ef 100%);
34
+ background: var(--body-bg, #f8fafc);
38
35
  min-height: 100vh;
39
36
  overflow-x: hidden;
40
- }
41
-
42
- /* Ambient blobs */
43
- body::before, body::after {
44
- content: '';
45
- position: fixed;
46
- border-radius: 50%;
47
- pointer-events: none;
48
- z-index: 0;
49
- }
50
- body::before {
51
- width: 280px; height: 280px;
52
- top: 10vh; left: -80px;
53
- background: radial-gradient(circle, rgba(15,118,110,0.14) 0, transparent 70%);
54
- }
55
- body::after {
56
- width: 320px; height: 320px;
57
- bottom: 8vh; right: -100px;
58
- background: radial-gradient(circle, rgba(16,185,129,0.1) 0, transparent 70%);
37
+ -webkit-font-smoothing: antialiased;
38
+ -moz-osx-font-smoothing: grayscale;
59
39
  }
60
40
 
61
41
  /* ── Top nav ───────────────────────────────────────────────── */
@@ -13,55 +13,49 @@
13
13
  /* ─── Standalone design tokens (fallbacks via @property-like approach) ──────── */
14
14
  :root {
15
15
  /* Colours */
16
- --mbkh-primary: var(--primary-color-ml, #0f766e);
17
- --mbkh-primary-lt: var(--primary-light-color-ml, #d7f4ee);
18
- --mbkh-primary-dk: var(--primary-dark-color-ml, #0b4f4a);
19
- --mbkh-primary-hover: var(--primary-hover, #0d635c);
16
+ --mbkh-primary: var(--primary-color-ml, #0f766e);
17
+ --mbkh-primary-lt: var(--primary-light-color-ml, #f0fdfa);
18
+ --mbkh-primary-dk: var(--primary-dark-color-ml, #115e59);
19
+ --mbkh-primary-hover: var(--primary-hover, #134e4a);
20
20
 
21
- --mbkh-danger: var(--danger-color-ml, #dc2626);
22
- --mbkh-success: var(--success-color-ml, #16a34a);
23
- --mbkh-warning: var(--warning-color-ml, #d97706);
21
+ --mbkh-danger: var(--danger-color-ml, #ef4444);
22
+ --mbkh-success: var(--success-color-ml, #10b981);
23
+ --mbkh-warning: var(--warning-color-ml, #f59e0b);
24
24
 
25
25
  /* Backgrounds */
26
- --mbkh-body-bg: var(--body-bg, #f4f7f3);
27
- --mbkh-surface: var(--bg-surface, rgba(255,255,255,0.85));
28
- --mbkh-surface2: var(--bg-surface-secondary, rgba(244,247,243,0.85));
29
- --mbkh-surface3: var(--bg-surface-tertiary, #e2ede8);
26
+ --mbkh-body-bg: var(--body-bg, #f8fafc);
27
+ --mbkh-surface: var(--bg-surface, #ffffff);
28
+ --mbkh-surface2: var(--bg-surface-secondary, #f8fafc);
29
+ --mbkh-surface3: var(--bg-surface-tertiary, #f1f5f9);
30
30
 
31
31
  /* Borders */
32
- --mbkh-border: var(--border-color, rgba(15,118,110,0.16));
33
- --mbkh-border-lt: var(--border-color-light, rgba(15,118,110,0.08));
34
- --mbkh-border-active: rgba(15,118,110,0.45);
32
+ --mbkh-border: var(--border-color, #e2e8f0);
33
+ --mbkh-border-lt: var(--border-color-light, #f1f5f9);
34
+ --mbkh-border-active: rgba(15, 118, 110, 0.35);
35
35
 
36
36
  /* Text */
37
- --mbkh-text: var(--text, #1f2a2a);
38
- --mbkh-text-sec: var(--text-secondary, #425855);
39
- --mbkh-text-muted: var(--text-muted, #667c79);
40
- --mbkh-text-dim: var(--text-disabled, #a0b2af);
37
+ --mbkh-text: var(--text, #0f172a);
38
+ --mbkh-text-sec: var(--text-secondary, #475569);
39
+ --mbkh-text-muted: var(--text-muted, #64748b);
40
+ --mbkh-text-dim: var(--text-disabled, #94a3b8);
41
41
 
42
42
  /* Typography */
43
- --mbkh-font: var(--font, "IBM Plex Sans", "Segoe UI", sans-serif);
44
- --mbkh-font-heading: var(--font-headings, "Sora", "Segoe UI", sans-serif);
45
- --mbkh-font-mono: var(--font-mono, "JetBrains Mono", Consolas, monospace);
43
+ --mbkh-font: var(--font, "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif);
44
+ --mbkh-font-heading: var(--font-headings, "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif);
45
+ --mbkh-font-mono: var(--font-mono, "Segoe UI Mono", "Cascadia Code", Consolas, "Courier New", monospace);
46
46
 
47
47
  /* Shape */
48
- --mbkh-radius: var(--radius-lg, 14px);
48
+ --mbkh-radius: var(--radius-lg, 12px);
49
49
  --mbkh-radius-md: var(--radius, 8px);
50
50
  --mbkh-radius-sm: var(--radius-sm, 6px);
51
51
 
52
52
  /* Shadows */
53
- --mbkh-shadow: var(--shadow-lg,
54
- 0 20px 40px rgba(15,118,110,0.12),
55
- 0 4px 12px rgba(0,0,0,0.06));
56
- --mbkh-shadow-sm: var(--shadow-md,
57
- 0 8px 20px rgba(15,118,110,0.08),
58
- 0 2px 6px rgba(0,0,0,0.04));
59
- --mbkh-shadow-dropdown: var(--shadow-dropdown,
60
- 0 12px 30px rgba(15,42,42,0.14),
61
- 0 4px 10px rgba(0,0,0,0.05));
53
+ --mbkh-shadow: var(--shadow-dropdown, 0 10px 25px -5px rgba(15, 23, 42, 0.12), 0 8px 10px -6px rgba(15, 23, 42, 0.04));
54
+ --mbkh-shadow-sm: var(--shadow-sm, 0 1px 3px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.04));
55
+ --mbkh-shadow-dropdown: var(--shadow-dropdown, 0 10px 25px -5px rgba(15, 23, 42, 0.12), 0 8px 10px -6px rgba(15, 23, 42, 0.04));
62
56
 
63
57
  /* Transition */
64
- --mbkh-transition: 0.15s ease-in-out;
58
+ --mbkh-transition: 0.12s ease;
65
59
  }
66
60
 
67
61
  /* ─── Reset inside helper components ───────────────────────────────────────── */
@@ -82,14 +76,14 @@
82
76
  position: fixed;
83
77
  inset: 0;
84
78
  z-index: 99990;
85
- background: rgba(15, 42, 42, 0.45);
79
+ background: rgba(15, 23, 42, 0.5);
86
80
  backdrop-filter: blur(4px);
87
81
  -webkit-backdrop-filter: blur(4px);
88
82
  display: flex;
89
83
  align-items: center;
90
84
  justify-content: center;
91
85
  padding: 16px;
92
- animation: mbkh-fade-in 0.18s ease;
86
+ animation: mbkh-fade-in 0.15s ease;
93
87
  }
94
88
 
95
89
  @keyframes mbkh-fade-in { from { opacity: 0; } to { opacity: 1; } }
@@ -151,9 +151,10 @@
151
151
  close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
152
152
  search: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
153
153
  grid: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>`,
154
- list: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>`,
155
- upload: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0018 9h-1.26A8 8 0 103 16.3"/></svg>`,
156
- plus: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`,
154
+ list: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>`,
155
+ upload: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>`,
156
+ download: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
157
+ plus: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`,
157
158
  check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
158
159
  warn: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
159
160
  info: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
@@ -1,73 +1,86 @@
1
- const TRUE_VALUES = new Set(['true', 't', '1', 'yes', 'y', 'on']);
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'];
4
-
5
- export function normalizeBooleanLike(value) {
6
- if (typeof value === 'boolean') return value;
7
- if (typeof value === 'string') {
8
- const norm = value.trim().toLowerCase();
9
- if (TRUE_VALUES.has(norm)) return true;
10
- if (FALSE_VALUES.has(norm)) return false;
11
- }
12
- return value;
13
- }
14
-
15
- export function parseAndValidateMbkbucketVar(rawValue) {
16
- let parsed;
17
- try {
18
- parsed = rawValue ? JSON.parse(rawValue) : {};
19
- } catch {
20
- throw new Error('Invalid JSON in process.env.mbkbucketVar');
21
- }
22
-
23
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
24
- throw new Error('mbkbucketVar must be a valid object');
25
- }
26
-
27
- const defaults = { p_view_inline: true, publiView_enabled: false };
28
- for (const [key, defaultVal] of Object.entries(defaults)) {
29
- const val = parsed[key];
30
- if (val === undefined || (typeof val === 'string' && val.trim() === '')) {
31
- parsed[key] = defaultVal;
32
- }
33
- }
34
-
35
- parsed.p_view_inline = normalizeBooleanLike(parsed.p_view_inline);
36
- parsed.publiView_enabled = normalizeBooleanLike(parsed.publiView_enabled);
37
-
38
- const errors = [];
39
- if (typeof parsed.p_view_inline !== 'boolean') errors.push('mbkbucketVar.p_view_inline must be a boolean if provided');
40
- if (typeof parsed.publiView_enabled !== 'boolean') errors.push('mbkbucketVar.publiView_enabled must be a boolean if provided');
41
- if (errors.length) throw new Error(errors.join(' | '));
42
-
43
- return parsed;
44
- }
45
-
46
- export function parseAndValidateBucketConnection(rawValue) {
47
- if (!rawValue) return null;
48
-
49
- let parsed;
50
- try {
51
- parsed = JSON.parse(rawValue);
52
- } catch {
53
- throw new Error('BucketConnection is not valid JSON');
54
- }
55
-
56
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !Object.keys(parsed).length) {
57
- throw new Error('BucketConnection must be a non-empty object');
58
- }
59
-
60
- for (const [bucketName, config] of Object.entries(parsed)) {
61
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
62
- throw new Error(`Bucket '${bucketName}' configuration must be an object, not a string. Remove quotes around the inner JSON object.`);
63
- }
64
-
65
- const missingFields = REQUIRED_BUCKET_FIELDS.filter(field => !config[field]);
66
- if (missingFields.length) {
67
- throw new Error(`Bucket '${bucketName}' is missing required fields: ${missingFields.join(', ')}`);
68
- }
69
- }
70
-
71
- return parsed;
72
- }
73
-
1
+ const TRUE_VALUES = new Set(['true', 't', '1', 'yes', 'y', 'on']);
2
+ const FALSE_VALUES = new Set(['false', 'f', '0', 'no', 'n', 'off']);
3
+ const REQUIRED_S3_FIELDS = ['BUCKET_NAME', 'ACCESS_KEY_ID', 'SECRET_ACCESS_KEY', 'ENDPOINT'];
4
+
5
+ export function normalizeBooleanLike(value) {
6
+ if (typeof value === 'boolean') return value;
7
+ if (typeof value === 'string') {
8
+ const norm = value.trim().toLowerCase();
9
+ if (TRUE_VALUES.has(norm)) return true;
10
+ if (FALSE_VALUES.has(norm)) return false;
11
+ }
12
+ return value;
13
+ }
14
+
15
+ export function parseAndValidateMbkbucketVar(rawValue) {
16
+ let parsed;
17
+ try {
18
+ parsed = rawValue ? JSON.parse(rawValue) : {};
19
+ } catch {
20
+ throw new Error('Invalid JSON in process.env.mbkbucketVar');
21
+ }
22
+
23
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
24
+ throw new Error('mbkbucketVar must be a valid object');
25
+ }
26
+
27
+ const defaults = { p_view_inline: true, publiView_enabled: false };
28
+ for (const [key, defaultVal] of Object.entries(defaults)) {
29
+ const val = parsed[key];
30
+ if (val === undefined || (typeof val === 'string' && val.trim() === '')) {
31
+ parsed[key] = defaultVal;
32
+ }
33
+ }
34
+
35
+ parsed.p_view_inline = normalizeBooleanLike(parsed.p_view_inline);
36
+ parsed.publiView_enabled = normalizeBooleanLike(parsed.publiView_enabled);
37
+
38
+ const errors = [];
39
+ if (typeof parsed.p_view_inline !== 'boolean') errors.push('mbkbucketVar.p_view_inline must be a boolean if provided');
40
+ if (typeof parsed.publiView_enabled !== 'boolean') errors.push('mbkbucketVar.publiView_enabled must be a boolean if provided');
41
+ if (errors.length) throw new Error(errors.join(' | '));
42
+
43
+ return parsed;
44
+ }
45
+
46
+ export function parseAndValidateBucketConnection(rawValue) {
47
+ if (!rawValue) return null;
48
+
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(rawValue);
52
+ } catch {
53
+ throw new Error('BucketConnection is not valid JSON');
54
+ }
55
+
56
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !Object.keys(parsed).length) {
57
+ throw new Error('BucketConnection must be a non-empty object');
58
+ }
59
+
60
+ for (const [bucketName, config] of Object.entries(parsed)) {
61
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
62
+ throw new Error(`Bucket '${bucketName}' configuration must be an object, not a string. Remove quotes around the inner JSON object.`);
63
+ }
64
+
65
+ const providerType = String(config.type || config.provider || '').toLowerCase();
66
+
67
+ if (providerType === 'gdrive' || providerType === 'google-drive' || providerType === 'googledrive') {
68
+ const hasOAuth = Boolean(config.client_id && config.client_secret && (config.refresh_token || config.access_token || config.code));
69
+ const hasServiceAccount = Boolean((config.client_email || config.service_account_email) && (config.private_key || config.service_account_key));
70
+ const hasAccessTokenOnly = Boolean(config.access_token);
71
+ const hasServiceAccountKeyObj = Boolean(config.service_account && typeof config.service_account === 'object');
72
+
73
+ if (!hasOAuth && !hasServiceAccount && !hasAccessTokenOnly && !hasServiceAccountKeyObj) {
74
+ throw new Error(`Google Drive connection '${bucketName}' requires OAuth credentials (client_id, client_secret, refresh_token) or Service Account credentials (client_email, private_key).`);
75
+ }
76
+ } else {
77
+ // Default is S3-compatible provider
78
+ const missingFields = REQUIRED_S3_FIELDS.filter(field => !config[field]);
79
+ if (missingFields.length) {
80
+ throw new Error(`Bucket '${bucketName}' is missing required fields: ${missingFields.join(', ')}`);
81
+ }
82
+ }
83
+ }
84
+
85
+ return parsed;
86
+ }