streamby-core 0.29.8 → 0.29.11
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/dist/adapters/database/connectionManager.js +27 -7
- package/dist/middleware/createRouter.js +8 -0
- package/dist/middleware/routes/dbConnection.js +115 -33
- package/dist/middleware/routes/project.js +92 -2
- package/dist/middleware/routes/storage.js +84 -49
- package/dist/middleware/routes/storageConnection.js +40 -28
- package/dist/middleware/routes/userIntegration.d.ts +3 -0
- package/dist/middleware/routes/userIntegration.js +93 -0
- package/dist/services/backfillBuiltinConnections.d.ts +2 -0
- package/dist/services/backfillBuiltinConnections.js +57 -0
- package/dist/services/projectIntegration.d.ts +13 -0
- package/dist/services/projectIntegration.js +63 -0
- package/dist/services/userIntegration.d.ts +17 -0
- package/dist/services/userIntegration.js +86 -0
- package/dist/types/index.d.ts +22 -4
- package/dist/utils/builtinAccess.d.ts +6 -0
- package/dist/utils/builtinAccess.js +32 -0
- package/dist/utils/sanitize.d.ts +1 -0
- package/dist/utils/sanitize.js +7 -0
- package/package.json +1 -1
|
@@ -13,6 +13,8 @@ const manager_1 = require("../../models/manager");
|
|
|
13
13
|
const connectionManager_1 = require("../../adapters/database/connectionManager");
|
|
14
14
|
const auth_1 = require("../../utils/auth");
|
|
15
15
|
const encryption_1 = require("../../utils/encryption");
|
|
16
|
+
const builtinAccess_1 = require("../../utils/builtinAccess");
|
|
17
|
+
const userIntegration_1 = require("../../services/userIntegration");
|
|
16
18
|
const mongodb_1 = require("mongodb");
|
|
17
19
|
const crypto_1 = __importDefault(require("crypto"));
|
|
18
20
|
const VALID_CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
|
|
@@ -46,36 +48,64 @@ function getRawFoldersCollection() {
|
|
|
46
48
|
return null;
|
|
47
49
|
return (0, connectionManager_1.getConnection)(activeId).client.db().collection('storage_folders');
|
|
48
50
|
}
|
|
49
|
-
function connFilter(projectId, connId, extra) {
|
|
50
|
-
const isBuiltin = connId === 'builtin' || connId.startsWith('builtin-');
|
|
51
|
-
const connMatch = isBuiltin
|
|
52
|
-
? { $or: [{ storageConnectionId: connId }, { storageConnectionId: { $exists: false } }] }
|
|
53
|
-
: { storageConnectionId: connId };
|
|
54
|
-
return { projectId, ...connMatch, ...(extra ?? {}) };
|
|
55
|
-
}
|
|
56
|
-
async function resolveConnAdapter(connId, project, globalAdapter) {
|
|
57
|
-
if (connId === 'builtin' || connId.startsWith('builtin-'))
|
|
58
|
-
return globalAdapter;
|
|
59
|
-
const conn = project.storageConnections?.find((c) => c.id === connId);
|
|
60
|
-
if (!conn)
|
|
61
|
-
return { error: 'Storage connection not found', status: 404 };
|
|
62
|
-
if (!(0, encryption_1.isEncryptionKeySet)())
|
|
63
|
-
return { error: 'Encryption key not set', status: 500 };
|
|
64
|
-
const credential = project.credentials?.find((c) => c.id === conn.credentialId);
|
|
65
|
-
if (!credential)
|
|
66
|
-
return { error: 'Credential not found in project', status: 400 };
|
|
67
|
-
try {
|
|
68
|
-
const s3Config = JSON.parse((0, encryption_1.decrypt)(credential.encryptedValue));
|
|
69
|
-
return new s3_1.S3Adapter(s3Config);
|
|
70
|
-
}
|
|
71
|
-
catch (e) {
|
|
72
|
-
return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
51
|
function storageRouter(config) {
|
|
76
52
|
const router = (0, express_1.Router)();
|
|
77
53
|
const adapter = config.adapter || (0, storage_1.createStorageProvider)(config.storageProviders);
|
|
78
54
|
const Project = (0, manager_1.getModel)('projects');
|
|
55
|
+
// A row is builtin iff its own `source === 'builtin'` (set at backfill/creation time) —
|
|
56
|
+
// NOT by comparing connId (the row's own generated id) against config.storageProviders,
|
|
57
|
+
// which no longer holds since builtin rows get their own id distinct from provider.id.
|
|
58
|
+
function findStorageConn(project, connId) {
|
|
59
|
+
return project.storageConnections?.find((c) => c.id === connId);
|
|
60
|
+
}
|
|
61
|
+
function connFilter(projectId, connId, project, extra) {
|
|
62
|
+
const conn = findStorageConn(project, connId);
|
|
63
|
+
const isBuiltin = conn?.source === 'builtin';
|
|
64
|
+
const connMatch = isBuiltin
|
|
65
|
+
? { $or: [{ storageConnectionId: connId }, { storageConnectionId: { $exists: false } }] }
|
|
66
|
+
: { storageConnectionId: connId };
|
|
67
|
+
return { projectId, ...connMatch, ...(extra ?? {}) };
|
|
68
|
+
}
|
|
69
|
+
async function resolveConnAdapter(connId, project, auth) {
|
|
70
|
+
const conn = findStorageConn(project, connId);
|
|
71
|
+
if (!conn)
|
|
72
|
+
return { error: 'Storage connection not found', status: 404 };
|
|
73
|
+
if (conn.source === 'builtin') {
|
|
74
|
+
const builtinId = conn.integrationId;
|
|
75
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'storage'))) {
|
|
76
|
+
return { error: 'Access to this built-in storage is not permitted', status: 403 };
|
|
77
|
+
}
|
|
78
|
+
const provider = config.storageProviders.find(p => p.id === builtinId);
|
|
79
|
+
if (!provider)
|
|
80
|
+
return { error: 'Storage provider not found', status: 404 };
|
|
81
|
+
return provider.type === 's3' ? new s3_1.S3Adapter(provider.config) : adapter;
|
|
82
|
+
}
|
|
83
|
+
if (conn.source === 'integration') {
|
|
84
|
+
if (!conn.integrationId)
|
|
85
|
+
return { error: 'Connection is missing its integrationId', status: 500 };
|
|
86
|
+
try {
|
|
87
|
+
const s3Config = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId);
|
|
88
|
+
if (!s3Config)
|
|
89
|
+
return { error: 'Integration not found', status: 400 };
|
|
90
|
+
return new s3_1.S3Adapter(s3Config);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!(0, encryption_1.isEncryptionKeySet)())
|
|
97
|
+
return { error: 'Encryption key not set', status: 500 };
|
|
98
|
+
const credential = project.credentials?.find((c) => c.id === conn.credentialId);
|
|
99
|
+
if (!credential)
|
|
100
|
+
return { error: 'Credential not found in project', status: 400 };
|
|
101
|
+
try {
|
|
102
|
+
const s3Config = JSON.parse((0, encryption_1.decrypt)(credential.encryptedValue));
|
|
103
|
+
return new s3_1.S3Adapter(s3Config);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
79
109
|
router.get('/upload-project-image-url/:id', async (req, res) => {
|
|
80
110
|
try {
|
|
81
111
|
const auth = req.auth;
|
|
@@ -301,7 +331,7 @@ function storageRouter(config) {
|
|
|
301
331
|
return res.status(404).json({ message: 'Project not found' });
|
|
302
332
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
303
333
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
304
|
-
const connAdapter = await resolveConnAdapter(connId, project,
|
|
334
|
+
const connAdapter = await resolveConnAdapter(connId, project, auth);
|
|
305
335
|
if ('error' in connAdapter)
|
|
306
336
|
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
307
337
|
if (!connAdapter.getPresignedUploadUrl)
|
|
@@ -340,13 +370,13 @@ function storageRouter(config) {
|
|
|
340
370
|
return res.status(404).json({ message: 'Project not found' });
|
|
341
371
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
342
372
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
343
|
-
const connAdapter = await resolveConnAdapter(connId, project,
|
|
373
|
+
const connAdapter = await resolveConnAdapter(connId, project, auth);
|
|
344
374
|
if ('error' in connAdapter)
|
|
345
375
|
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
346
376
|
const collection = getRawFilesCollection();
|
|
347
377
|
if (!collection)
|
|
348
378
|
return res.status(500).json({ message: 'Database not available' });
|
|
349
|
-
const files = await collection.find(connFilter(projectId, connId)).sort({ createdAt: -1 }).limit(12).toArray();
|
|
379
|
+
const files = await collection.find(connFilter(projectId, connId, project)).sort({ createdAt: -1 }).limit(12).toArray();
|
|
350
380
|
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
|
|
351
381
|
res.json({ data });
|
|
352
382
|
}
|
|
@@ -366,13 +396,13 @@ function storageRouter(config) {
|
|
|
366
396
|
return res.status(404).json({ message: 'Project not found' });
|
|
367
397
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
368
398
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
369
|
-
const connAdapter = await resolveConnAdapter(connId, project,
|
|
399
|
+
const connAdapter = await resolveConnAdapter(connId, project, auth);
|
|
370
400
|
if ('error' in connAdapter)
|
|
371
401
|
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
372
402
|
const collection = getRawFilesCollection();
|
|
373
403
|
if (!collection)
|
|
374
404
|
return res.status(500).json({ message: 'Database not available' });
|
|
375
|
-
const files = await collection.find(connFilter(projectId, connId, { category })).sort({ createdAt: -1 }).toArray();
|
|
405
|
+
const files = await collection.find(connFilter(projectId, connId, project, { category })).sort({ createdAt: -1 }).toArray();
|
|
376
406
|
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
|
|
377
407
|
res.json({ data });
|
|
378
408
|
}
|
|
@@ -396,20 +426,20 @@ function storageRouter(config) {
|
|
|
396
426
|
const collection = getRawFilesCollection();
|
|
397
427
|
if (!collection)
|
|
398
428
|
return res.status(500).json({ message: 'Database not available' });
|
|
399
|
-
const fileDoc = await collection.findOne(connFilter(projectId, connId, { fileId }));
|
|
429
|
+
const fileDoc = await collection.findOne(connFilter(projectId, connId, project, { fileId }));
|
|
400
430
|
if (!fileDoc)
|
|
401
431
|
return res.status(404).json({ message: 'File not found' });
|
|
402
432
|
if (!validateContentType(contentType, fileName, fileDoc.category)) {
|
|
403
433
|
return res.status(400).json({ message: `File does not match category ${fileDoc.category}` });
|
|
404
434
|
}
|
|
405
|
-
const connAdapter = await resolveConnAdapter(connId, project,
|
|
435
|
+
const connAdapter = await resolveConnAdapter(connId, project, auth);
|
|
406
436
|
if ('error' in connAdapter)
|
|
407
437
|
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
408
438
|
if (!connAdapter.getPresignedUploadUrl) {
|
|
409
439
|
return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
|
|
410
440
|
}
|
|
411
441
|
const url = await connAdapter.getPresignedUploadUrl(fileDoc.storageKey, contentType);
|
|
412
|
-
await collection.updateOne(connFilter(projectId, connId, { fileId }), { $set: { contentType, updatedAt: new Date() } });
|
|
442
|
+
await collection.updateOne(connFilter(projectId, connId, project, { fileId }), { $set: { contentType, updatedAt: new Date() } });
|
|
413
443
|
res.json({ url, storageKey: fileDoc.storageKey, fileId });
|
|
414
444
|
}
|
|
415
445
|
catch (err) {
|
|
@@ -432,7 +462,7 @@ function storageRouter(config) {
|
|
|
432
462
|
const collection = getRawFilesCollection();
|
|
433
463
|
if (!collection)
|
|
434
464
|
return res.status(500).json({ message: 'Database not available' });
|
|
435
|
-
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { fileId }), { $set: { displayName } }, { returnDocument: 'after' });
|
|
465
|
+
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, project, { fileId }), { $set: { displayName } }, { returnDocument: 'after' });
|
|
436
466
|
if (!result)
|
|
437
467
|
return res.status(404).json({ message: 'File not found' });
|
|
438
468
|
res.json({ message: 'File renamed successfully', file: result });
|
|
@@ -450,18 +480,18 @@ function storageRouter(config) {
|
|
|
450
480
|
return res.status(404).json({ message: 'Project not found' });
|
|
451
481
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
452
482
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
453
|
-
const connAdapter = await resolveConnAdapter(connId, project,
|
|
483
|
+
const connAdapter = await resolveConnAdapter(connId, project, auth);
|
|
454
484
|
if ('error' in connAdapter)
|
|
455
485
|
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
456
486
|
const collection = getRawFilesCollection();
|
|
457
487
|
if (!collection)
|
|
458
488
|
return res.status(500).json({ message: 'Database not available' });
|
|
459
|
-
const fileDoc = await collection.findOne(connFilter(projectId, connId, { fileId }));
|
|
489
|
+
const fileDoc = await collection.findOne(connFilter(projectId, connId, project, { fileId }));
|
|
460
490
|
if (!fileDoc)
|
|
461
491
|
return res.status(404).json({ message: 'File not found' });
|
|
462
492
|
if (connAdapter.deleteFile)
|
|
463
493
|
await connAdapter.deleteFile(fileDoc.storageKey);
|
|
464
|
-
await collection.deleteOne(connFilter(projectId, connId, { fileId }));
|
|
494
|
+
await collection.deleteOne(connFilter(projectId, connId, project, { fileId }));
|
|
465
495
|
res.json({ message: 'File deleted successfully' });
|
|
466
496
|
}
|
|
467
497
|
catch (err) {
|
|
@@ -486,7 +516,7 @@ function storageRouter(config) {
|
|
|
486
516
|
? { parentId: null }
|
|
487
517
|
: { parentId };
|
|
488
518
|
const folders = await collection
|
|
489
|
-
.find(connFilter(projectId, connId, parentFilter))
|
|
519
|
+
.find(connFilter(projectId, connId, project, parentFilter))
|
|
490
520
|
.sort({ name: 1 })
|
|
491
521
|
.toArray();
|
|
492
522
|
res.json({
|
|
@@ -517,7 +547,7 @@ function storageRouter(config) {
|
|
|
517
547
|
const collection = getRawFoldersCollection();
|
|
518
548
|
if (!collection)
|
|
519
549
|
return res.status(500).json({ message: 'Database not available' });
|
|
520
|
-
const f = await collection.findOne(connFilter(projectId, connId, { folderId }));
|
|
550
|
+
const f = await collection.findOne(connFilter(projectId, connId, project, { folderId }));
|
|
521
551
|
if (!f)
|
|
522
552
|
return res.status(404).json({ message: 'Folder not found' });
|
|
523
553
|
res.json({
|
|
@@ -549,11 +579,15 @@ function storageRouter(config) {
|
|
|
549
579
|
return res.status(404).json({ message: 'Project not found' });
|
|
550
580
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
551
581
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
582
|
+
const conn = findStorageConn(project, connId);
|
|
583
|
+
const isBuiltin = conn?.source === 'builtin';
|
|
584
|
+
if (isBuiltin && !(await (0, builtinAccess_1.assertBuiltinAccess)(auth, conn.integrationId, config, 'storage'))) {
|
|
585
|
+
return res.status(403).json({ message: 'Access to this built-in storage is not permitted' });
|
|
586
|
+
}
|
|
552
587
|
const collection = getRawFoldersCollection();
|
|
553
588
|
if (!collection)
|
|
554
589
|
return res.status(500).json({ message: 'Database not available' });
|
|
555
590
|
const folderId = crypto_1.default.randomUUID();
|
|
556
|
-
const isBuiltin = connId === 'builtin' || connId.startsWith('builtin-');
|
|
557
591
|
const folderDoc = {
|
|
558
592
|
_id: new mongodb_1.ObjectId(),
|
|
559
593
|
folderId,
|
|
@@ -599,7 +633,7 @@ function storageRouter(config) {
|
|
|
599
633
|
const collection = getRawFoldersCollection();
|
|
600
634
|
if (!collection)
|
|
601
635
|
return res.status(500).json({ message: 'Database not available' });
|
|
602
|
-
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { folderId }), { $set: { name: name.trim(), updatedAt: new Date() } }, { returnDocument: 'after' });
|
|
636
|
+
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, project, { folderId }), { $set: { name: name.trim(), updatedAt: new Date() } }, { returnDocument: 'after' });
|
|
603
637
|
if (!result)
|
|
604
638
|
return res.status(404).json({ message: 'Folder not found' });
|
|
605
639
|
res.json({
|
|
@@ -633,7 +667,7 @@ function storageRouter(config) {
|
|
|
633
667
|
const collection = getRawFoldersCollection();
|
|
634
668
|
if (!collection)
|
|
635
669
|
return res.status(500).json({ message: 'Database not available' });
|
|
636
|
-
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { folderId }), { $set: { parentId: newParentId ?? null, updatedAt: new Date() } }, { returnDocument: 'after' });
|
|
670
|
+
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, project, { folderId }), { $set: { parentId: newParentId ?? null, updatedAt: new Date() } }, { returnDocument: 'after' });
|
|
637
671
|
if (!result)
|
|
638
672
|
return res.status(404).json({ message: 'Folder not found' });
|
|
639
673
|
res.json({
|
|
@@ -663,15 +697,15 @@ function storageRouter(config) {
|
|
|
663
697
|
const foldersCollection = getRawFoldersCollection();
|
|
664
698
|
if (!foldersCollection)
|
|
665
699
|
return res.status(500).json({ message: 'Database not available' });
|
|
666
|
-
const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, { folderId }));
|
|
700
|
+
const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, project, { folderId }));
|
|
667
701
|
if (!folderDoc)
|
|
668
702
|
return res.status(404).json({ message: 'Folder not found' });
|
|
669
703
|
const filesCollection = getRawFilesCollection();
|
|
670
704
|
if (filesCollection) {
|
|
671
|
-
await filesCollection.updateMany(connFilter(projectId, connId, { folderId }), { $set: { folderId: folderDoc.parentId ?? null } });
|
|
705
|
+
await filesCollection.updateMany(connFilter(projectId, connId, project, { folderId }), { $set: { folderId: folderDoc.parentId ?? null } });
|
|
672
706
|
}
|
|
673
|
-
await foldersCollection.updateMany(connFilter(projectId, connId, { parentId: folderId }), { $set: { parentId: folderDoc.parentId ?? null } });
|
|
674
|
-
await foldersCollection.deleteOne(connFilter(projectId, connId, { folderId }));
|
|
707
|
+
await foldersCollection.updateMany(connFilter(projectId, connId, project, { parentId: folderId }), { $set: { parentId: folderDoc.parentId ?? null } });
|
|
708
|
+
await foldersCollection.deleteOne(connFilter(projectId, connId, project, { folderId }));
|
|
675
709
|
res.json({ message: 'Folder deleted successfully' });
|
|
676
710
|
}
|
|
677
711
|
catch (err) {
|
|
@@ -691,7 +725,7 @@ function storageRouter(config) {
|
|
|
691
725
|
if (folderId != null) {
|
|
692
726
|
const foldersCollection = getRawFoldersCollection();
|
|
693
727
|
if (foldersCollection) {
|
|
694
|
-
const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, { folderId }));
|
|
728
|
+
const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, project, { folderId }));
|
|
695
729
|
if (!folderDoc)
|
|
696
730
|
return res.status(404).json({ message: 'Folder not found' });
|
|
697
731
|
}
|
|
@@ -699,7 +733,7 @@ function storageRouter(config) {
|
|
|
699
733
|
const filesCollection = getRawFilesCollection();
|
|
700
734
|
if (!filesCollection)
|
|
701
735
|
return res.status(500).json({ message: 'Database not available' });
|
|
702
|
-
const result = await filesCollection.findOneAndUpdate(connFilter(projectId, connId, { fileId }), { $set: { folderId: folderId ?? null } }, { returnDocument: 'after' });
|
|
736
|
+
const result = await filesCollection.findOneAndUpdate(connFilter(projectId, connId, project, { fileId }), { $set: { folderId: folderId ?? null } }, { returnDocument: 'after' });
|
|
703
737
|
if (!result)
|
|
704
738
|
return res.status(404).json({ message: 'File not found' });
|
|
705
739
|
res.json({ message: 'File moved successfully', file: result });
|
|
@@ -717,6 +751,7 @@ function storageRouter(config) {
|
|
|
717
751
|
r2: { value: 'cloudflare-r2', name: 'Cloudflare R2' },
|
|
718
752
|
};
|
|
719
753
|
const storages = (config.storageProviders || []).map(provider => ({
|
|
754
|
+
id: provider.id,
|
|
720
755
|
...STORAGE_DISPLAY[provider.type],
|
|
721
756
|
type: provider.type,
|
|
722
757
|
}));
|
|
@@ -5,13 +5,8 @@ const express_1 = require("express");
|
|
|
5
5
|
const mongodb_1 = require("mongodb");
|
|
6
6
|
const manager_1 = require("../../models/manager");
|
|
7
7
|
const auth_1 = require("../../utils/auth");
|
|
8
|
+
const builtinAccess_1 = require("../../utils/builtinAccess");
|
|
8
9
|
const VALID_STORAGE_TYPES = ['s3', 'gcs', 'r2', 'azure'];
|
|
9
|
-
const STORAGE_DISPLAY = {
|
|
10
|
-
s3: 'AWS S3',
|
|
11
|
-
gcs: 'Google Cloud Storage',
|
|
12
|
-
r2: 'Cloudflare R2',
|
|
13
|
-
azure: 'Azure Blob Storage',
|
|
14
|
-
};
|
|
15
10
|
function storageConnectionRouter(config) {
|
|
16
11
|
const router = (0, express_1.Router)();
|
|
17
12
|
const Project = (0, manager_1.getModel)('projects');
|
|
@@ -24,17 +19,16 @@ function storageConnectionRouter(config) {
|
|
|
24
19
|
return res.status(404).json({ message: 'Project not found' });
|
|
25
20
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
26
21
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
isBuiltin: true,
|
|
22
|
+
const connections = project.storageConnections || [];
|
|
23
|
+
// Recompute availability live for builtins — never trust a stored flag, per
|
|
24
|
+
// "validate on every use" (TCORE-69). BYOC (source: 'integration') is always available.
|
|
25
|
+
const data = await Promise.all(connections.map(async (conn) => {
|
|
26
|
+
if (conn.source !== 'builtin')
|
|
27
|
+
return conn;
|
|
28
|
+
const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, conn.integrationId ?? conn.id, config, 'storage');
|
|
29
|
+
return { ...conn, isBuiltin: true, available };
|
|
35
30
|
}));
|
|
36
|
-
|
|
37
|
-
return res.status(200).json({ data: [...builtinConnections, ...projectConnections] });
|
|
31
|
+
return res.status(200).json({ data });
|
|
38
32
|
}
|
|
39
33
|
catch (err) {
|
|
40
34
|
res.status(500).json({ message: 'Failed to fetch storage connections', details: err.message });
|
|
@@ -47,9 +41,15 @@ function storageConnectionRouter(config) {
|
|
|
47
41
|
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
48
42
|
return res.status(403).json({ message: 'Permission denied' });
|
|
49
43
|
}
|
|
50
|
-
const { name, type, credentialId, description } = req.body;
|
|
51
|
-
if (!name || !type
|
|
52
|
-
return res.status(400).json({ message: 'name
|
|
44
|
+
const { name, type, credentialId, integrationId, description } = req.body;
|
|
45
|
+
if (!name || !type) {
|
|
46
|
+
return res.status(400).json({ message: 'name and type are required' });
|
|
47
|
+
}
|
|
48
|
+
if (!credentialId && !integrationId) {
|
|
49
|
+
return res.status(400).json({ message: 'Either credentialId or integrationId is required' });
|
|
50
|
+
}
|
|
51
|
+
if (credentialId && integrationId) {
|
|
52
|
+
return res.status(400).json({ message: 'Provide only one of credentialId or integrationId' });
|
|
53
53
|
}
|
|
54
54
|
if (!VALID_STORAGE_TYPES.includes(type)) {
|
|
55
55
|
return res.status(400).json({ message: `type must be one of: ${VALID_STORAGE_TYPES.join(', ')}` });
|
|
@@ -59,16 +59,28 @@ function storageConnectionRouter(config) {
|
|
|
59
59
|
return res.status(404).json({ message: 'Project not found' });
|
|
60
60
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
61
61
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
if (integrationId) {
|
|
63
|
+
const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
|
|
64
|
+
const integration = await UserIntegrationModel.findOne({ id: integrationId, userId: auth.userId });
|
|
65
|
+
if (!integration)
|
|
66
|
+
return res.status(404).json({ message: 'Integration not found' });
|
|
67
|
+
if (integration.kind !== 'storage')
|
|
68
|
+
return res.status(400).json({ message: 'Integration is not a storage integration' });
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const credExists = project.credentials?.some((c) => c.id === credentialId);
|
|
72
|
+
if (!credExists)
|
|
73
|
+
return res.status(400).json({ message: 'Credential not found in project' });
|
|
74
|
+
}
|
|
65
75
|
const connection = {
|
|
66
76
|
id: new mongodb_1.ObjectId().toHexString(),
|
|
67
77
|
name,
|
|
68
78
|
type,
|
|
69
|
-
credentialId,
|
|
79
|
+
credentialId: credentialId ?? '',
|
|
70
80
|
projectId: req.params.id,
|
|
71
81
|
createdAt: new Date(),
|
|
82
|
+
source: integrationId ? 'integration' : 'manual',
|
|
83
|
+
...(integrationId && { integrationId }),
|
|
72
84
|
...(description !== undefined && { description }),
|
|
73
85
|
};
|
|
74
86
|
await Project.update({ _id: req.params.id }, { $push: { storageConnections: connection } });
|
|
@@ -82,9 +94,6 @@ function storageConnectionRouter(config) {
|
|
|
82
94
|
router.delete('/projects/:id/connections/storage/:connId', async (req, res) => {
|
|
83
95
|
try {
|
|
84
96
|
const { connId } = req.params;
|
|
85
|
-
if (connId === 'builtin' || connId.startsWith('builtin-')) {
|
|
86
|
-
return res.status(403).json({ message: 'Cannot delete a built-in storage connection' });
|
|
87
|
-
}
|
|
88
97
|
const auth = req.auth;
|
|
89
98
|
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
90
99
|
return res.status(403).json({ message: 'Permission denied' });
|
|
@@ -94,9 +103,12 @@ function storageConnectionRouter(config) {
|
|
|
94
103
|
return res.status(404).json({ message: 'Project not found' });
|
|
95
104
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
96
105
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
97
|
-
const
|
|
98
|
-
if (!
|
|
106
|
+
const conn = project.storageConnections?.find((c) => c.id === connId);
|
|
107
|
+
if (!conn)
|
|
99
108
|
return res.status(404).json({ message: 'Storage connection not found' });
|
|
109
|
+
if (conn.source === 'builtin') {
|
|
110
|
+
return res.status(403).json({ message: 'Cannot delete a built-in storage connection' });
|
|
111
|
+
}
|
|
100
112
|
await Project.update({ _id: req.params.id }, { $pull: { storageConnections: { id: connId } } });
|
|
101
113
|
return res.status(200).json({ message: 'Storage connection deleted successfully' });
|
|
102
114
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.userIntegrationRouter = userIntegrationRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const builtinAccess_1 = require("../../utils/builtinAccess");
|
|
6
|
+
const userIntegration_1 = require("../../services/userIntegration");
|
|
7
|
+
const sanitize_1 = require("../../utils/sanitize");
|
|
8
|
+
const VALID_KINDS = ['database', 'storage'];
|
|
9
|
+
const VALID_DB_PROVIDERS = ['postgresql', 'mongodb'];
|
|
10
|
+
const VALID_STORAGE_PROVIDERS = ['s3', 'gcs', 'r2', 'azure'];
|
|
11
|
+
function userIntegrationRouter(config) {
|
|
12
|
+
const router = (0, express_1.Router)();
|
|
13
|
+
// ─── Combined pool: account integrations + built-ins the user can access ─────
|
|
14
|
+
router.get('/user/integrations', async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const auth = req.auth;
|
|
17
|
+
const owned = await (0, userIntegration_1.listUserIntegrations)(auth.userId);
|
|
18
|
+
const pool = owned.map(i => ({
|
|
19
|
+
id: i.id, kind: i.kind, name: i.name, provider: i.provider, source: 'integration', available: true,
|
|
20
|
+
}));
|
|
21
|
+
for (const db of config.databases ?? []) {
|
|
22
|
+
// requiredPlan is not populated here — canUseBuiltin only returns a boolean today,
|
|
23
|
+
// so there's no plan-level detail to surface without a hook-signature change.
|
|
24
|
+
const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, db.id, config, 'database');
|
|
25
|
+
pool.push({ id: db.id, kind: 'database', name: builtinAccess_1.BUILTIN_DB_DISPLAY[db.type] ?? db.type, provider: db.type, source: 'builtin', available });
|
|
26
|
+
}
|
|
27
|
+
for (const provider of config.storageProviders ?? []) {
|
|
28
|
+
const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, provider.id, config, 'storage');
|
|
29
|
+
pool.push({ id: provider.id, kind: 'storage', name: builtinAccess_1.BUILTIN_STORAGE_DISPLAY[provider.type] ?? provider.type, provider: provider.type, source: 'builtin', available });
|
|
30
|
+
}
|
|
31
|
+
res.json({ data: pool });
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
res.status(500).json({ message: 'Failed to list integrations', details: err.message });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// ─── Create ────────────────────────────────────────────────────────────────
|
|
38
|
+
router.post('/user/integrations', async (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const auth = req.auth;
|
|
41
|
+
const { kind, provider, name, description, credential } = req.body;
|
|
42
|
+
if (!kind || !VALID_KINDS.includes(kind)) {
|
|
43
|
+
return res.status(400).json({ message: `kind must be one of: ${VALID_KINDS.join(', ')}` });
|
|
44
|
+
}
|
|
45
|
+
const validProviders = kind === 'database' ? VALID_DB_PROVIDERS : VALID_STORAGE_PROVIDERS;
|
|
46
|
+
if (!provider || !validProviders.includes(provider)) {
|
|
47
|
+
return res.status(400).json({ message: `provider must be one of: ${validProviders.join(', ')}` });
|
|
48
|
+
}
|
|
49
|
+
if (!name)
|
|
50
|
+
return res.status(400).json({ message: 'name is required' });
|
|
51
|
+
if (credential === undefined || credential === null) {
|
|
52
|
+
return res.status(400).json({ message: 'credential is required' });
|
|
53
|
+
}
|
|
54
|
+
const integration = await (0, userIntegration_1.createUserIntegration)(auth.userId, {
|
|
55
|
+
kind, provider, name, description, credentialValue: credential,
|
|
56
|
+
});
|
|
57
|
+
res.status(201).json({ data: (0, sanitize_1.sanitizeUserIntegration)(integration) });
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
res.status(500).json({ message: 'Failed to create integration', details: err.message });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
// ─── Update ────────────────────────────────────────────────────────────────
|
|
64
|
+
router.patch('/user/integrations/:integrationId', async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const auth = req.auth;
|
|
67
|
+
const { name, description, credential } = req.body;
|
|
68
|
+
const updated = await (0, userIntegration_1.updateUserIntegration)(auth.userId, req.params.integrationId, {
|
|
69
|
+
name, description, credentialValue: credential,
|
|
70
|
+
});
|
|
71
|
+
if (!updated)
|
|
72
|
+
return res.status(404).json({ message: 'Integration not found' });
|
|
73
|
+
res.json({ data: (0, sanitize_1.sanitizeUserIntegration)(updated) });
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
res.status(500).json({ message: 'Failed to update integration', details: err.message });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// ─── Delete ────────────────────────────────────────────────────────────────
|
|
80
|
+
router.delete('/user/integrations/:integrationId', async (req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const auth = req.auth;
|
|
83
|
+
const deleted = await (0, userIntegration_1.deleteUserIntegration)(auth.userId, req.params.integrationId);
|
|
84
|
+
if (!deleted)
|
|
85
|
+
return res.status(404).json({ message: 'Integration not found' });
|
|
86
|
+
res.json({ message: 'Integration deleted' });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
res.status(500).json({ message: 'Failed to delete integration', details: err.message });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return router;
|
|
93
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.backfillBuiltinConnections = backfillBuiltinConnections;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const manager_1 = require("../models/manager");
|
|
9
|
+
const builtinAccess_1 = require("../utils/builtinAccess");
|
|
10
|
+
// Pre-BYOC, every project appeared connected to every configured builtin because
|
|
11
|
+
// storageConnection.ts/dbConnection.ts SYNTHESIZED builtins at read time instead of
|
|
12
|
+
// persisting them (see storageConnection.ts's old GET handler). Now that connections must
|
|
13
|
+
// be real rows on the project, this seeds those rows once for projects that predate the
|
|
14
|
+
// change — so existing projects don't silently lose access on deploy.
|
|
15
|
+
//
|
|
16
|
+
// Guarded by ProjectInfo.builtinBackfilledAt: runs once per project, ever. An admin who
|
|
17
|
+
// later disconnects a builtin on purpose won't have it silently reappear on the next boot.
|
|
18
|
+
async function backfillBuiltinConnections(config) {
|
|
19
|
+
if (!config.databases?.length && !config.storageProviders?.length)
|
|
20
|
+
return;
|
|
21
|
+
const Project = (0, manager_1.getModel)('projects');
|
|
22
|
+
const allProjects = await Project.find({});
|
|
23
|
+
const pending = allProjects.filter(p => !p.builtinBackfilledAt);
|
|
24
|
+
if (!pending.length)
|
|
25
|
+
return;
|
|
26
|
+
for (const project of pending) {
|
|
27
|
+
const now = new Date();
|
|
28
|
+
const dbConnections = (config.databases ?? []).map(db => ({
|
|
29
|
+
id: crypto_1.default.randomUUID(),
|
|
30
|
+
name: builtinAccess_1.BUILTIN_DB_DISPLAY[db.type] ?? db.type,
|
|
31
|
+
dbType: db.type === 'sql' ? 'postgresql' : 'mongodb',
|
|
32
|
+
credentialId: '',
|
|
33
|
+
projectId: project.id,
|
|
34
|
+
createdAt: now,
|
|
35
|
+
isBuiltin: true,
|
|
36
|
+
integrationId: db.id,
|
|
37
|
+
source: 'builtin',
|
|
38
|
+
}));
|
|
39
|
+
const storageConnections = (config.storageProviders ?? []).map(provider => ({
|
|
40
|
+
id: crypto_1.default.randomUUID(),
|
|
41
|
+
name: builtinAccess_1.BUILTIN_STORAGE_DISPLAY[provider.type] ?? provider.type,
|
|
42
|
+
type: provider.type,
|
|
43
|
+
credentialId: '',
|
|
44
|
+
projectId: project.id,
|
|
45
|
+
createdAt: now,
|
|
46
|
+
isBuiltin: true,
|
|
47
|
+
integrationId: provider.id,
|
|
48
|
+
source: 'builtin',
|
|
49
|
+
}));
|
|
50
|
+
await Project.update({ _id: project.id }, {
|
|
51
|
+
dbConnections: [...(project.dbConnections ?? []), ...dbConnections],
|
|
52
|
+
storageConnections: [...(project.storageConnections ?? []), ...storageConnections],
|
|
53
|
+
builtinBackfilledAt: now,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
console.log(`✅ Backfilled built-in connections for ${pending.length} project(s).`);
|
|
57
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Auth, DbConnection, StorageConnection, StreamByConfig } from '../types';
|
|
2
|
+
type ClassifyResult = {
|
|
3
|
+
kind: 'database';
|
|
4
|
+
connection: DbConnection;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'storage';
|
|
7
|
+
connection: StorageConnection;
|
|
8
|
+
} | {
|
|
9
|
+
error: string;
|
|
10
|
+
status: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function classifyIntegrationId(id: string, auth: Auth, config: StreamByConfig, projectId: string): Promise<ClassifyResult>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.classifyIntegrationId = classifyIntegrationId;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const manager_1 = require("../models/manager");
|
|
9
|
+
const builtinAccess_1 = require("../utils/builtinAccess");
|
|
10
|
+
// Classifies a client-supplied integrationId (builtin db, builtin storage, or a user's own
|
|
11
|
+
// BYOC integration) and revalidates access/ownership server-side — never trust the client.
|
|
12
|
+
// Shared by POST /projects/create (bulk, at project creation) and
|
|
13
|
+
// POST /projects/:id/integrations (single, added to an existing project).
|
|
14
|
+
async function classifyIntegrationId(id, auth, config, projectId) {
|
|
15
|
+
const now = new Date();
|
|
16
|
+
if ((0, builtinAccess_1.isBuiltinDb)(id, config)) {
|
|
17
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, id, config, 'database'))) {
|
|
18
|
+
return { error: `Access to built-in database '${id}' is not permitted`, status: 403 };
|
|
19
|
+
}
|
|
20
|
+
const db = config.databases.find(d => d.id === id);
|
|
21
|
+
return {
|
|
22
|
+
kind: 'database',
|
|
23
|
+
connection: {
|
|
24
|
+
id: crypto_1.default.randomUUID(), name: builtinAccess_1.BUILTIN_DB_DISPLAY[db.type] ?? db.type, dbType: db.type === 'sql' ? 'postgresql' : 'mongodb',
|
|
25
|
+
credentialId: '', projectId, createdAt: now, isBuiltin: true, integrationId: id, source: 'builtin',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if ((0, builtinAccess_1.isBuiltinStorageId)(id, config)) {
|
|
30
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, id, config, 'storage'))) {
|
|
31
|
+
return { error: `Access to built-in storage '${id}' is not permitted`, status: 403 };
|
|
32
|
+
}
|
|
33
|
+
const provider = config.storageProviders.find(p => p.id === id);
|
|
34
|
+
return {
|
|
35
|
+
kind: 'storage',
|
|
36
|
+
connection: {
|
|
37
|
+
id: crypto_1.default.randomUUID(), name: builtinAccess_1.BUILTIN_STORAGE_DISPLAY[provider.type] ?? provider.type, type: provider.type,
|
|
38
|
+
credentialId: '', projectId, createdAt: now, isBuiltin: true, integrationId: id, source: 'builtin',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
|
|
43
|
+
const integration = await UserIntegrationModel.findOne({ id, userId: auth.userId });
|
|
44
|
+
if (!integration) {
|
|
45
|
+
return { error: `Integration '${id}' is not valid or does not belong to you`, status: 403 };
|
|
46
|
+
}
|
|
47
|
+
if (integration.kind === 'database') {
|
|
48
|
+
return {
|
|
49
|
+
kind: 'database',
|
|
50
|
+
connection: {
|
|
51
|
+
id: crypto_1.default.randomUUID(), name: integration.name, dbType: integration.provider,
|
|
52
|
+
credentialId: '', projectId, createdAt: now, isBuiltin: false, integrationId: id, source: 'integration',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
kind: 'storage',
|
|
58
|
+
connection: {
|
|
59
|
+
id: crypto_1.default.randomUUID(), name: integration.name, type: integration.provider,
|
|
60
|
+
credentialId: '', projectId, createdAt: now, isBuiltin: false, integrationId: id, source: 'integration',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|