streamby-core 0.29.9 → 0.29.12
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 +1 -0
- package/dist/middleware/routes/dbConnection.js +100 -37
- package/dist/middleware/routes/project.js +71 -45
- package/dist/middleware/routes/storage.js +54 -28
- package/dist/middleware/routes/storageConnection.js +30 -12
- package/dist/middleware/routes/userIntegration.js +2 -2
- package/dist/services/backfillBuiltinConnections.js +3 -2
- package/dist/services/projectIntegration.d.ts +13 -0
- package/dist/services/projectIntegration.js +63 -0
- package/dist/services/userIntegration.d.ts +1 -0
- package/dist/services/userIntegration.js +13 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/utils/builtinAccess.d.ts +2 -0
- package/dist/utils/builtinAccess.js +13 -0
- package/package.json +1 -1
|
@@ -81,6 +81,7 @@ const ensureTablesExist = async (pool) => {
|
|
|
81
81
|
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "storageConnections" JSONB DEFAULT '[]';
|
|
82
82
|
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
|
|
83
83
|
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
|
|
84
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "builtinBackfilledAt" TIMESTAMP WITH TIME ZONE;
|
|
84
85
|
`);
|
|
85
86
|
// Ensure dbType exists with a non-null default
|
|
86
87
|
await pool.query(`
|
|
@@ -40,24 +40,41 @@ const manager_1 = require("../../models/manager");
|
|
|
40
40
|
const auth_1 = require("../../utils/auth");
|
|
41
41
|
const connectionManager_1 = require("../../adapters/database/connectionManager");
|
|
42
42
|
const builtinAccess_1 = require("../../utils/builtinAccess");
|
|
43
|
+
const userIntegration_1 = require("../../services/userIntegration");
|
|
43
44
|
const dbConnection_1 = require("../../services/dbConnection");
|
|
44
45
|
const VALID_DB_TYPES = ['postgresql', 'mongodb'];
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
// `builtinId` is config.databases[].id (e.g. 'mongo'), NOT the DbConnection row's own id —
|
|
47
|
+
// those are different values since TCORE-69's backfill gives each row its own UUID.
|
|
48
|
+
function resolveBuiltinConnection(builtinId, config) {
|
|
49
|
+
const db = (config.databases ?? []).find(d => d.id === builtinId);
|
|
47
50
|
if (!db)
|
|
48
|
-
return { error: `Database '${
|
|
51
|
+
return { error: `Database '${builtinId}' not found in config`, status: 404 };
|
|
49
52
|
try {
|
|
50
|
-
const { client, type } = (0, connectionManager_1.getConnection)(
|
|
53
|
+
const { client, type } = (0, connectionManager_1.getConnection)(builtinId);
|
|
51
54
|
return { client: client, dbType: type };
|
|
52
55
|
}
|
|
53
56
|
catch (e) {
|
|
54
57
|
return { error: e.message, status: 503 };
|
|
55
58
|
}
|
|
56
59
|
}
|
|
60
|
+
// Finds the connection row + classifies it. A row is builtin iff conn.source === 'builtin'
|
|
61
|
+
// (persisted at creation/backfill time) — NOT by comparing the row's own id against
|
|
62
|
+
// config.databases, which no longer holds since builtin rows get their own generated id.
|
|
63
|
+
function findDbConnection(project, connId) {
|
|
64
|
+
return project.dbConnections?.find((c) => c.id === connId);
|
|
65
|
+
}
|
|
57
66
|
async function getDecryptedConnectionString(project, connId) {
|
|
58
67
|
const conn = project.dbConnections?.find((c) => c.id === connId);
|
|
59
68
|
if (!conn)
|
|
60
69
|
return { error: 'DB connection not found', status: 404 };
|
|
70
|
+
if (conn.source === 'integration') {
|
|
71
|
+
if (!conn.integrationId)
|
|
72
|
+
return { error: 'Connection is missing its integrationId', status: 500 };
|
|
73
|
+
const credential = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId);
|
|
74
|
+
if (!credential)
|
|
75
|
+
return { error: 'Integration not found', status: 400 };
|
|
76
|
+
return { connectionString: credential, conn };
|
|
77
|
+
}
|
|
61
78
|
const { decrypt, isEncryptionKeySet } = await Promise.resolve().then(() => __importStar(require('../../utils/encryption')));
|
|
62
79
|
if (!isEncryptionKeySet())
|
|
63
80
|
return { error: 'Encryption key is not set', status: 500 };
|
|
@@ -91,9 +108,15 @@ function dbConnectionRouter(config) {
|
|
|
91
108
|
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
92
109
|
return res.status(403).json({ message: 'Permission denied' });
|
|
93
110
|
}
|
|
94
|
-
const { name, dbType, credentialId, description } = req.body;
|
|
95
|
-
if (!name || !dbType
|
|
96
|
-
return res.status(400).json({ message: 'name
|
|
111
|
+
const { name, dbType, credentialId, integrationId, description } = req.body;
|
|
112
|
+
if (!name || !dbType) {
|
|
113
|
+
return res.status(400).json({ message: 'name and dbType are required' });
|
|
114
|
+
}
|
|
115
|
+
if (!credentialId && !integrationId) {
|
|
116
|
+
return res.status(400).json({ message: 'Either credentialId or integrationId is required' });
|
|
117
|
+
}
|
|
118
|
+
if (credentialId && integrationId) {
|
|
119
|
+
return res.status(400).json({ message: 'Provide only one of credentialId or integrationId' });
|
|
97
120
|
}
|
|
98
121
|
if (!VALID_DB_TYPES.includes(dbType)) {
|
|
99
122
|
return res.status(400).json({ message: `dbType must be one of: ${VALID_DB_TYPES.join(', ')}` });
|
|
@@ -103,16 +126,28 @@ function dbConnectionRouter(config) {
|
|
|
103
126
|
return res.status(404).json({ message: 'Project not found' });
|
|
104
127
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
105
128
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
129
|
+
if (integrationId) {
|
|
130
|
+
const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
|
|
131
|
+
const integration = await UserIntegrationModel.findOne({ id: integrationId, userId: auth.userId });
|
|
132
|
+
if (!integration)
|
|
133
|
+
return res.status(404).json({ message: 'Integration not found' });
|
|
134
|
+
if (integration.kind !== 'database')
|
|
135
|
+
return res.status(400).json({ message: 'Integration is not a database integration' });
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const credExists = project.credentials?.some((c) => c.id === credentialId);
|
|
139
|
+
if (!credExists)
|
|
140
|
+
return res.status(400).json({ message: 'Credential not found in project' });
|
|
141
|
+
}
|
|
109
142
|
const connection = {
|
|
110
143
|
id: new mongodb_1.ObjectId().toHexString(),
|
|
111
144
|
name,
|
|
112
145
|
dbType,
|
|
113
|
-
credentialId,
|
|
146
|
+
credentialId: credentialId ?? '',
|
|
114
147
|
projectId: req.params.id,
|
|
115
148
|
createdAt: new Date(),
|
|
149
|
+
source: integrationId ? 'integration' : 'manual',
|
|
150
|
+
...(integrationId && { integrationId }),
|
|
116
151
|
...(description !== undefined && { description }),
|
|
117
152
|
};
|
|
118
153
|
await Project.update({ _id: req.params.id }, { $push: { dbConnections: connection } });
|
|
@@ -125,9 +160,6 @@ function dbConnectionRouter(config) {
|
|
|
125
160
|
// ─── Delete DB connection ─────────────────────────────────────────────────
|
|
126
161
|
router.delete('/projects/:id/connections/db/:connId', async (req, res) => {
|
|
127
162
|
try {
|
|
128
|
-
if ((0, builtinAccess_1.isBuiltinDb)(req.params.connId, config)) {
|
|
129
|
-
return res.status(403).json({ message: 'Cannot delete a built-in database connection' });
|
|
130
|
-
}
|
|
131
163
|
const auth = req.auth;
|
|
132
164
|
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
133
165
|
return res.status(403).json({ message: 'Permission denied' });
|
|
@@ -137,9 +169,12 @@ function dbConnectionRouter(config) {
|
|
|
137
169
|
return res.status(404).json({ message: 'Project not found' });
|
|
138
170
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
139
171
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
140
|
-
const
|
|
141
|
-
if (!
|
|
172
|
+
const conn = findDbConnection(project, req.params.connId);
|
|
173
|
+
if (!conn)
|
|
142
174
|
return res.status(404).json({ message: 'DB connection not found' });
|
|
175
|
+
if (conn.source === 'builtin') {
|
|
176
|
+
return res.status(403).json({ message: 'Cannot delete a built-in database connection' });
|
|
177
|
+
}
|
|
143
178
|
await Project.update({ _id: req.params.id }, { $pull: { dbConnections: { id: req.params.connId } } });
|
|
144
179
|
return res.status(200).json({ message: 'DB connection deleted successfully' });
|
|
145
180
|
}
|
|
@@ -156,11 +191,15 @@ function dbConnectionRouter(config) {
|
|
|
156
191
|
return res.status(404).json({ message: 'Project not found' });
|
|
157
192
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
158
193
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
159
|
-
|
|
160
|
-
|
|
194
|
+
const conn = findDbConnection(project, req.params.connId);
|
|
195
|
+
if (!conn)
|
|
196
|
+
return res.status(404).json({ message: 'DB connection not found' });
|
|
197
|
+
if (conn.source === 'builtin') {
|
|
198
|
+
const builtinId = conn.integrationId;
|
|
199
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
|
|
161
200
|
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
162
201
|
}
|
|
163
|
-
const internal = resolveBuiltinConnection(
|
|
202
|
+
const internal = resolveBuiltinConnection(builtinId, config);
|
|
164
203
|
if ('error' in internal)
|
|
165
204
|
return res.status(internal.status).json({ message: internal.error });
|
|
166
205
|
const tables = await (0, dbConnection_1.listTablesInternal)(internal.client, internal.dbType, req.params.id);
|
|
@@ -191,11 +230,15 @@ function dbConnectionRouter(config) {
|
|
|
191
230
|
const schema = req.body;
|
|
192
231
|
if (!schema.tableName)
|
|
193
232
|
return res.status(400).json({ message: 'tableName is required' });
|
|
194
|
-
|
|
195
|
-
|
|
233
|
+
const conn = findDbConnection(project, req.params.connId);
|
|
234
|
+
if (!conn)
|
|
235
|
+
return res.status(404).json({ message: 'DB connection not found' });
|
|
236
|
+
if (conn.source === 'builtin') {
|
|
237
|
+
const builtinId = conn.integrationId;
|
|
238
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
|
|
196
239
|
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
197
240
|
}
|
|
198
|
-
const internal = resolveBuiltinConnection(
|
|
241
|
+
const internal = resolveBuiltinConnection(builtinId, config);
|
|
199
242
|
if ('error' in internal)
|
|
200
243
|
return res.status(internal.status).json({ message: internal.error });
|
|
201
244
|
if (internal.dbType === 'sql' && (!Array.isArray(schema.columns) || schema.columns.length === 0)) {
|
|
@@ -229,11 +272,15 @@ function dbConnectionRouter(config) {
|
|
|
229
272
|
return res.status(404).json({ message: 'Project not found' });
|
|
230
273
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
231
274
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
232
|
-
|
|
233
|
-
|
|
275
|
+
const conn = findDbConnection(project, req.params.connId);
|
|
276
|
+
if (!conn)
|
|
277
|
+
return res.status(404).json({ message: 'DB connection not found' });
|
|
278
|
+
if (conn.source === 'builtin') {
|
|
279
|
+
const builtinId = conn.integrationId;
|
|
280
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
|
|
234
281
|
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
235
282
|
}
|
|
236
|
-
const internal = resolveBuiltinConnection(
|
|
283
|
+
const internal = resolveBuiltinConnection(builtinId, config);
|
|
237
284
|
if ('error' in internal)
|
|
238
285
|
return res.status(internal.status).json({ message: internal.error });
|
|
239
286
|
await (0, dbConnection_1.deleteTableOrCollectionInternal)(internal.client, internal.dbType, req.params.tableName, req.params.id);
|
|
@@ -260,11 +307,15 @@ function dbConnectionRouter(config) {
|
|
|
260
307
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
261
308
|
const limit = Math.min(parseInt(String(req.query.limit ?? 50), 10), 500);
|
|
262
309
|
const offset = Math.max(parseInt(String(req.query.offset ?? 0), 10), 0);
|
|
263
|
-
|
|
264
|
-
|
|
310
|
+
const conn = findDbConnection(project, req.params.connId);
|
|
311
|
+
if (!conn)
|
|
312
|
+
return res.status(404).json({ message: 'DB connection not found' });
|
|
313
|
+
if (conn.source === 'builtin') {
|
|
314
|
+
const builtinId = conn.integrationId;
|
|
315
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
|
|
265
316
|
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
266
317
|
}
|
|
267
|
-
const internal = resolveBuiltinConnection(
|
|
318
|
+
const internal = resolveBuiltinConnection(builtinId, config);
|
|
268
319
|
if ('error' in internal)
|
|
269
320
|
return res.status(internal.status).json({ message: internal.error });
|
|
270
321
|
const records = await (0, dbConnection_1.queryRecordsInternal)(internal.client, internal.dbType, req.params.tableName, limit, offset, req.params.id);
|
|
@@ -296,11 +347,15 @@ function dbConnectionRouter(config) {
|
|
|
296
347
|
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
297
348
|
return res.status(400).json({ message: 'Request body must be a JSON object' });
|
|
298
349
|
}
|
|
299
|
-
|
|
300
|
-
|
|
350
|
+
const conn = findDbConnection(project, req.params.connId);
|
|
351
|
+
if (!conn)
|
|
352
|
+
return res.status(404).json({ message: 'DB connection not found' });
|
|
353
|
+
if (conn.source === 'builtin') {
|
|
354
|
+
const builtinId = conn.integrationId;
|
|
355
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
|
|
301
356
|
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
302
357
|
}
|
|
303
|
-
const internal = resolveBuiltinConnection(
|
|
358
|
+
const internal = resolveBuiltinConnection(builtinId, config);
|
|
304
359
|
if ('error' in internal)
|
|
305
360
|
return res.status(internal.status).json({ message: internal.error });
|
|
306
361
|
const inserted = await (0, dbConnection_1.insertRecordInternal)(internal.client, internal.dbType, req.params.tableName, record, req.params.id);
|
|
@@ -331,11 +386,15 @@ function dbConnectionRouter(config) {
|
|
|
331
386
|
if (!updates || typeof updates !== 'object' || Array.isArray(updates)) {
|
|
332
387
|
return res.status(400).json({ message: 'Request body must be a JSON object' });
|
|
333
388
|
}
|
|
334
|
-
|
|
335
|
-
|
|
389
|
+
const conn = findDbConnection(project, req.params.connId);
|
|
390
|
+
if (!conn)
|
|
391
|
+
return res.status(404).json({ message: 'DB connection not found' });
|
|
392
|
+
if (conn.source === 'builtin') {
|
|
393
|
+
const builtinId = conn.integrationId;
|
|
394
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
|
|
336
395
|
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
337
396
|
}
|
|
338
|
-
const internal = resolveBuiltinConnection(
|
|
397
|
+
const internal = resolveBuiltinConnection(builtinId, config);
|
|
339
398
|
if ('error' in internal)
|
|
340
399
|
return res.status(internal.status).json({ message: internal.error });
|
|
341
400
|
const updated = await (0, dbConnection_1.updateRecordInternal)(internal.client, internal.dbType, req.params.tableName, req.params.recordId, updates, req.params.id);
|
|
@@ -362,11 +421,15 @@ function dbConnectionRouter(config) {
|
|
|
362
421
|
return res.status(404).json({ message: 'Project not found' });
|
|
363
422
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
364
423
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
365
|
-
|
|
366
|
-
|
|
424
|
+
const conn = findDbConnection(project, req.params.connId);
|
|
425
|
+
if (!conn)
|
|
426
|
+
return res.status(404).json({ message: 'DB connection not found' });
|
|
427
|
+
if (conn.source === 'builtin') {
|
|
428
|
+
const builtinId = conn.integrationId;
|
|
429
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
|
|
367
430
|
return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
|
|
368
431
|
}
|
|
369
|
-
const internal = resolveBuiltinConnection(
|
|
432
|
+
const internal = resolveBuiltinConnection(builtinId, config);
|
|
370
433
|
if ('error' in internal)
|
|
371
434
|
return res.status(internal.status).json({ message: internal.error });
|
|
372
435
|
await (0, dbConnection_1.deleteRecordInternal)(internal.client, internal.dbType, req.params.tableName, req.params.recordId, req.params.id);
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.projectRouter = projectRouter;
|
|
7
4
|
const express_1 = require("express");
|
|
8
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
9
5
|
const manager_1 = require("../../models/manager");
|
|
10
6
|
const auth_1 = require("../../utils/auth");
|
|
11
7
|
const connectionManager_1 = require("../../adapters/database/connectionManager");
|
|
12
8
|
const sql_1 = require("../../adapters/database/sql");
|
|
13
9
|
const sanitize_1 = require("../../utils/sanitize");
|
|
14
|
-
const
|
|
10
|
+
const projectIntegration_1 = require("../../services/projectIntegration");
|
|
15
11
|
function projectRouter(config) {
|
|
16
12
|
const router = (0, express_1.Router)();
|
|
17
13
|
const Project = (0, manager_1.getModel)('projects');
|
|
@@ -98,47 +94,14 @@ function projectRouter(config) {
|
|
|
98
94
|
const dbConnections = [];
|
|
99
95
|
const storageConnections = [];
|
|
100
96
|
if (Array.isArray(integrationIds)) {
|
|
101
|
-
const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
|
|
102
97
|
for (const id of integrationIds) {
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
id: crypto_1.default.randomUUID(), name: id, dbType: db.type === 'sql' ? 'postgresql' : 'mongodb',
|
|
111
|
-
credentialId: '', projectId: '', createdAt: now, isBuiltin: true, integrationId: id, source: 'builtin',
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
else if ((0, builtinAccess_1.isBuiltinStorageId)(id, config)) {
|
|
115
|
-
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, id, config, 'storage'))) {
|
|
116
|
-
return res.status(403).json({ message: `Access to built-in storage '${id}' is not permitted` });
|
|
117
|
-
}
|
|
118
|
-
const provider = config.storageProviders.find(p => p.id === id);
|
|
119
|
-
storageConnections.push({
|
|
120
|
-
id: crypto_1.default.randomUUID(), name: id, type: provider.type,
|
|
121
|
-
credentialId: '', projectId: '', createdAt: now, isBuiltin: true, integrationId: id, source: 'builtin',
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
const integration = await UserIntegrationModel.findOne({ id, userId: auth.userId });
|
|
126
|
-
if (!integration) {
|
|
127
|
-
return res.status(403).json({ message: `Integration '${id}' is not valid or does not belong to you` });
|
|
128
|
-
}
|
|
129
|
-
if (integration.kind === 'database') {
|
|
130
|
-
dbConnections.push({
|
|
131
|
-
id: crypto_1.default.randomUUID(), name: integration.name, dbType: integration.provider,
|
|
132
|
-
credentialId: '', projectId: '', createdAt: now, isBuiltin: false, integrationId: id, source: 'integration',
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
storageConnections.push({
|
|
137
|
-
id: crypto_1.default.randomUUID(), name: integration.name, type: integration.provider,
|
|
138
|
-
credentialId: '', projectId: '', createdAt: now, isBuiltin: false, integrationId: id, source: 'integration',
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
98
|
+
const result = await (0, projectIntegration_1.classifyIntegrationId)(id, auth, config, '');
|
|
99
|
+
if ('error' in result)
|
|
100
|
+
return res.status(result.status).json({ message: result.error });
|
|
101
|
+
if (result.kind === 'database')
|
|
102
|
+
dbConnections.push(result.connection);
|
|
103
|
+
else
|
|
104
|
+
storageConnections.push(result.connection);
|
|
142
105
|
}
|
|
143
106
|
}
|
|
144
107
|
let newProject = await Project.create({
|
|
@@ -430,5 +393,68 @@ function projectRouter(config) {
|
|
|
430
393
|
res.status(500).json({ message: 'Failed to update project origins', details: err.message });
|
|
431
394
|
}
|
|
432
395
|
});
|
|
396
|
+
// ─── Add an integration to an existing project ───────────────────────────
|
|
397
|
+
router.post('/projects/:id/integrations', async (req, res) => {
|
|
398
|
+
try {
|
|
399
|
+
const auth = req.auth;
|
|
400
|
+
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
401
|
+
return res.status(403).json({ message: 'Permission denied' });
|
|
402
|
+
}
|
|
403
|
+
const projectId = req.params.id;
|
|
404
|
+
const { integrationId } = req.body;
|
|
405
|
+
if (!integrationId)
|
|
406
|
+
return res.status(400).json({ message: 'integrationId is required' });
|
|
407
|
+
const project = await Project.findOne({ _id: projectId });
|
|
408
|
+
if (!project)
|
|
409
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
410
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
411
|
+
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
412
|
+
const alreadyConnected = [...(project.dbConnections ?? []), ...(project.storageConnections ?? [])]
|
|
413
|
+
.some((c) => c.integrationId === integrationId);
|
|
414
|
+
if (alreadyConnected)
|
|
415
|
+
return res.status(400).json({ message: 'This integration is already connected to the project' });
|
|
416
|
+
const result = await (0, projectIntegration_1.classifyIntegrationId)(integrationId, auth, config, projectId);
|
|
417
|
+
if ('error' in result)
|
|
418
|
+
return res.status(result.status).json({ message: result.error });
|
|
419
|
+
const updated = result.kind === 'database'
|
|
420
|
+
? await Project.update({ _id: projectId }, { $push: { dbConnections: result.connection } })
|
|
421
|
+
: await Project.update({ _id: projectId }, { $push: { storageConnections: result.connection } });
|
|
422
|
+
if (!updated)
|
|
423
|
+
return res.status(404).json({ message: 'Project not found or not updated' });
|
|
424
|
+
res.status(201).json({ data: result.connection, project: (0, sanitize_1.sanitizeProject)(updated) });
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
res.status(500).json({ message: 'Failed to add integration', details: err.message });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
// ─── Remove an integration from a project ────────────────────────────────
|
|
431
|
+
router.delete('/projects/:id/integrations/:integrationId', async (req, res) => {
|
|
432
|
+
try {
|
|
433
|
+
const auth = req.auth;
|
|
434
|
+
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
435
|
+
return res.status(403).json({ message: 'Permission denied' });
|
|
436
|
+
}
|
|
437
|
+
const projectId = req.params.id;
|
|
438
|
+
const { integrationId } = req.params;
|
|
439
|
+
const project = await Project.findOne({ _id: projectId });
|
|
440
|
+
if (!project)
|
|
441
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
442
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
443
|
+
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
444
|
+
const dbConn = (project.dbConnections ?? []).find((c) => c.integrationId === integrationId);
|
|
445
|
+
const storageConn = (project.storageConnections ?? []).find((c) => c.integrationId === integrationId);
|
|
446
|
+
if (!dbConn && !storageConn)
|
|
447
|
+
return res.status(404).json({ message: 'Integration not connected to this project' });
|
|
448
|
+
const updated = dbConn
|
|
449
|
+
? await Project.update({ _id: projectId }, { $pull: { dbConnections: { id: dbConn.id } } })
|
|
450
|
+
: await Project.update({ _id: projectId }, { $pull: { storageConnections: { id: storageConn.id } } });
|
|
451
|
+
if (!updated)
|
|
452
|
+
return res.status(404).json({ message: 'Project not found or not updated' });
|
|
453
|
+
res.status(200).json({ message: 'Integration removed', project: (0, sanitize_1.sanitizeProject)(updated) });
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
res.status(500).json({ message: 'Failed to remove integration', details: err.message });
|
|
457
|
+
}
|
|
458
|
+
});
|
|
433
459
|
return router;
|
|
434
460
|
}
|
|
@@ -14,6 +14,7 @@ const connectionManager_1 = require("../../adapters/database/connectionManager")
|
|
|
14
14
|
const auth_1 = require("../../utils/auth");
|
|
15
15
|
const encryption_1 = require("../../utils/encryption");
|
|
16
16
|
const builtinAccess_1 = require("../../utils/builtinAccess");
|
|
17
|
+
const userIntegration_1 = require("../../services/userIntegration");
|
|
17
18
|
const mongodb_1 = require("mongodb");
|
|
18
19
|
const crypto_1 = __importDefault(require("crypto"));
|
|
19
20
|
const VALID_CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
|
|
@@ -51,26 +52,50 @@ function storageRouter(config) {
|
|
|
51
52
|
const router = (0, express_1.Router)();
|
|
52
53
|
const adapter = config.adapter || (0, storage_1.createStorageProvider)(config.storageProviders);
|
|
53
54
|
const Project = (0, manager_1.getModel)('projects');
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
// Builtin files may be stamped one of two ways depending on when they were uploaded:
|
|
65
|
+
// legacy docs (pre-per-row-ids) used the builtin's config id (e.g. 'builtin') or have no
|
|
66
|
+
// storageConnectionId at all; current uploads use the row's own generated id (connId).
|
|
56
67
|
const connMatch = isBuiltin
|
|
57
|
-
? { $or: [{ storageConnectionId: connId }, { storageConnectionId: { $exists: false } }] }
|
|
68
|
+
? { $or: [{ storageConnectionId: connId }, { storageConnectionId: conn.integrationId }, { storageConnectionId: { $exists: false } }] }
|
|
58
69
|
: { storageConnectionId: connId };
|
|
59
70
|
return { projectId, ...connMatch, ...(extra ?? {}) };
|
|
60
71
|
}
|
|
61
72
|
async function resolveConnAdapter(connId, project, auth) {
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
const conn = findStorageConn(project, connId);
|
|
74
|
+
if (!conn)
|
|
75
|
+
return { error: 'Storage connection not found', status: 404 };
|
|
76
|
+
if (conn.source === 'builtin') {
|
|
77
|
+
const builtinId = conn.integrationId;
|
|
78
|
+
if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'storage'))) {
|
|
64
79
|
return { error: 'Access to this built-in storage is not permitted', status: 403 };
|
|
65
80
|
}
|
|
66
|
-
const provider = config.storageProviders.find(p => p.id ===
|
|
81
|
+
const provider = config.storageProviders.find(p => p.id === builtinId);
|
|
67
82
|
if (!provider)
|
|
68
83
|
return { error: 'Storage provider not found', status: 404 };
|
|
69
84
|
return provider.type === 's3' ? new s3_1.S3Adapter(provider.config) : adapter;
|
|
70
85
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
86
|
+
if (conn.source === 'integration') {
|
|
87
|
+
if (!conn.integrationId)
|
|
88
|
+
return { error: 'Connection is missing its integrationId', status: 500 };
|
|
89
|
+
try {
|
|
90
|
+
const s3Config = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId);
|
|
91
|
+
if (!s3Config)
|
|
92
|
+
return { error: 'Integration not found', status: 400 };
|
|
93
|
+
return new s3_1.S3Adapter(s3Config);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
74
99
|
if (!(0, encryption_1.isEncryptionKeySet)())
|
|
75
100
|
return { error: 'Encryption key not set', status: 500 };
|
|
76
101
|
const credential = project.credentials?.find((c) => c.id === conn.credentialId);
|
|
@@ -354,7 +379,7 @@ function storageRouter(config) {
|
|
|
354
379
|
const collection = getRawFilesCollection();
|
|
355
380
|
if (!collection)
|
|
356
381
|
return res.status(500).json({ message: 'Database not available' });
|
|
357
|
-
const files = await collection.find(connFilter(projectId, connId)).sort({ createdAt: -1 }).limit(12).toArray();
|
|
382
|
+
const files = await collection.find(connFilter(projectId, connId, project)).sort({ createdAt: -1 }).limit(12).toArray();
|
|
358
383
|
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
|
|
359
384
|
res.json({ data });
|
|
360
385
|
}
|
|
@@ -380,7 +405,7 @@ function storageRouter(config) {
|
|
|
380
405
|
const collection = getRawFilesCollection();
|
|
381
406
|
if (!collection)
|
|
382
407
|
return res.status(500).json({ message: 'Database not available' });
|
|
383
|
-
const files = await collection.find(connFilter(projectId, connId, { category })).sort({ createdAt: -1 }).toArray();
|
|
408
|
+
const files = await collection.find(connFilter(projectId, connId, project, { category })).sort({ createdAt: -1 }).toArray();
|
|
384
409
|
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
|
|
385
410
|
res.json({ data });
|
|
386
411
|
}
|
|
@@ -404,7 +429,7 @@ function storageRouter(config) {
|
|
|
404
429
|
const collection = getRawFilesCollection();
|
|
405
430
|
if (!collection)
|
|
406
431
|
return res.status(500).json({ message: 'Database not available' });
|
|
407
|
-
const fileDoc = await collection.findOne(connFilter(projectId, connId, { fileId }));
|
|
432
|
+
const fileDoc = await collection.findOne(connFilter(projectId, connId, project, { fileId }));
|
|
408
433
|
if (!fileDoc)
|
|
409
434
|
return res.status(404).json({ message: 'File not found' });
|
|
410
435
|
if (!validateContentType(contentType, fileName, fileDoc.category)) {
|
|
@@ -417,7 +442,7 @@ function storageRouter(config) {
|
|
|
417
442
|
return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
|
|
418
443
|
}
|
|
419
444
|
const url = await connAdapter.getPresignedUploadUrl(fileDoc.storageKey, contentType);
|
|
420
|
-
await collection.updateOne(connFilter(projectId, connId, { fileId }), { $set: { contentType, updatedAt: new Date() } });
|
|
445
|
+
await collection.updateOne(connFilter(projectId, connId, project, { fileId }), { $set: { contentType, updatedAt: new Date() } });
|
|
421
446
|
res.json({ url, storageKey: fileDoc.storageKey, fileId });
|
|
422
447
|
}
|
|
423
448
|
catch (err) {
|
|
@@ -440,7 +465,7 @@ function storageRouter(config) {
|
|
|
440
465
|
const collection = getRawFilesCollection();
|
|
441
466
|
if (!collection)
|
|
442
467
|
return res.status(500).json({ message: 'Database not available' });
|
|
443
|
-
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { fileId }), { $set: { displayName } }, { returnDocument: 'after' });
|
|
468
|
+
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, project, { fileId }), { $set: { displayName } }, { returnDocument: 'after' });
|
|
444
469
|
if (!result)
|
|
445
470
|
return res.status(404).json({ message: 'File not found' });
|
|
446
471
|
res.json({ message: 'File renamed successfully', file: result });
|
|
@@ -464,12 +489,12 @@ function storageRouter(config) {
|
|
|
464
489
|
const collection = getRawFilesCollection();
|
|
465
490
|
if (!collection)
|
|
466
491
|
return res.status(500).json({ message: 'Database not available' });
|
|
467
|
-
const fileDoc = await collection.findOne(connFilter(projectId, connId, { fileId }));
|
|
492
|
+
const fileDoc = await collection.findOne(connFilter(projectId, connId, project, { fileId }));
|
|
468
493
|
if (!fileDoc)
|
|
469
494
|
return res.status(404).json({ message: 'File not found' });
|
|
470
495
|
if (connAdapter.deleteFile)
|
|
471
496
|
await connAdapter.deleteFile(fileDoc.storageKey);
|
|
472
|
-
await collection.deleteOne(connFilter(projectId, connId, { fileId }));
|
|
497
|
+
await collection.deleteOne(connFilter(projectId, connId, project, { fileId }));
|
|
473
498
|
res.json({ message: 'File deleted successfully' });
|
|
474
499
|
}
|
|
475
500
|
catch (err) {
|
|
@@ -494,7 +519,7 @@ function storageRouter(config) {
|
|
|
494
519
|
? { parentId: null }
|
|
495
520
|
: { parentId };
|
|
496
521
|
const folders = await collection
|
|
497
|
-
.find(connFilter(projectId, connId, parentFilter))
|
|
522
|
+
.find(connFilter(projectId, connId, project, parentFilter))
|
|
498
523
|
.sort({ name: 1 })
|
|
499
524
|
.toArray();
|
|
500
525
|
res.json({
|
|
@@ -525,7 +550,7 @@ function storageRouter(config) {
|
|
|
525
550
|
const collection = getRawFoldersCollection();
|
|
526
551
|
if (!collection)
|
|
527
552
|
return res.status(500).json({ message: 'Database not available' });
|
|
528
|
-
const f = await collection.findOne(connFilter(projectId, connId, { folderId }));
|
|
553
|
+
const f = await collection.findOne(connFilter(projectId, connId, project, { folderId }));
|
|
529
554
|
if (!f)
|
|
530
555
|
return res.status(404).json({ message: 'Folder not found' });
|
|
531
556
|
res.json({
|
|
@@ -557,8 +582,9 @@ function storageRouter(config) {
|
|
|
557
582
|
return res.status(404).json({ message: 'Project not found' });
|
|
558
583
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
559
584
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
560
|
-
const
|
|
561
|
-
|
|
585
|
+
const conn = findStorageConn(project, connId);
|
|
586
|
+
const isBuiltin = conn?.source === 'builtin';
|
|
587
|
+
if (isBuiltin && !(await (0, builtinAccess_1.assertBuiltinAccess)(auth, conn.integrationId, config, 'storage'))) {
|
|
562
588
|
return res.status(403).json({ message: 'Access to this built-in storage is not permitted' });
|
|
563
589
|
}
|
|
564
590
|
const collection = getRawFoldersCollection();
|
|
@@ -610,7 +636,7 @@ function storageRouter(config) {
|
|
|
610
636
|
const collection = getRawFoldersCollection();
|
|
611
637
|
if (!collection)
|
|
612
638
|
return res.status(500).json({ message: 'Database not available' });
|
|
613
|
-
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { folderId }), { $set: { name: name.trim(), updatedAt: new Date() } }, { returnDocument: 'after' });
|
|
639
|
+
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, project, { folderId }), { $set: { name: name.trim(), updatedAt: new Date() } }, { returnDocument: 'after' });
|
|
614
640
|
if (!result)
|
|
615
641
|
return res.status(404).json({ message: 'Folder not found' });
|
|
616
642
|
res.json({
|
|
@@ -644,7 +670,7 @@ function storageRouter(config) {
|
|
|
644
670
|
const collection = getRawFoldersCollection();
|
|
645
671
|
if (!collection)
|
|
646
672
|
return res.status(500).json({ message: 'Database not available' });
|
|
647
|
-
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { folderId }), { $set: { parentId: newParentId ?? null, updatedAt: new Date() } }, { returnDocument: 'after' });
|
|
673
|
+
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, project, { folderId }), { $set: { parentId: newParentId ?? null, updatedAt: new Date() } }, { returnDocument: 'after' });
|
|
648
674
|
if (!result)
|
|
649
675
|
return res.status(404).json({ message: 'Folder not found' });
|
|
650
676
|
res.json({
|
|
@@ -674,15 +700,15 @@ function storageRouter(config) {
|
|
|
674
700
|
const foldersCollection = getRawFoldersCollection();
|
|
675
701
|
if (!foldersCollection)
|
|
676
702
|
return res.status(500).json({ message: 'Database not available' });
|
|
677
|
-
const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, { folderId }));
|
|
703
|
+
const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, project, { folderId }));
|
|
678
704
|
if (!folderDoc)
|
|
679
705
|
return res.status(404).json({ message: 'Folder not found' });
|
|
680
706
|
const filesCollection = getRawFilesCollection();
|
|
681
707
|
if (filesCollection) {
|
|
682
|
-
await filesCollection.updateMany(connFilter(projectId, connId, { folderId }), { $set: { folderId: folderDoc.parentId ?? null } });
|
|
708
|
+
await filesCollection.updateMany(connFilter(projectId, connId, project, { folderId }), { $set: { folderId: folderDoc.parentId ?? null } });
|
|
683
709
|
}
|
|
684
|
-
await foldersCollection.updateMany(connFilter(projectId, connId, { parentId: folderId }), { $set: { parentId: folderDoc.parentId ?? null } });
|
|
685
|
-
await foldersCollection.deleteOne(connFilter(projectId, connId, { folderId }));
|
|
710
|
+
await foldersCollection.updateMany(connFilter(projectId, connId, project, { parentId: folderId }), { $set: { parentId: folderDoc.parentId ?? null } });
|
|
711
|
+
await foldersCollection.deleteOne(connFilter(projectId, connId, project, { folderId }));
|
|
686
712
|
res.json({ message: 'Folder deleted successfully' });
|
|
687
713
|
}
|
|
688
714
|
catch (err) {
|
|
@@ -702,7 +728,7 @@ function storageRouter(config) {
|
|
|
702
728
|
if (folderId != null) {
|
|
703
729
|
const foldersCollection = getRawFoldersCollection();
|
|
704
730
|
if (foldersCollection) {
|
|
705
|
-
const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, { folderId }));
|
|
731
|
+
const folderDoc = await foldersCollection.findOne(connFilter(projectId, connId, project, { folderId }));
|
|
706
732
|
if (!folderDoc)
|
|
707
733
|
return res.status(404).json({ message: 'Folder not found' });
|
|
708
734
|
}
|
|
@@ -710,7 +736,7 @@ function storageRouter(config) {
|
|
|
710
736
|
const filesCollection = getRawFilesCollection();
|
|
711
737
|
if (!filesCollection)
|
|
712
738
|
return res.status(500).json({ message: 'Database not available' });
|
|
713
|
-
const result = await filesCollection.findOneAndUpdate(connFilter(projectId, connId, { fileId }), { $set: { folderId: folderId ?? null } }, { returnDocument: 'after' });
|
|
739
|
+
const result = await filesCollection.findOneAndUpdate(connFilter(projectId, connId, project, { fileId }), { $set: { folderId: folderId ?? null } }, { returnDocument: 'after' });
|
|
714
740
|
if (!result)
|
|
715
741
|
return res.status(404).json({ message: 'File not found' });
|
|
716
742
|
res.json({ message: 'File moved successfully', file: result });
|
|
@@ -41,9 +41,15 @@ function storageConnectionRouter(config) {
|
|
|
41
41
|
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
42
42
|
return res.status(403).json({ message: 'Permission denied' });
|
|
43
43
|
}
|
|
44
|
-
const { name, type, credentialId, description } = req.body;
|
|
45
|
-
if (!name || !type
|
|
46
|
-
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' });
|
|
47
53
|
}
|
|
48
54
|
if (!VALID_STORAGE_TYPES.includes(type)) {
|
|
49
55
|
return res.status(400).json({ message: `type must be one of: ${VALID_STORAGE_TYPES.join(', ')}` });
|
|
@@ -53,16 +59,28 @@ function storageConnectionRouter(config) {
|
|
|
53
59
|
return res.status(404).json({ message: 'Project not found' });
|
|
54
60
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
55
61
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
}
|
|
59
75
|
const connection = {
|
|
60
76
|
id: new mongodb_1.ObjectId().toHexString(),
|
|
61
77
|
name,
|
|
62
78
|
type,
|
|
63
|
-
credentialId,
|
|
79
|
+
credentialId: credentialId ?? '',
|
|
64
80
|
projectId: req.params.id,
|
|
65
81
|
createdAt: new Date(),
|
|
82
|
+
source: integrationId ? 'integration' : 'manual',
|
|
83
|
+
...(integrationId && { integrationId }),
|
|
66
84
|
...(description !== undefined && { description }),
|
|
67
85
|
};
|
|
68
86
|
await Project.update({ _id: req.params.id }, { $push: { storageConnections: connection } });
|
|
@@ -76,9 +94,6 @@ function storageConnectionRouter(config) {
|
|
|
76
94
|
router.delete('/projects/:id/connections/storage/:connId', async (req, res) => {
|
|
77
95
|
try {
|
|
78
96
|
const { connId } = req.params;
|
|
79
|
-
if ((0, builtinAccess_1.isBuiltinStorageId)(connId, config)) {
|
|
80
|
-
return res.status(403).json({ message: 'Cannot delete a built-in storage connection' });
|
|
81
|
-
}
|
|
82
97
|
const auth = req.auth;
|
|
83
98
|
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
84
99
|
return res.status(403).json({ message: 'Permission denied' });
|
|
@@ -88,9 +103,12 @@ function storageConnectionRouter(config) {
|
|
|
88
103
|
return res.status(404).json({ message: 'Project not found' });
|
|
89
104
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
90
105
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
91
|
-
const
|
|
92
|
-
if (!
|
|
106
|
+
const conn = project.storageConnections?.find((c) => c.id === connId);
|
|
107
|
+
if (!conn)
|
|
93
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
|
+
}
|
|
94
112
|
await Project.update({ _id: req.params.id }, { $pull: { storageConnections: { id: connId } } });
|
|
95
113
|
return res.status(200).json({ message: 'Storage connection deleted successfully' });
|
|
96
114
|
}
|
|
@@ -22,11 +22,11 @@ function userIntegrationRouter(config) {
|
|
|
22
22
|
// requiredPlan is not populated here — canUseBuiltin only returns a boolean today,
|
|
23
23
|
// so there's no plan-level detail to surface without a hook-signature change.
|
|
24
24
|
const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, db.id, config, 'database');
|
|
25
|
-
pool.push({ id: db.id, kind: 'database', name: db.
|
|
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
26
|
}
|
|
27
27
|
for (const provider of config.storageProviders ?? []) {
|
|
28
28
|
const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, provider.id, config, 'storage');
|
|
29
|
-
pool.push({ id: provider.id, kind: 'storage', name: provider.
|
|
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
30
|
}
|
|
31
31
|
res.json({ data: pool });
|
|
32
32
|
}
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.backfillBuiltinConnections = backfillBuiltinConnections;
|
|
7
7
|
const crypto_1 = __importDefault(require("crypto"));
|
|
8
8
|
const manager_1 = require("../models/manager");
|
|
9
|
+
const builtinAccess_1 = require("../utils/builtinAccess");
|
|
9
10
|
// Pre-BYOC, every project appeared connected to every configured builtin because
|
|
10
11
|
// storageConnection.ts/dbConnection.ts SYNTHESIZED builtins at read time instead of
|
|
11
12
|
// persisting them (see storageConnection.ts's old GET handler). Now that connections must
|
|
@@ -26,7 +27,7 @@ async function backfillBuiltinConnections(config) {
|
|
|
26
27
|
const now = new Date();
|
|
27
28
|
const dbConnections = (config.databases ?? []).map(db => ({
|
|
28
29
|
id: crypto_1.default.randomUUID(),
|
|
29
|
-
name: db.
|
|
30
|
+
name: builtinAccess_1.BUILTIN_DB_DISPLAY[db.type] ?? db.type,
|
|
30
31
|
dbType: db.type === 'sql' ? 'postgresql' : 'mongodb',
|
|
31
32
|
credentialId: '',
|
|
32
33
|
projectId: project.id,
|
|
@@ -37,7 +38,7 @@ async function backfillBuiltinConnections(config) {
|
|
|
37
38
|
}));
|
|
38
39
|
const storageConnections = (config.storageProviders ?? []).map(provider => ({
|
|
39
40
|
id: crypto_1.default.randomUUID(),
|
|
40
|
-
name: provider.
|
|
41
|
+
name: builtinAccess_1.BUILTIN_STORAGE_DISPLAY[provider.type] ?? provider.type,
|
|
41
42
|
type: provider.type,
|
|
42
43
|
credentialId: '',
|
|
43
44
|
projectId: project.id,
|
|
@@ -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
|
+
}
|
|
@@ -14,3 +14,4 @@ export declare function updateUserIntegration(userId: string, integrationId: str
|
|
|
14
14
|
}): Promise<UserIntegration | null>;
|
|
15
15
|
export declare function deleteUserIntegration(userId: string, integrationId: string): Promise<boolean>;
|
|
16
16
|
export declare function getDecryptedIntegrationCredential(userId: string, integrationId: string): Promise<unknown | null>;
|
|
17
|
+
export declare function getDecryptedIntegrationCredentialById(integrationId: string): Promise<unknown | null>;
|
|
@@ -8,6 +8,7 @@ exports.createUserIntegration = createUserIntegration;
|
|
|
8
8
|
exports.updateUserIntegration = updateUserIntegration;
|
|
9
9
|
exports.deleteUserIntegration = deleteUserIntegration;
|
|
10
10
|
exports.getDecryptedIntegrationCredential = getDecryptedIntegrationCredential;
|
|
11
|
+
exports.getDecryptedIntegrationCredentialById = getDecryptedIntegrationCredentialById;
|
|
11
12
|
const crypto_1 = __importDefault(require("crypto"));
|
|
12
13
|
const manager_1 = require("../models/manager");
|
|
13
14
|
const encryption_1 = require("../utils/encryption");
|
|
@@ -63,6 +64,18 @@ async function deleteUserIntegration(userId, integrationId) {
|
|
|
63
64
|
async function getDecryptedIntegrationCredential(userId, integrationId) {
|
|
64
65
|
const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
|
|
65
66
|
const integration = await UserIntegrationModel.findOne({ id: integrationId, userId });
|
|
67
|
+
return decryptIntegration(integration);
|
|
68
|
+
}
|
|
69
|
+
// Used when resolving a project connection's `integrationId` at connection-use time — the
|
|
70
|
+
// project already validated ownership when the connection was created (see project.ts /
|
|
71
|
+
// dbConnection.ts POST), so there's no userId to filter by here, only the integrationId
|
|
72
|
+
// stored on the connection itself.
|
|
73
|
+
async function getDecryptedIntegrationCredentialById(integrationId) {
|
|
74
|
+
const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
|
|
75
|
+
const integration = await UserIntegrationModel.findOne({ id: integrationId });
|
|
76
|
+
return decryptIntegration(integration);
|
|
77
|
+
}
|
|
78
|
+
async function decryptIntegration(integration) {
|
|
66
79
|
if (!integration)
|
|
67
80
|
return null;
|
|
68
81
|
if (!(0, encryption_1.isEncryptionKeySet)()) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface StorageConnection {
|
|
|
18
18
|
description?: string;
|
|
19
19
|
isBuiltin?: boolean;
|
|
20
20
|
integrationId?: string;
|
|
21
|
-
source?: 'builtin' | 'integration';
|
|
21
|
+
source?: 'builtin' | 'integration' | 'manual';
|
|
22
22
|
available?: boolean;
|
|
23
23
|
}
|
|
24
24
|
export type StorageProvider = {
|
|
@@ -159,7 +159,7 @@ export interface DbConnection {
|
|
|
159
159
|
description?: string;
|
|
160
160
|
isBuiltin?: boolean;
|
|
161
161
|
integrationId?: string;
|
|
162
|
-
source?: 'builtin' | 'integration';
|
|
162
|
+
source?: 'builtin' | 'integration' | 'manual';
|
|
163
163
|
}
|
|
164
164
|
export type IntegrationKind = 'database' | 'storage';
|
|
165
165
|
export interface UserIntegration {
|
|
@@ -2,3 +2,5 @@ import { Auth, StreamByConfig } from '../types';
|
|
|
2
2
|
export declare function assertBuiltinAccess(auth: Auth, builtinId: string, config: StreamByConfig, kind: 'database' | 'storage'): Promise<boolean>;
|
|
3
3
|
export declare function isBuiltinStorageId(id: string, config: StreamByConfig): boolean;
|
|
4
4
|
export declare function isBuiltinDb(id: string, config: StreamByConfig): boolean;
|
|
5
|
+
export declare const BUILTIN_STORAGE_DISPLAY: Record<string, string>;
|
|
6
|
+
export declare const BUILTIN_DB_DISPLAY: Record<string, string>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BUILTIN_DB_DISPLAY = exports.BUILTIN_STORAGE_DISPLAY = void 0;
|
|
3
4
|
exports.assertBuiltinAccess = assertBuiltinAccess;
|
|
4
5
|
exports.isBuiltinStorageId = isBuiltinStorageId;
|
|
5
6
|
exports.isBuiltinDb = isBuiltinDb;
|
|
@@ -17,3 +18,15 @@ function isBuiltinStorageId(id, config) {
|
|
|
17
18
|
function isBuiltinDb(id, config) {
|
|
18
19
|
return (config.databases ?? []).some(db => db.id === id);
|
|
19
20
|
}
|
|
21
|
+
// Display names for builtin connections — never the raw config id/type. The frontend pairs
|
|
22
|
+
// this with the StreamBy icon to visually mark a connection as built-in vs BYOC.
|
|
23
|
+
exports.BUILTIN_STORAGE_DISPLAY = {
|
|
24
|
+
s3: 'AWS S3',
|
|
25
|
+
gcs: 'Google Cloud Storage',
|
|
26
|
+
r2: 'Cloudflare R2',
|
|
27
|
+
azure: 'Azure Blob Storage',
|
|
28
|
+
};
|
|
29
|
+
exports.BUILTIN_DB_DISPLAY = {
|
|
30
|
+
sql: 'PostgreSQL',
|
|
31
|
+
nosql: 'MongoDB',
|
|
32
|
+
};
|