@progalaxyelabs/stonescriptphp-files 2.0.1 → 3.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/package.json +1 -1
- package/src/authorization.js +124 -0
- package/src/azure-storage.js +74 -20
- package/src/index.js +22 -5
- package/src/routes/delete.js +10 -2
- package/src/routes/download.js +10 -2
- package/src/routes/list.js +7 -0
- package/src/routes/upload.js +9 -1
package/package.json
CHANGED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional authorization middleware for stonescriptphp-files.
|
|
3
|
+
*
|
|
4
|
+
* When AUTHORIZATION_URL is configured, calls the platform API to check
|
|
5
|
+
* if the current user is allowed to perform the file operation.
|
|
6
|
+
* The API acts as an authorization oracle — it knows about business entities
|
|
7
|
+
* and role hierarchies. The files service stays generic.
|
|
8
|
+
*
|
|
9
|
+
* When AUTHORIZATION_URL is NOT set, this is a no-op pass-through.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create authorization middleware
|
|
14
|
+
* @param {string|null} authorizationUrl - URL to call for auth checks (e.g., "http://api:9100/files/authorize")
|
|
15
|
+
* @param {number} timeout - Request timeout in ms (default: 3000)
|
|
16
|
+
* @returns {Function} Express middleware
|
|
17
|
+
*/
|
|
18
|
+
export function createAuthorizationMiddleware(authorizationUrl, timeout = 3000) {
|
|
19
|
+
if (!authorizationUrl) {
|
|
20
|
+
// No authorization configured — pass through (backwards compat)
|
|
21
|
+
return (req, res, next) => {
|
|
22
|
+
req.fileScope = 'user';
|
|
23
|
+
next();
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`Authorization middleware enabled: ${authorizationUrl}`);
|
|
28
|
+
|
|
29
|
+
return async (req, res, next) => {
|
|
30
|
+
try {
|
|
31
|
+
// Determine action from HTTP method + path
|
|
32
|
+
let action;
|
|
33
|
+
if (req.method === 'POST' && req.path === '/upload') {
|
|
34
|
+
action = 'upload';
|
|
35
|
+
} else if (req.method === 'GET' && req.path.startsWith('/files/')) {
|
|
36
|
+
action = 'download';
|
|
37
|
+
} else if (req.method === 'DELETE' && req.path.startsWith('/files/')) {
|
|
38
|
+
action = 'delete';
|
|
39
|
+
} else {
|
|
40
|
+
// Not a file operation route (e.g., /health, /list) — pass through
|
|
41
|
+
req.fileScope = 'user';
|
|
42
|
+
return next();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Build authorization request body
|
|
46
|
+
const body = { action };
|
|
47
|
+
|
|
48
|
+
if ((action === 'download' || action === 'delete') && req.params.id) {
|
|
49
|
+
body.file_id = req.params.id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (action === 'upload') {
|
|
53
|
+
body.resource_type = req.body?.resource_type || null;
|
|
54
|
+
body.resource_id = req.body?.resource_id ? parseInt(req.body.resource_id) : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Forward the user's JWT token
|
|
58
|
+
const authHeader = req.headers.authorization;
|
|
59
|
+
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
62
|
+
|
|
63
|
+
const response = await fetch(authorizationUrl, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
...(authHeader ? { 'Authorization': authHeader } : {})
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
signal: controller.signal
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
clearTimeout(timeoutId);
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const errorRaw = await response.json().catch(() => ({}));
|
|
77
|
+
const errorBody = errorRaw.data || errorRaw;
|
|
78
|
+
|
|
79
|
+
if (response.status === 403) {
|
|
80
|
+
console.warn(`Authorization denied: user=${req.user?.id} action=${action} reason=${errorBody.reason || errorRaw.message || 'unknown'}`);
|
|
81
|
+
return res.status(403).json({
|
|
82
|
+
error: 'Forbidden',
|
|
83
|
+
message: errorBody.reason || errorRaw.message || 'Access denied'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.error(`Authorization service error: status=${response.status} body=${JSON.stringify(errorBody)}`);
|
|
88
|
+
return res.status(503).json({
|
|
89
|
+
error: 'Service Unavailable',
|
|
90
|
+
message: 'Authorization service returned an error'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const raw = await response.json();
|
|
95
|
+
// Unwrap StoneScriptPHP {status, message, data} wrapper if present
|
|
96
|
+
const authResult = raw.data || raw;
|
|
97
|
+
|
|
98
|
+
if (!authResult.allowed) {
|
|
99
|
+
console.warn(`Authorization denied: user=${req.user?.id} action=${action} reason=${authResult.reason || 'unknown'}`);
|
|
100
|
+
return res.status(403).json({
|
|
101
|
+
error: 'Forbidden',
|
|
102
|
+
message: authResult.reason || 'Access denied'
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Set scope on request for route handlers
|
|
107
|
+
req.fileScope = authResult.scope || 'user';
|
|
108
|
+
next();
|
|
109
|
+
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error.name === 'AbortError') {
|
|
112
|
+
console.error(`Authorization timeout: url=${authorizationUrl} timeout=${timeout}ms`);
|
|
113
|
+
} else {
|
|
114
|
+
console.error(`Authorization error: ${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fail closed — deny access when authorization service is unavailable
|
|
118
|
+
return res.status(503).json({
|
|
119
|
+
error: 'Service Unavailable',
|
|
120
|
+
message: 'Authorization service unavailable'
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
package/src/azure-storage.js
CHANGED
|
@@ -7,20 +7,22 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
7
7
|
*/
|
|
8
8
|
export class AzureStorageClient {
|
|
9
9
|
constructor(connectionString, containerName = 'platform-files') {
|
|
10
|
-
if (!connectionString) {
|
|
11
|
-
throw new Error('Azure Storage connection string is required');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
10
|
this.connectionString = connectionString;
|
|
15
11
|
this.containerName = containerName;
|
|
16
12
|
this.blobServiceClient = null;
|
|
17
13
|
this.containerClient = null;
|
|
14
|
+
this.isConfigured = !!connectionString;
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
/**
|
|
21
18
|
* Initialize Azure Blob Storage client
|
|
22
19
|
*/
|
|
23
20
|
async initialize() {
|
|
21
|
+
if (!this.isConfigured) {
|
|
22
|
+
console.warn('⚠️ AZURE_STORAGE_CONNECTION_STRING not set — file uploads will fail');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
24
26
|
try {
|
|
25
27
|
// Create BlobServiceClient
|
|
26
28
|
this.blobServiceClient = BlobServiceClient.fromConnectionString(this.connectionString);
|
|
@@ -45,17 +47,29 @@ export class AzureStorageClient {
|
|
|
45
47
|
* @param {Buffer} fileBuffer - File content
|
|
46
48
|
* @param {string} originalFilename - Original filename
|
|
47
49
|
* @param {string} contentType - MIME type
|
|
50
|
+
* @param {string} scope - Storage scope: 'user' (default) or 'tenant'
|
|
48
51
|
* @returns {Object} File metadata
|
|
49
52
|
*/
|
|
50
|
-
async uploadFile(tenantId, userId, fileBuffer, originalFilename, contentType) {
|
|
53
|
+
async uploadFile(tenantId, userId, fileBuffer, originalFilename, contentType, scope = 'user') {
|
|
54
|
+
if (!this.isConfigured) {
|
|
55
|
+
const error = new Error('Storage not configured');
|
|
56
|
+
error.statusCode = 503;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
if (!this.containerClient) {
|
|
52
61
|
throw new Error('Azure Storage not initialized');
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
// Generate unique blob name with
|
|
64
|
+
// Generate unique blob name with scope-based prefix
|
|
56
65
|
const fileId = uuidv4();
|
|
57
66
|
const extension = originalFilename.split('.').pop();
|
|
58
|
-
|
|
67
|
+
let prefix;
|
|
68
|
+
if (scope === 'tenant') {
|
|
69
|
+
prefix = tenantId ? `${tenantId}/shared/` : 'shared/';
|
|
70
|
+
} else {
|
|
71
|
+
prefix = tenantId ? `${tenantId}/${userId}/` : `${userId}/`;
|
|
72
|
+
}
|
|
59
73
|
const blobName = `${prefix}${fileId}.${extension}`;
|
|
60
74
|
|
|
61
75
|
const blockBlobClient = this.containerClient.getBlockBlobClient(blobName);
|
|
@@ -71,7 +85,8 @@ export class AzureStorageClient {
|
|
|
71
85
|
original_filename: originalFilename,
|
|
72
86
|
content_type: contentType,
|
|
73
87
|
uploaded_at: new Date().toISOString(),
|
|
74
|
-
file_id: fileId
|
|
88
|
+
file_id: fileId,
|
|
89
|
+
scope: scope
|
|
75
90
|
}
|
|
76
91
|
});
|
|
77
92
|
|
|
@@ -92,15 +107,28 @@ export class AzureStorageClient {
|
|
|
92
107
|
* @param {string} fileId - File ID (UUID)
|
|
93
108
|
* @param {string} tenantId - Tenant ID (for multi-tenant namespacing)
|
|
94
109
|
* @param {string} userId - User ID (for authorization)
|
|
110
|
+
* @param {string} scope - Storage scope: 'user' (default) or 'tenant'
|
|
95
111
|
* @returns {Object} { stream, metadata }
|
|
96
112
|
*/
|
|
97
|
-
async downloadFile(fileId, tenantId, userId) {
|
|
113
|
+
async downloadFile(fileId, tenantId, userId, scope = 'user') {
|
|
114
|
+
if (!this.isConfigured) {
|
|
115
|
+
const error = new Error('Storage not configured');
|
|
116
|
+
error.statusCode = 503;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
98
120
|
if (!this.containerClient) {
|
|
99
121
|
throw new Error('Azure Storage not initialized');
|
|
100
122
|
}
|
|
101
123
|
|
|
102
|
-
//
|
|
103
|
-
|
|
124
|
+
// Build prefix based on scope
|
|
125
|
+
let prefix;
|
|
126
|
+
if (scope === 'tenant') {
|
|
127
|
+
prefix = tenantId ? `${tenantId}/shared/` : 'shared/';
|
|
128
|
+
} else {
|
|
129
|
+
prefix = tenantId ? `${tenantId}/${userId}/` : `${userId}/`;
|
|
130
|
+
}
|
|
131
|
+
|
|
104
132
|
const blobs = this.containerClient.listBlobsFlat({
|
|
105
133
|
prefix,
|
|
106
134
|
includeMetadata: true
|
|
@@ -110,8 +138,9 @@ export class AzureStorageClient {
|
|
|
110
138
|
if (blob.metadata && blob.metadata.file_id === fileId) {
|
|
111
139
|
const blockBlobClient = this.containerClient.getBlockBlobClient(blob.name);
|
|
112
140
|
|
|
113
|
-
// Verify ownership
|
|
114
|
-
|
|
141
|
+
// Verify ownership for user-scoped files only
|
|
142
|
+
// Tenant-scoped files are shared — authorization middleware already checked access
|
|
143
|
+
if (scope === 'user' && blob.metadata.user_id !== userId) {
|
|
115
144
|
throw new Error('Unauthorized: File belongs to different user');
|
|
116
145
|
}
|
|
117
146
|
|
|
@@ -136,15 +165,27 @@ export class AzureStorageClient {
|
|
|
136
165
|
* List user's files
|
|
137
166
|
* @param {string} tenantId - Tenant ID (for multi-tenant namespacing)
|
|
138
167
|
* @param {string} userId - User ID
|
|
168
|
+
* @param {string} scope - Storage scope: 'user' (default) or 'tenant'
|
|
139
169
|
* @returns {Array} List of file metadata
|
|
140
170
|
*/
|
|
141
|
-
async listFiles(tenantId, userId) {
|
|
171
|
+
async listFiles(tenantId, userId, scope = 'user') {
|
|
172
|
+
if (!this.isConfigured) {
|
|
173
|
+
const error = new Error('Storage not configured');
|
|
174
|
+
error.statusCode = 503;
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
|
|
142
178
|
if (!this.containerClient) {
|
|
143
179
|
throw new Error('Azure Storage not initialized');
|
|
144
180
|
}
|
|
145
181
|
|
|
146
182
|
const files = [];
|
|
147
|
-
|
|
183
|
+
let prefix;
|
|
184
|
+
if (scope === 'tenant') {
|
|
185
|
+
prefix = tenantId ? `${tenantId}/shared/` : 'shared/';
|
|
186
|
+
} else {
|
|
187
|
+
prefix = tenantId ? `${tenantId}/${userId}/` : `${userId}/`;
|
|
188
|
+
}
|
|
148
189
|
const blobs = this.containerClient.listBlobsFlat({
|
|
149
190
|
prefix,
|
|
150
191
|
includeMetadata: true
|
|
@@ -170,14 +211,27 @@ export class AzureStorageClient {
|
|
|
170
211
|
* @param {string} fileId - File ID (UUID)
|
|
171
212
|
* @param {string} tenantId - Tenant ID (for multi-tenant namespacing)
|
|
172
213
|
* @param {string} userId - User ID (for authorization)
|
|
214
|
+
* @param {string} scope - Storage scope: 'user' (default) or 'tenant'
|
|
173
215
|
*/
|
|
174
|
-
async deleteFile(fileId, tenantId, userId) {
|
|
216
|
+
async deleteFile(fileId, tenantId, userId, scope = 'user') {
|
|
217
|
+
if (!this.isConfigured) {
|
|
218
|
+
const error = new Error('Storage not configured');
|
|
219
|
+
error.statusCode = 503;
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
|
|
175
223
|
if (!this.containerClient) {
|
|
176
224
|
throw new Error('Azure Storage not initialized');
|
|
177
225
|
}
|
|
178
226
|
|
|
179
|
-
// Find blob by file_id
|
|
180
|
-
|
|
227
|
+
// Find blob by file_id with scope-based prefix
|
|
228
|
+
let prefix;
|
|
229
|
+
if (scope === 'tenant') {
|
|
230
|
+
prefix = tenantId ? `${tenantId}/shared/` : 'shared/';
|
|
231
|
+
} else {
|
|
232
|
+
prefix = tenantId ? `${tenantId}/${userId}/` : `${userId}/`;
|
|
233
|
+
}
|
|
234
|
+
|
|
181
235
|
const blobs = this.containerClient.listBlobsFlat({
|
|
182
236
|
prefix,
|
|
183
237
|
includeMetadata: true
|
|
@@ -185,8 +239,8 @@ export class AzureStorageClient {
|
|
|
185
239
|
|
|
186
240
|
for await (const blob of blobs) {
|
|
187
241
|
if (blob.metadata && blob.metadata.file_id === fileId) {
|
|
188
|
-
// Verify ownership
|
|
189
|
-
if (blob.metadata.user_id !== userId) {
|
|
242
|
+
// Verify ownership for user-scoped files only
|
|
243
|
+
if (scope === 'user' && blob.metadata.user_id !== userId) {
|
|
190
244
|
throw new Error('Unauthorized: File belongs to different user');
|
|
191
245
|
}
|
|
192
246
|
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import cors from 'cors';
|
|
|
3
3
|
import { AzureStorageClient } from './azure-storage.js';
|
|
4
4
|
import { createAuthMiddleware, createJwksAuthMiddleware } from './auth.js';
|
|
5
5
|
import { JwksClient } from './jwks-client.js';
|
|
6
|
+
import { createAuthorizationMiddleware } from './authorization.js';
|
|
6
7
|
import { createRateLimiters } from './rate-limit.js';
|
|
7
8
|
import { createUploadRouter } from './routes/upload.js';
|
|
8
9
|
import { createDownloadRouter } from './routes/download.js';
|
|
@@ -13,6 +14,7 @@ import { createHealthRouter } from './routes/health.js';
|
|
|
13
14
|
/**
|
|
14
15
|
* Resolve the authentication middleware based on configuration priority.
|
|
15
16
|
* Priority: authServers > jwksUrl > jwtPublicKey
|
|
17
|
+
* If no auth is configured, returns a pass-through middleware that logs a warning.
|
|
16
18
|
*
|
|
17
19
|
* @param {Object} config - Configuration options
|
|
18
20
|
* @returns {Function} Express middleware function for authentication
|
|
@@ -37,7 +39,14 @@ function resolveAuthMiddleware(config) {
|
|
|
37
39
|
return createAuthMiddleware(jwtPublicKey);
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
// No auth configured - return pass-through middleware with warning
|
|
43
|
+
console.warn('⚠️ No authentication configured — all endpoints will fail with 503');
|
|
44
|
+
return (req, res, next) => {
|
|
45
|
+
res.status(503).json({
|
|
46
|
+
error: 'Service Unavailable',
|
|
47
|
+
message: 'Authentication not configured'
|
|
48
|
+
});
|
|
49
|
+
};
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
/**
|
|
@@ -51,6 +60,8 @@ function resolveAuthMiddleware(config) {
|
|
|
51
60
|
* @param {string} config.jwksUrl - Single JWKS URL for key retrieval
|
|
52
61
|
* @param {number} config.jwksCacheTtl - JWKS cache TTL in seconds (default: 3600)
|
|
53
62
|
* @param {boolean} config.tenantScoped - Enable tenant-scoped file isolation (default: true)
|
|
63
|
+
* @param {string} config.authorizationUrl - URL for authorization checks (default: process.env.AUTHORIZATION_URL). When set, files service calls this URL before upload/download/delete.
|
|
64
|
+
* @param {number} config.authorizationTimeout - Authorization request timeout in ms (default: 3000)
|
|
54
65
|
* @param {number} config.maxFileSize - Max file size in bytes (default: 100MB)
|
|
55
66
|
* @param {string|string[]} config.corsOrigins - CORS allowed origins (default: '*')
|
|
56
67
|
* @param {number} config.rateLimitWindowMs - Rate limit window in ms (default: 60000)
|
|
@@ -72,6 +83,11 @@ export function createFilesServer(config = {}) {
|
|
|
72
83
|
// Resolve auth middleware based on config priority
|
|
73
84
|
const authenticate = resolveAuthMiddleware(config);
|
|
74
85
|
|
|
86
|
+
// Resolve authorization middleware (optional — no-op when URL not set)
|
|
87
|
+
const authorizationUrl = config.authorizationUrl || process.env.AUTHORIZATION_URL || null;
|
|
88
|
+
const authorizationTimeout = config.authorizationTimeout || parseInt(process.env.AUTHORIZATION_TIMEOUT) || 3000;
|
|
89
|
+
const authorize = createAuthorizationMiddleware(authorizationUrl, authorizationTimeout);
|
|
90
|
+
|
|
75
91
|
// Create rate limiters
|
|
76
92
|
const { uploadLimiter, downloadLimiter } = createRateLimiters(config);
|
|
77
93
|
|
|
@@ -91,12 +107,12 @@ export function createFilesServer(config = {}) {
|
|
|
91
107
|
next();
|
|
92
108
|
});
|
|
93
109
|
|
|
94
|
-
// Routes with rate limiters
|
|
110
|
+
// Routes with rate limiters and optional authorization
|
|
95
111
|
app.use(createHealthRouter());
|
|
96
|
-
app.use(authenticate, uploadLimiter, createUploadRouter(storage, maxFileSize));
|
|
97
|
-
app.use(authenticate, downloadLimiter, createDownloadRouter(storage));
|
|
112
|
+
app.use(authenticate, authorize, uploadLimiter, createUploadRouter(storage, maxFileSize));
|
|
113
|
+
app.use(authenticate, authorize, downloadLimiter, createDownloadRouter(storage));
|
|
98
114
|
app.use(authenticate, downloadLimiter, createListRouter(storage));
|
|
99
|
-
app.use(authenticate, downloadLimiter, createDeleteRouter(storage));
|
|
115
|
+
app.use(authenticate, authorize, downloadLimiter, createDeleteRouter(storage));
|
|
100
116
|
|
|
101
117
|
// 404 handler
|
|
102
118
|
app.use((req, res) => {
|
|
@@ -149,6 +165,7 @@ export function createFilesServer(config = {}) {
|
|
|
149
165
|
// Named exports for advanced/composable usage
|
|
150
166
|
export { AzureStorageClient } from './azure-storage.js';
|
|
151
167
|
export { createAuthMiddleware, createJwksAuthMiddleware } from './auth.js';
|
|
168
|
+
export { createAuthorizationMiddleware } from './authorization.js';
|
|
152
169
|
export { JwksClient } from './jwks-client.js';
|
|
153
170
|
export { createRateLimiters } from './rate-limit.js';
|
|
154
171
|
export { createUploadRouter } from './routes/upload.js';
|
package/src/routes/delete.js
CHANGED
|
@@ -19,6 +19,7 @@ export function createDeleteRouter(storage) {
|
|
|
19
19
|
const fileId = req.params.id;
|
|
20
20
|
const userId = req.user.id;
|
|
21
21
|
const tenantId = req.user.tenantId;
|
|
22
|
+
const scope = req.fileScope || 'user';
|
|
22
23
|
|
|
23
24
|
// Validate file ID format (UUID)
|
|
24
25
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -29,8 +30,8 @@ export function createDeleteRouter(storage) {
|
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
// Delete file from Azure (validates ownership)
|
|
33
|
-
await storage.deleteFile(fileId, tenantId, userId);
|
|
33
|
+
// Delete file from Azure (validates ownership for user-scoped files)
|
|
34
|
+
await storage.deleteFile(fileId, tenantId, userId, scope);
|
|
34
35
|
|
|
35
36
|
res.status(200).json({
|
|
36
37
|
success: true,
|
|
@@ -53,6 +54,13 @@ export function createDeleteRouter(storage) {
|
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
if (error.statusCode === 503) {
|
|
58
|
+
return res.status(503).json({
|
|
59
|
+
error: 'Service Unavailable',
|
|
60
|
+
message: error.message
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
res.status(500).json({
|
|
57
65
|
error: 'Internal Server Error',
|
|
58
66
|
message: 'Failed to delete file'
|
package/src/routes/download.js
CHANGED
|
@@ -19,6 +19,7 @@ export function createDownloadRouter(storage) {
|
|
|
19
19
|
const fileId = req.params.id;
|
|
20
20
|
const userId = req.user.id;
|
|
21
21
|
const tenantId = req.user.tenantId;
|
|
22
|
+
const scope = req.fileScope || 'user';
|
|
22
23
|
|
|
23
24
|
// Validate file ID format (UUID)
|
|
24
25
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -29,8 +30,8 @@ export function createDownloadRouter(storage) {
|
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
// Download file from Azure (validates ownership)
|
|
33
|
-
const { stream, metadata } = await storage.downloadFile(fileId, tenantId, userId);
|
|
33
|
+
// Download file from Azure (validates ownership for user-scoped files)
|
|
34
|
+
const { stream, metadata } = await storage.downloadFile(fileId, tenantId, userId, scope);
|
|
34
35
|
|
|
35
36
|
// Set response headers
|
|
36
37
|
res.setHeader('Content-Type', metadata.contentType);
|
|
@@ -67,6 +68,13 @@ export function createDownloadRouter(storage) {
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
if (error.statusCode === 503) {
|
|
72
|
+
return res.status(503).json({
|
|
73
|
+
error: 'Service Unavailable',
|
|
74
|
+
message: error.message
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
res.status(500).json({
|
|
71
79
|
error: 'Internal Server Error',
|
|
72
80
|
message: 'Failed to download file'
|
package/src/routes/list.js
CHANGED
|
@@ -30,6 +30,13 @@ export function createListRouter(storage) {
|
|
|
30
30
|
} catch (error) {
|
|
31
31
|
console.error('List files error:', error);
|
|
32
32
|
|
|
33
|
+
if (error.statusCode === 503) {
|
|
34
|
+
return res.status(503).json({
|
|
35
|
+
error: 'Service Unavailable',
|
|
36
|
+
message: error.message
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
res.status(500).json({
|
|
34
41
|
error: 'Internal Server Error',
|
|
35
42
|
message: 'Failed to list files'
|
package/src/routes/upload.js
CHANGED
|
@@ -42,10 +42,11 @@ export function createUploadRouter(storage, maxFileSize = 100 * 1024 * 1024) {
|
|
|
42
42
|
|
|
43
43
|
const userId = req.user.id;
|
|
44
44
|
const tenantId = req.user.tenantId;
|
|
45
|
+
const scope = req.fileScope || 'user';
|
|
45
46
|
const { buffer, originalname, mimetype } = req.file;
|
|
46
47
|
|
|
47
48
|
// Upload to Azure Blob Storage
|
|
48
|
-
const fileMetadata = await storage.uploadFile(tenantId, userId, buffer, originalname, mimetype);
|
|
49
|
+
const fileMetadata = await storage.uploadFile(tenantId, userId, buffer, originalname, mimetype, scope);
|
|
49
50
|
|
|
50
51
|
// Build response
|
|
51
52
|
const fileResponse = {
|
|
@@ -76,6 +77,13 @@ export function createUploadRouter(storage, maxFileSize = 100 * 1024 * 1024) {
|
|
|
76
77
|
});
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
if (error.statusCode === 503) {
|
|
81
|
+
return res.status(503).json({
|
|
82
|
+
error: 'Service Unavailable',
|
|
83
|
+
message: error.message
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
res.status(500).json({
|
|
80
88
|
error: 'Internal Server Error',
|
|
81
89
|
message: 'Failed to upload file'
|