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
@@ -0,0 +1,62 @@
1
+ /**
2
+ * MBKBucket - Typed Storage Error Hierarchy
3
+ * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ export class StorageError extends Error {
8
+ /**
9
+ * @param {string} message
10
+ * @param {number} [status=500]
11
+ * @param {string} [code='INTERNAL_STORAGE_ERROR']
12
+ */
13
+ constructor(message, status = 500, code = 'INTERNAL_STORAGE_ERROR') {
14
+ super(message);
15
+ this.name = this.constructor.name;
16
+ this.status = status;
17
+ this.code = code;
18
+ Error.captureStackTrace?.(this, this.constructor);
19
+ }
20
+ }
21
+
22
+ export class StorageNotFoundError extends StorageError {
23
+ constructor(message = 'File or folder not found') {
24
+ super(message, 404, 'NOT_FOUND');
25
+ }
26
+ }
27
+
28
+ export class StorageAccessDeniedError extends StorageError {
29
+ constructor(message = 'Access denied') {
30
+ super(message, 403, 'ACCESS_DENIED');
31
+ }
32
+ }
33
+
34
+ export class StorageConflictError extends StorageError {
35
+ constructor(message = 'Resource already exists') {
36
+ super(message, 409, 'CONFLICT');
37
+ }
38
+ }
39
+
40
+ export class StorageValidationError extends StorageError {
41
+ constructor(message = 'Validation failed') {
42
+ super(message, 400, 'VALIDATION_ERROR');
43
+ }
44
+ }
45
+
46
+ export class StorageConfigError extends StorageError {
47
+ constructor(message = 'Storage configuration error') {
48
+ super(message, 503, 'STORAGE_CONFIG_ERROR');
49
+ }
50
+ }
51
+
52
+ export class StorageQuotaExceededError extends StorageError {
53
+ constructor(message = 'Storage quota exceeded') {
54
+ super(message, 507, 'QUOTA_EXCEEDED');
55
+ }
56
+ }
57
+
58
+ export class StorageUnsupportedOperationError extends StorageError {
59
+ constructor(message = 'Operation not supported by this storage provider') {
60
+ super(message, 501, 'NOT_IMPLEMENTED');
61
+ }
62
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * MBKBucket - Google Drive Authentication & OAuth Helpers
3
+ * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import crypto from "node:crypto";
8
+ import { createLogger } from "#logger";
9
+
10
+ const debugAuth = createLogger('gdrive-auth');
11
+
12
+ export const GOOGLE_DRIVE_SCOPES = [
13
+ 'https://www.googleapis.com/auth/drive',
14
+ 'https://www.googleapis.com/auth/drive.file',
15
+ 'https://www.googleapis.com/auth/drive.metadata.readonly'
16
+ ];
17
+
18
+ /**
19
+ * Generate Google OAuth 2.0 Authorization URL for user consent.
20
+ * @param {object} options
21
+ * @param {string} options.clientId
22
+ * @param {string} options.redirectUri
23
+ * @param {string} [options.state]
24
+ * @param {string[]} [options.scopes]
25
+ * @param {'consent'|'select_account'|'none'} [options.prompt='consent']
26
+ * @param {'offline'|'online'} [options.accessType='offline']
27
+ * @returns {string}
28
+ */
29
+ export function getGoogleAuthUrl({
30
+ clientId,
31
+ redirectUri,
32
+ state = '',
33
+ scopes = GOOGLE_DRIVE_SCOPES,
34
+ prompt = 'consent',
35
+ accessType = 'offline'
36
+ }) {
37
+ if (!clientId) throw new Error('clientId is required for Google OAuth');
38
+ if (!redirectUri) throw new Error('redirectUri is required for Google OAuth');
39
+
40
+ const params = new URLSearchParams({
41
+ client_id: clientId,
42
+ redirect_uri: redirectUri,
43
+ response_type: 'code',
44
+ scope: Array.isArray(scopes) ? scopes.join(' ') : String(scopes),
45
+ access_type: accessType,
46
+ prompt: prompt,
47
+ include_granted_scopes: 'true',
48
+ ...(state && { state })
49
+ });
50
+
51
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
52
+ }
53
+
54
+ /**
55
+ * Exchange an OAuth authorization code for tokens (access_token, refresh_token).
56
+ * @param {object} options
57
+ * @param {string} options.code
58
+ * @param {string} options.clientId
59
+ * @param {string} options.clientSecret
60
+ * @param {string} options.redirectUri
61
+ * @returns {Promise<{ access_token: string, refresh_token?: string, expires_in: number, token_type: string, scope: string, expiry_date: number }>}
62
+ */
63
+ export async function exchangeCodeForTokens({
64
+ code,
65
+ clientId,
66
+ clientSecret,
67
+ redirectUri
68
+ }) {
69
+ if (!code) throw new Error('Authorization code is required');
70
+ if (!clientId || !clientSecret) throw new Error('clientId and clientSecret are required');
71
+
72
+ debugAuth('Exchanging auth code for tokens with clientId=%s', clientId);
73
+
74
+ const body = new URLSearchParams({
75
+ code,
76
+ client_id: clientId,
77
+ client_secret: clientSecret,
78
+ redirect_uri: redirectUri || 'postmessage',
79
+ grant_type: 'authorization_code'
80
+ });
81
+
82
+ const res = await fetch('https://oauth2.googleapis.com/token', {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
85
+ body: body.toString()
86
+ });
87
+
88
+ const data = await res.json();
89
+ if (!res.ok || data.error) {
90
+ throw new Error(`Failed to exchange Google OAuth code: ${data.error_description || data.error || res.statusText}`);
91
+ }
92
+
93
+ const expiry_date = Date.now() + (Number(data.expires_in) || 3600) * 1000;
94
+ return { ...data, expiry_date };
95
+ }
96
+
97
+ /**
98
+ * Refresh an expired access token using a refresh token.
99
+ * @param {object} options
100
+ * @param {string} options.clientId
101
+ * @param {string} options.clientSecret
102
+ * @param {string} options.refreshToken
103
+ * @returns {Promise<{ access_token: string, expires_in: number, expiry_date: number, token_type: string }>}
104
+ */
105
+ export async function refreshGoogleAccessToken({
106
+ clientId,
107
+ clientSecret,
108
+ refreshToken
109
+ }) {
110
+ if (!refreshToken) throw new Error('refreshToken is required');
111
+ if (!clientId || !clientSecret) throw new Error('clientId and clientSecret are required');
112
+
113
+ debugAuth('Refreshing access token for clientId=%s', clientId);
114
+
115
+ const body = new URLSearchParams({
116
+ client_id: clientId,
117
+ client_secret: clientSecret,
118
+ refresh_token: refreshToken,
119
+ grant_type: 'refresh_token'
120
+ });
121
+
122
+ const res = await fetch('https://oauth2.googleapis.com/token', {
123
+ method: 'POST',
124
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
125
+ body: body.toString()
126
+ });
127
+
128
+ const data = await res.json();
129
+ if (!res.ok || data.error) {
130
+ throw new Error(`Failed to refresh Google access token: ${data.error_description || data.error || res.statusText}`);
131
+ }
132
+
133
+ const expiry_date = Date.now() + (Number(data.expires_in) || 3600) * 1000;
134
+ return { ...data, expiry_date };
135
+ }
136
+
137
+ /**
138
+ * Generate an OAuth2 access token from a Google Service Account using JWT signing.
139
+ * @param {object} options
140
+ * @param {string} options.clientEmail
141
+ * @param {string} options.privateKey
142
+ * @param {string[]} [options.scopes]
143
+ * @returns {Promise<{ access_token: string, expires_in: number, expiry_date: number }>}
144
+ */
145
+ export async function getServiceAccountAccessToken({
146
+ clientEmail,
147
+ privateKey,
148
+ scopes = GOOGLE_DRIVE_SCOPES
149
+ }) {
150
+ if (!clientEmail || !privateKey) {
151
+ throw new Error('clientEmail and privateKey are required for Service Account authentication');
152
+ }
153
+
154
+ const now = Math.floor(Date.now() / 1000);
155
+ const header = { alg: 'RS256', typ: 'JWT' };
156
+ const claim = {
157
+ iss: clientEmail,
158
+ scope: Array.isArray(scopes) ? scopes.join(' ') : String(scopes),
159
+ aud: 'https://oauth2.googleapis.com/token',
160
+ exp: now + 3600,
161
+ iat: now
162
+ };
163
+
164
+ const base64Url = str => Buffer.from(str).toString('base64url');
165
+ const encodedHeader = base64Url(JSON.stringify(header));
166
+ const encodedClaim = base64Url(JSON.stringify(claim));
167
+ const signatureInput = `${encodedHeader}.${encodedClaim}`;
168
+
169
+ const signer = crypto.createSign('RSA-SHA256');
170
+ signer.update(signatureInput);
171
+ signer.end();
172
+ const signature = signer.sign(privateKey, 'base64url');
173
+ const jwt = `${signatureInput}.${signature}`;
174
+
175
+ const body = new URLSearchParams({
176
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
177
+ assertion: jwt
178
+ });
179
+
180
+ const res = await fetch('https://oauth2.googleapis.com/token', {
181
+ method: 'POST',
182
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
183
+ body: body.toString()
184
+ });
185
+
186
+ const data = await res.json();
187
+ if (!res.ok || data.error) {
188
+ throw new Error(`Service Account token request failed: ${data.error_description || data.error || res.statusText}`);
189
+ }
190
+
191
+ const expiry_date = Date.now() + (Number(data.expires_in) || 3600) * 1000;
192
+ return { access_token: data.access_token, expires_in: data.expires_in, expiry_date };
193
+ }