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.
- package/.env.example +15 -14
- package/README.md +561 -377
- package/index.js +32 -30
- package/lib/cli.js +154 -565
- package/lib/public/bucketadmin.css +2042 -1708
- package/lib/public/bucketadmin.js +1622 -1286
- package/lib/public/mbkbucket-helper-demo.html +4 -24
- package/lib/public/mbkbucket-helper.css +28 -34
- package/lib/public/mbkbucket-helper.js +4 -3
- package/lib/src/config/validation.js +86 -73
- package/lib/src/controllers/api.controller.js +459 -410
- package/lib/src/controllers/dashboard.controller.js +69 -49
- package/lib/src/controllers/info.controller.js +34 -3
- package/lib/src/controllers/view.controller.js +233 -231
- package/lib/src/core/storage/errors.js +62 -0
- package/lib/src/core/storage/gdrive-auth.js +193 -0
- package/lib/src/core/storage/gdrive-provider.js +656 -0
- package/lib/src/core/storage/index.js +14 -0
- package/lib/src/core/storage/models.js +80 -0
- package/lib/src/core/storage/path-strategy.js +65 -0
- package/lib/src/core/storage/s3-provider.js +491 -0
- package/lib/src/core/storage/storage-manager.js +203 -0
- package/lib/src/core/storage/storage-provider.js +217 -0
- package/lib/src/middleware/bucket-resolver.js +37 -37
- package/lib/src/routes/api.routes.js +5 -1
- package/lib/src/routes/index.js +60 -60
- package/lib/src/routes/view.routes.js +28 -25
- package/lib/src/services/s3.service.js +42 -594
- package/lib/src/services/storage.service.js +213 -0
- package/lib/src/utils/errors.js +56 -46
- package/package.json +11 -12
- package/views/bucket.hbs +47 -71
- package/views/bucketadmin_move_modal.hbs +35 -0
- package/views/bucketadmin_preview_modal.hbs +108 -40
- package/views/bucketadmincontent.hbs +193 -144
- package/views/mbkbucket_info.hbs +134 -232
- package/.mbkbucket.config.example.json +0 -14
- 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
|
+
}
|