indraq_cli 2.1.0 → 2.2.1
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/LICENSE +21 -21
- package/README.md +41 -15
- package/cloud-api/.dockerignore +18 -18
- package/cloud-api/.env.example +14 -14
- package/cloud-api/Dockerfile +19 -19
- package/cloud-api/README.md +93 -93
- package/cloud-api/dist/index.js +265 -27
- package/cloud-api/node_modules/.package-lock.json +1 -1
- package/cloud-api/package-lock.json +2 -2
- package/cloud-api/package.json +1 -1
- package/cloud-api/sql/001_init.sql +8 -8
- package/cloud-api/sql/002_user_providers_and_project_resources.sql +40 -40
- package/cloud-api/sql/003_environment_infrastructure.sql +19 -19
- package/cloud-api/sql/004_managed_aws_credentials.sql +20 -20
- package/cloud-api/sql/005_project_members_and_resource_grants.sql +22 -22
- package/cloud-api/sql/006_catalog_identity_password_security.sql +96 -96
- package/cloud-api/sql/007_aws_access_key_rotation.sql +16 -16
- package/cloud-api/sql/008_provider_scopes.sql +91 -91
- package/cloud-api/sql/009_scope_user_secrets_and_project_lifecycle.sql +13 -0
- package/cloud-api/src/index.ts +164 -27
- package/cloud-api/tsconfig.json +18 -18
- package/dist/cli/help-content.d.ts.map +1 -1
- package/dist/cli/help-content.js +29 -7
- package/dist/cli/help-content.js.map +1 -1
- package/dist/modules/access/commands/access.command.js +30 -30
- package/dist/modules/aws/services/route53-lookup.service.js.map +1 -1
- package/dist/modules/cloud/commands/cloud.command.d.ts +4 -0
- package/dist/modules/cloud/commands/cloud.command.d.ts.map +1 -1
- package/dist/modules/cloud/commands/cloud.command.js +52 -1
- package/dist/modules/cloud/commands/cloud.command.js.map +1 -1
- package/dist/modules/cloud/index.d.ts.map +1 -1
- package/dist/modules/cloud/index.js +6 -3
- package/dist/modules/cloud/index.js.map +1 -1
- package/dist/modules/cloud/services/cloud-jenkins.service.js +71 -71
- package/dist/modules/cloud/services/user-provider.service.d.ts +2 -8
- package/dist/modules/cloud/services/user-provider.service.d.ts.map +1 -1
- package/dist/modules/cloud/services/user-provider.service.js +7 -35
- package/dist/modules/cloud/services/user-provider.service.js.map +1 -1
- package/dist/modules/deploy/commands/build.command.d.ts +1 -0
- package/dist/modules/deploy/commands/build.command.d.ts.map +1 -1
- package/dist/modules/deploy/commands/build.command.js +12 -5
- package/dist/modules/deploy/commands/build.command.js.map +1 -1
- package/dist/modules/deploy/index.d.ts.map +1 -1
- package/dist/modules/deploy/index.js +11 -4
- package/dist/modules/deploy/index.js.map +1 -1
- package/dist/modules/prebuild/commands/prebuild.command.d.ts +0 -4
- package/dist/modules/prebuild/commands/prebuild.command.d.ts.map +1 -1
- package/dist/modules/prebuild/commands/prebuild.command.js +34 -38
- package/dist/modules/prebuild/commands/prebuild.command.js.map +1 -1
- package/dist/modules/prebuild/index.d.ts.map +1 -1
- package/dist/modules/prebuild/index.js +4 -8
- package/dist/modules/prebuild/index.js.map +1 -1
- package/dist/modules/scaffold/services/docker-template.service.js +95 -95
- package/dist/modules/scopes/commands/scope.command.d.ts.map +1 -1
- package/dist/modules/scopes/commands/scope.command.js +3 -47
- package/dist/modules/scopes/commands/scope.command.js.map +1 -1
- package/dist/modules/scopes/services/scope-native.service.js +2 -2
- package/dist/modules/scopes/services/scope-native.service.js.map +1 -1
- package/dist/modules/users/commands/user.command.d.ts +1 -0
- package/dist/modules/users/commands/user.command.d.ts.map +1 -1
- package/dist/modules/users/commands/user.command.js +15 -1
- package/dist/modules/users/commands/user.command.js.map +1 -1
- package/dist/modules/users/index.d.ts.map +1 -1
- package/dist/modules/users/index.js +3 -2
- package/dist/modules/users/index.js.map +1 -1
- package/dist/modules/users/providers/jenkins-user.provider.js +95 -95
- package/dist/shared/config/config-files.js +7 -7
- package/dist/shared/jenkins/jenkins.service.d.ts.map +1 -1
- package/dist/shared/jenkins/jenkins.service.js +13 -8
- package/dist/shared/jenkins/jenkins.service.js.map +1 -1
- package/dist/shared/jenkins/role-strategy-groovy.js +84 -84
- package/docs/ARCHITECTURE.md +29 -29
- package/docs/CREATE-DEPLOYMENT-reference.groovy +1105 -1105
- package/package.json +9 -5
- package/templates/jenkins/CREATE-DEPLOYMENT.groovy +1105 -1105
- package/templates/jenkins/Jenkinsfile-Mobile-App +759 -759
- package/CHANGELOG_v2.1.0.md +0 -27
- package/UPGRADE_v2.0_to_v2.1.md +0 -68
- package/cloud-api/dist/index.js.map +0 -1
package/cloud-api/dist/index.js
CHANGED
|
@@ -106,28 +106,31 @@ const listQuery = (req, defaultLimit = 10, maximum = 5000) => {
|
|
|
106
106
|
const limit = all ? maximum : Math.max(1, Math.min(maximum, Number.isInteger(parsed) ? parsed : defaultLimit));
|
|
107
107
|
return { search, limit, all };
|
|
108
108
|
};
|
|
109
|
-
const projectAccessLevel = async (projectIdRaw, user) => {
|
|
109
|
+
const projectAccessLevel = async (projectIdRaw, user, includeArchived = false) => {
|
|
110
110
|
const projectId = ensureUuid(projectIdRaw, 'Project ID');
|
|
111
|
-
const result = await pool.query(`SELECT p.id,p.created_by,pm.role,pm.access_level
|
|
111
|
+
const result = await pool.query(`SELECT p.id,p.created_by,p.archived_at,pm.role,pm.access_level
|
|
112
112
|
FROM projects p
|
|
113
113
|
LEFT JOIN project_members pm ON pm.project_id=p.id AND pm.user_id=$2
|
|
114
114
|
WHERE p.id=$1
|
|
115
115
|
LIMIT 1`, [projectId, user.id]);
|
|
116
116
|
if (!result.rowCount)
|
|
117
117
|
return null;
|
|
118
|
-
if (user.role === 'admin' || user.role === 'manager')
|
|
119
|
-
return 'write';
|
|
120
118
|
const row = result.rows[0];
|
|
119
|
+
if (row.archived_at && !includeArchived)
|
|
120
|
+
return null;
|
|
121
|
+
if (user.role === 'admin')
|
|
122
|
+
return 'write';
|
|
121
123
|
if (row.created_by === user.id || row.role === 'owner' || row.access_level === 'write')
|
|
122
124
|
return 'write';
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
if (row.role || row.access_level === 'view')
|
|
126
|
+
return 'view';
|
|
127
|
+
return null;
|
|
125
128
|
};
|
|
126
129
|
const hasProjectAccess = async (projectId, user) => (await projectAccessLevel(projectId, user)) !== null;
|
|
127
130
|
const hasProjectWriteAccess = async (projectId, user) => (await projectAccessLevel(projectId, user)) === 'write';
|
|
128
131
|
const hasProjectResourceAccess = async (resourceIdRaw, user, manage = false) => {
|
|
129
132
|
const resourceId = ensureUuid(resourceIdRaw, 'Project resource ID');
|
|
130
|
-
if (user.role === 'admin'
|
|
133
|
+
if (user.role === 'admin')
|
|
131
134
|
return true;
|
|
132
135
|
const resource = await pool.query('SELECT project_id,created_by FROM project_resources WHERE id=$1', [resourceId]);
|
|
133
136
|
if (!resource.rowCount)
|
|
@@ -871,10 +874,10 @@ app.post('/me/aws-credentials/:awsUsername/resolve', auth, async (req, res) => {
|
|
|
871
874
|
});
|
|
872
875
|
app.get('/projects', auth, async (req, res) => {
|
|
873
876
|
const { search, limit } = listQuery(req);
|
|
874
|
-
const
|
|
877
|
+
const archivedMode = req.query.archived === 'only' ? 'only' : req.query.archived === 'all' ? 'all' : 'active';
|
|
875
878
|
const result = await pool.query(`SELECT p.*,u.email AS creator_email,
|
|
876
879
|
CASE
|
|
877
|
-
WHEN $3
|
|
880
|
+
WHEN $3='admin' THEN 'write'
|
|
878
881
|
WHEN p.created_by=$1 THEN 'write'
|
|
879
882
|
WHEN pm.role='owner' OR pm.access_level='write' THEN 'write'
|
|
880
883
|
ELSE 'view'
|
|
@@ -882,9 +885,11 @@ app.get('/projects', auth, async (req, res) => {
|
|
|
882
885
|
FROM projects p
|
|
883
886
|
JOIN users u ON u.id=p.created_by
|
|
884
887
|
LEFT JOIN project_members pm ON pm.project_id=p.id AND pm.user_id=$1
|
|
885
|
-
WHERE ($
|
|
888
|
+
WHERE ($3='admin' OR p.created_by=$1 OR pm.user_id=$1)
|
|
889
|
+
AND ($2='' OR p.name ILIKE '%'||$2||'%' OR p.kind ILIKE '%'||$2||'%' OR u.email ILIKE '%'||$2||'%')
|
|
890
|
+
AND ($4='all' OR ($4='only' AND p.archived_at IS NOT NULL) OR ($4='active' AND p.archived_at IS NULL))
|
|
886
891
|
ORDER BY p.created_at DESC
|
|
887
|
-
LIMIT $
|
|
892
|
+
LIMIT $5`, [req.auth.id, search, req.auth.role, archivedMode, limit]);
|
|
888
893
|
res.json(result.rows);
|
|
889
894
|
});
|
|
890
895
|
app.post('/projects', auth, async (req, res) => {
|
|
@@ -927,6 +932,10 @@ app.patch('/projects/:id', ...allow('manager', 'admin'), async (req, res) => {
|
|
|
927
932
|
if (!reason)
|
|
928
933
|
return;
|
|
929
934
|
const id = ensureUuid(routeParam(req.params.id), 'Project ID');
|
|
935
|
+
if (!(await hasProjectWriteAccess(id, req.auth))) {
|
|
936
|
+
res.status(403).json({ error: 'WRITE access to the project is required.' });
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
930
939
|
if (typeof req.body?.name === 'string' && req.body.name.trim()) {
|
|
931
940
|
const duplicate = await pool.query('SELECT id FROM projects WHERE lower(name)=lower($1) AND id<>$2 LIMIT 1', [req.body.name.trim(), id]);
|
|
932
941
|
if (duplicate.rowCount) {
|
|
@@ -946,7 +955,11 @@ app.delete('/projects/:id', ...allow('manager', 'admin'), async (req, res) => {
|
|
|
946
955
|
const reason = requireReason(req, res);
|
|
947
956
|
if (!reason)
|
|
948
957
|
return;
|
|
949
|
-
const id = routeParam(req.params.id);
|
|
958
|
+
const id = ensureUuid(routeParam(req.params.id), 'Project ID');
|
|
959
|
+
if ((await projectAccessLevel(id, req.auth, true)) !== 'write') {
|
|
960
|
+
res.status(403).json({ error: 'WRITE access to the project is required.' });
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
950
963
|
const result = await pool.query('DELETE FROM projects WHERE id=$1 RETURNING id,name', [id]);
|
|
951
964
|
if (!result.rowCount) {
|
|
952
965
|
res.status(404).json({ error: 'Project not found' });
|
|
@@ -955,8 +968,46 @@ app.delete('/projects/:id', ...allow('manager', 'admin'), async (req, res) => {
|
|
|
955
968
|
await audit(req.auth.id, 'project.delete', 'project', id, reason, { name: result.rows[0].name });
|
|
956
969
|
res.status(204).end();
|
|
957
970
|
});
|
|
971
|
+
app.post('/projects/:id/archive', ...allow('manager', 'admin'), async (req, res) => {
|
|
972
|
+
const reason = requireReason(req, res);
|
|
973
|
+
if (!reason)
|
|
974
|
+
return;
|
|
975
|
+
const id = ensureUuid(routeParam(req.params.id), 'Project ID');
|
|
976
|
+
if ((await projectAccessLevel(id, req.auth, true)) !== 'write') {
|
|
977
|
+
res.status(403).json({ error: 'WRITE access to the project is required.' });
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const result = await pool.query('UPDATE projects SET archived_at=COALESCE(archived_at,now()),archived_by=COALESCE(archived_by,$2),updated_at=now() WHERE id=$1 RETURNING id,name,archived_at,archived_by', [id, req.auth.id]);
|
|
981
|
+
if (!result.rowCount) {
|
|
982
|
+
res.status(404).json({ error: 'Project not found' });
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
await audit(req.auth.id, 'project.archive', 'project', id, reason, { name: result.rows[0].name });
|
|
986
|
+
res.json(result.rows[0]);
|
|
987
|
+
});
|
|
988
|
+
app.post('/projects/:id/restore', ...allow('manager', 'admin'), async (req, res) => {
|
|
989
|
+
const reason = requireReason(req, res);
|
|
990
|
+
if (!reason)
|
|
991
|
+
return;
|
|
992
|
+
const id = ensureUuid(routeParam(req.params.id), 'Project ID');
|
|
993
|
+
if ((await projectAccessLevel(id, req.auth, true)) !== 'write') {
|
|
994
|
+
res.status(403).json({ error: 'WRITE access to the project is required.' });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const result = await pool.query('UPDATE projects SET archived_at=NULL,archived_by=NULL,updated_at=now() WHERE id=$1 RETURNING id,name,archived_at,archived_by', [id]);
|
|
998
|
+
if (!result.rowCount) {
|
|
999
|
+
res.status(404).json({ error: 'Project not found' });
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
await audit(req.auth.id, 'project.restore', 'project', id, reason, { name: result.rows[0].name });
|
|
1003
|
+
res.json(result.rows[0]);
|
|
1004
|
+
});
|
|
958
1005
|
app.get('/projects/:id/members', ...allow('manager', 'admin'), async (req, res) => {
|
|
959
|
-
const id = routeParam(req.params.id);
|
|
1006
|
+
const id = ensureUuid(routeParam(req.params.id), 'Project ID');
|
|
1007
|
+
if (!(await hasProjectWriteAccess(id, req.auth))) {
|
|
1008
|
+
res.status(403).json({ error: 'WRITE access to the project is required.' });
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
960
1011
|
const result = await pool.query(`SELECT pm.project_id,pm.user_id,pm.role,pm.access_level,pm.added_at,u.email,u.name,u.role AS user_role
|
|
961
1012
|
FROM project_members pm JOIN users u ON u.id=pm.user_id
|
|
962
1013
|
WHERE pm.project_id=$1 AND u.is_active=true ORDER BY u.email`, [id]);
|
|
@@ -966,7 +1017,11 @@ app.post('/projects/:id/members', ...allow('manager', 'admin'), async (req, res)
|
|
|
966
1017
|
const reason = requireReason(req, res);
|
|
967
1018
|
if (!reason)
|
|
968
1019
|
return;
|
|
969
|
-
const id = routeParam(req.params.id);
|
|
1020
|
+
const id = ensureUuid(routeParam(req.params.id), 'Project ID');
|
|
1021
|
+
if (!(await hasProjectWriteAccess(id, req.auth))) {
|
|
1022
|
+
res.status(403).json({ error: 'WRITE access to the project is required.' });
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
970
1025
|
const userId = String(req.body?.userId || '');
|
|
971
1026
|
const project = await pool.query('SELECT id FROM projects WHERE id=$1', [id]);
|
|
972
1027
|
if (!project.rowCount) {
|
|
@@ -1002,7 +1057,11 @@ app.delete('/projects/:id/members/:userId', ...allow('manager', 'admin'), async
|
|
|
1002
1057
|
const reason = requireReason(req, res);
|
|
1003
1058
|
if (!reason)
|
|
1004
1059
|
return;
|
|
1005
|
-
const id = routeParam(req.params.id);
|
|
1060
|
+
const id = ensureUuid(routeParam(req.params.id), 'Project ID');
|
|
1061
|
+
if (!(await hasProjectWriteAccess(id, req.auth))) {
|
|
1062
|
+
res.status(403).json({ error: 'WRITE access to the project is required.' });
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1006
1065
|
const userId = routeParam(req.params.userId);
|
|
1007
1066
|
const target = await pool.query('SELECT email,role FROM users WHERE id=$1', [userId]);
|
|
1008
1067
|
if (req.auth.role === 'manager' && target.rows[0]?.role && target.rows[0].role !== 'user') {
|
|
@@ -1235,6 +1294,10 @@ app.post('/scopes/:id/resolve', auth, async (req, res) => {
|
|
|
1235
1294
|
res.status(404).json({ error: 'Scope not found or access denied' });
|
|
1236
1295
|
return;
|
|
1237
1296
|
}
|
|
1297
|
+
if (access === 'use') {
|
|
1298
|
+
res.status(403).json({ error: 'MANAGE access is required to resolve the provider management credential.' });
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1238
1301
|
const result = await pool.query('SELECT id,provider,name,description,company,config,encrypted_secret FROM provider_scopes WHERE id=$1 AND is_active=true', [id]);
|
|
1239
1302
|
if (!result.rowCount) {
|
|
1240
1303
|
res.status(404).json({ error: 'Scope not found' });
|
|
@@ -1333,6 +1396,15 @@ app.get('/scopes/:id/audit', auth, async (req, res) => {
|
|
|
1333
1396
|
ORDER BY a.created_at DESC LIMIT $2`, [id, limit]);
|
|
1334
1397
|
res.json(result.rows);
|
|
1335
1398
|
});
|
|
1399
|
+
app.get('/me/scopes', auth, async (req, res) => {
|
|
1400
|
+
const result = await pool.query(`SELECT ps.id,ps.provider,ps.name,ps.description,ps.company,ps.config,(ps.encrypted_secret IS NOT NULL) has_secret,
|
|
1401
|
+
CASE WHEN $2='admin' THEN 'manage' ELSE psm.access END AS access
|
|
1402
|
+
FROM provider_scopes ps
|
|
1403
|
+
LEFT JOIN provider_scope_members psm ON psm.scope_id=ps.id AND psm.user_id=$1
|
|
1404
|
+
WHERE ps.is_active=true AND ($2='admin' OR psm.user_id=$1)
|
|
1405
|
+
ORDER BY ps.provider,lower(ps.name)`, [req.auth.id, req.auth.role]);
|
|
1406
|
+
res.json(result.rows);
|
|
1407
|
+
});
|
|
1336
1408
|
app.get('/me/scope-defaults', auth, async (req, res) => {
|
|
1337
1409
|
const result = await pool.query(`SELECT usd.provider,usd.scope_id,ps.name,ps.company,ps.config,ps.updated_at
|
|
1338
1410
|
FROM user_scope_defaults usd JOIN provider_scopes ps ON ps.id=usd.scope_id
|
|
@@ -1399,20 +1471,48 @@ app.post('/me/scopes/:provider/resolve', auth, async (req, res) => {
|
|
|
1399
1471
|
const visible = req.auth.role === 'admin'
|
|
1400
1472
|
? await pool.query('SELECT id FROM provider_scopes WHERE provider=$1 AND is_active=true ORDER BY created_at', [provider])
|
|
1401
1473
|
: await pool.query('SELECT ps.id FROM provider_scopes ps JOIN provider_scope_members psm ON psm.scope_id=ps.id WHERE psm.user_id=$1 AND ps.provider=$2 AND ps.is_active=true ORDER BY ps.created_at', [req.auth.id, provider]);
|
|
1402
|
-
if (visible.
|
|
1474
|
+
if (visible.rows.length === 1)
|
|
1403
1475
|
scopeId = visible.rows[0].id;
|
|
1404
|
-
else if (visible.
|
|
1476
|
+
else if (visible.rows.length > 1) {
|
|
1405
1477
|
res.status(409).json({ error: `Multiple ${provider} scopes are available. Link one to the project environment or set your default with \`indraq scope default\`.` });
|
|
1406
1478
|
return;
|
|
1407
1479
|
}
|
|
1408
1480
|
}
|
|
1409
|
-
|
|
1481
|
+
const access = scopeId ? await scopeAccessFor(scopeId, req.auth) : null;
|
|
1482
|
+
if (!scopeId || !access) {
|
|
1410
1483
|
res.status(404).json({ error: `No ${provider} scope is assigned to your account.` });
|
|
1411
1484
|
return;
|
|
1412
1485
|
}
|
|
1413
1486
|
const row = (await pool.query('SELECT id,provider,name,description,company,config,encrypted_secret FROM provider_scopes WHERE id=$1 AND is_active=true', [scopeId])).rows[0];
|
|
1414
|
-
|
|
1415
|
-
|
|
1487
|
+
let secret = null;
|
|
1488
|
+
let credentialSource = 'scope-management';
|
|
1489
|
+
if (req.auth.role !== 'admin' && provider === 'aws') {
|
|
1490
|
+
const own = await pool.query('SELECT access_key_id,encrypted_secret_access_key FROM scope_aws_credentials WHERE scope_id=$1 AND cloud_user_id=$2', [scopeId, req.auth.id]);
|
|
1491
|
+
if (!own.rowCount) {
|
|
1492
|
+
res.status(404).json({ error: 'No AWS credential is provisioned for you in this scope.' });
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
secret = { accessKeyId: own.rows[0].access_key_id, secretAccessKey: decrypt(own.rows[0].encrypted_secret_access_key) };
|
|
1496
|
+
credentialSource = 'user-native';
|
|
1497
|
+
}
|
|
1498
|
+
else if (req.auth.role !== 'admin' && ['npm', 'jenkins-dev', 'jenkins-prod'].includes(provider)) {
|
|
1499
|
+
const own = await pool.query('SELECT external_username,encrypted_secret FROM scope_external_accounts WHERE scope_id=$1 AND cloud_user_id=$2', [scopeId, req.auth.id]);
|
|
1500
|
+
if (!own.rowCount || !own.rows[0].encrypted_secret) {
|
|
1501
|
+
res.status(404).json({ error: `No ${provider} credential is provisioned for you in this scope. Rotate your provider password or ask a scope manager to reprovision you.` });
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
secret = provider === 'npm'
|
|
1505
|
+
? { identity: own.rows[0].external_username, secret: decrypt(own.rows[0].encrypted_secret) }
|
|
1506
|
+
: { username: own.rows[0].external_username, secret: decrypt(own.rows[0].encrypted_secret) };
|
|
1507
|
+
credentialSource = 'user-native';
|
|
1508
|
+
}
|
|
1509
|
+
else {
|
|
1510
|
+
secret = row.encrypted_secret ? JSON.parse(decrypt(row.encrypted_secret)) : null;
|
|
1511
|
+
}
|
|
1512
|
+
await audit(req.auth.id, 'scope.credential.resolve', 'provider-scope', scopeId, reason, { provider, projectId: projectId || null, environment: environment || null, credentialSource });
|
|
1513
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1514
|
+
res.setHeader('Pragma', 'no-cache');
|
|
1515
|
+
res.json({ id: row.id, provider: row.provider, name: row.name, description: row.description, company: row.company, config: row.config || {}, secret, access: access === 'admin' ? 'manage' : access });
|
|
1416
1516
|
});
|
|
1417
1517
|
app.get('/projects/:projectId/environment-scopes', auth, async (req, res) => {
|
|
1418
1518
|
const projectId = ensureUuid(routeParam(req.params.projectId), 'Project ID');
|
|
@@ -1558,10 +1658,11 @@ app.put('/scopes/:scopeId/users/:userId/external-account', ...allow('manager', '
|
|
|
1558
1658
|
return;
|
|
1559
1659
|
}
|
|
1560
1660
|
const externalEmail = req.body?.externalEmail ? String(req.body.externalEmail).trim() : null;
|
|
1561
|
-
const
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1661
|
+
const encryptedSecret = req.body?.secret ? encrypt(String(req.body.secret)) : null;
|
|
1662
|
+
const result = await pool.query(`INSERT INTO scope_external_accounts(scope_id,cloud_user_id,provider,external_username,external_email,managed_by_user_id,config,encrypted_secret)
|
|
1663
|
+
VALUES($1,$2,$3,$4,$5,$6,$7,$8)
|
|
1664
|
+
ON CONFLICT(scope_id,cloud_user_id) DO UPDATE SET external_username=excluded.external_username,external_email=excluded.external_email,managed_by_user_id=excluded.managed_by_user_id,config=excluded.config,encrypted_secret=COALESCE(excluded.encrypted_secret,scope_external_accounts.encrypted_secret),updated_at=now()
|
|
1665
|
+
RETURNING id,scope_id,cloud_user_id,provider,external_username,external_email,config,updated_at`, [scopeId, userId, scope.rows[0].provider, externalUsername, externalEmail, req.auth.id, req.body?.config || {}, encryptedSecret]);
|
|
1565
1666
|
await audit(req.auth.id, 'scope.native-account.upsert', 'provider-scope', scopeId, reason, { userId, provider: scope.rows[0].provider, externalUsername, externalEmail });
|
|
1566
1667
|
res.json(result.rows[0]);
|
|
1567
1668
|
});
|
|
@@ -1699,6 +1800,128 @@ app.post('/me/scopes/:scopeId/aws-credential/resolve', auth, async (req, res) =>
|
|
|
1699
1800
|
await audit(req.auth.id, 'scope.aws-credential.resolve', 'provider-scope', scopeId, reason, { awsUsername: row.aws_username, accessKeyId: row.access_key_id });
|
|
1700
1801
|
res.json({ awsUsername: row.aws_username, accessKeyId: row.access_key_id, secretAccessKey: decrypt(row.encrypted_secret_access_key), indraqRole: row.indraq_role });
|
|
1701
1802
|
});
|
|
1803
|
+
app.post('/me/scopes/:scopeId/aws-credential/rotate', auth, async (req, res) => {
|
|
1804
|
+
const reason = requireReason(req, res);
|
|
1805
|
+
if (!reason)
|
|
1806
|
+
return;
|
|
1807
|
+
const scopeId = ensureUuid(routeParam(req.params.scopeId), 'Scope ID');
|
|
1808
|
+
if (!(await scopeVisible(scopeId, req.auth))) {
|
|
1809
|
+
res.status(403).json({ error: 'Scope access denied.' });
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
const own = await pool.query('SELECT aws_username,access_key_id,encrypted_secret_access_key FROM scope_aws_credentials WHERE scope_id=$1 AND cloud_user_id=$2', [scopeId, req.auth.id]);
|
|
1813
|
+
if (!own.rowCount) {
|
|
1814
|
+
res.status(404).json({ error: 'No AWS credential is stored for you in this scope.' });
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
const scope = await pool.query('SELECT config,encrypted_secret FROM provider_scopes WHERE id=$1 AND provider=\'aws\' AND is_active=true', [scopeId]);
|
|
1818
|
+
if (!scope.rowCount || !scope.rows[0].encrypted_secret) {
|
|
1819
|
+
res.status(409).json({ error: 'AWS scope management credential is unavailable.' });
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const management = JSON.parse(decrypt(scope.rows[0].encrypted_secret));
|
|
1823
|
+
const region = String(scope.rows[0].config?.region || 'us-east-1');
|
|
1824
|
+
const iam = new IAMClient({ region, credentials: { accessKeyId: management.accessKeyId, secretAccessKey: management.secretAccessKey, ...(management.sessionToken ? { sessionToken: management.sessionToken } : {}) } });
|
|
1825
|
+
const created = await iam.send(new CreateAccessKeyCommand({ UserName: own.rows[0].aws_username }));
|
|
1826
|
+
const next = created.AccessKey;
|
|
1827
|
+
if (!next?.AccessKeyId || !next.SecretAccessKey) {
|
|
1828
|
+
res.status(502).json({ error: 'AWS did not return a replacement access key.' });
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
await pool.query('UPDATE scope_aws_credentials SET access_key_id=$1,encrypted_secret_access_key=$2,updated_at=now() WHERE scope_id=$3 AND cloud_user_id=$4', [next.AccessKeyId, encrypt(next.SecretAccessKey), scopeId, req.auth.id]);
|
|
1833
|
+
await iam.send(new DeleteAccessKeyCommand({ UserName: own.rows[0].aws_username, AccessKeyId: own.rows[0].access_key_id }));
|
|
1834
|
+
}
|
|
1835
|
+
catch (error) {
|
|
1836
|
+
try {
|
|
1837
|
+
await iam.send(new DeleteAccessKeyCommand({ UserName: own.rows[0].aws_username, AccessKeyId: next.AccessKeyId }));
|
|
1838
|
+
}
|
|
1839
|
+
catch { /* best effort */ }
|
|
1840
|
+
throw error;
|
|
1841
|
+
}
|
|
1842
|
+
await audit(req.auth.id, 'scope.aws-credential.self-rotate', 'provider-scope', scopeId, reason, { awsUsername: own.rows[0].aws_username, previousAccessKeyId: own.rows[0].access_key_id, accessKeyId: next.AccessKeyId });
|
|
1843
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1844
|
+
res.setHeader('Pragma', 'no-cache');
|
|
1845
|
+
res.json({ awsUsername: own.rows[0].aws_username, accessKeyId: next.AccessKeyId, secretAccessKey: next.SecretAccessKey });
|
|
1846
|
+
});
|
|
1847
|
+
app.post('/me/scopes/:scopeId/external-account/rotate', auth, async (req, res) => {
|
|
1848
|
+
const reason = requireReason(req, res);
|
|
1849
|
+
if (!reason)
|
|
1850
|
+
return;
|
|
1851
|
+
const scopeId = ensureUuid(routeParam(req.params.scopeId), 'Scope ID');
|
|
1852
|
+
if (!(await scopeVisible(scopeId, req.auth))) {
|
|
1853
|
+
res.status(403).json({ error: 'Scope access denied.' });
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
const password = String(req.body?.password || '');
|
|
1857
|
+
if (password.length < 10) {
|
|
1858
|
+
res.status(400).json({ error: 'Password must be at least 10 characters.' });
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
const account = await pool.query('SELECT provider,external_username FROM scope_external_accounts WHERE scope_id=$1 AND cloud_user_id=$2', [scopeId, req.auth.id]);
|
|
1862
|
+
if (!account.rowCount) {
|
|
1863
|
+
res.status(404).json({ error: 'No NPM/Jenkins account is stored for you in this scope.' });
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
const scope = await pool.query('SELECT provider,config,encrypted_secret FROM provider_scopes WHERE id=$1 AND is_active=true', [scopeId]);
|
|
1867
|
+
if (!scope.rowCount || !scope.rows[0].encrypted_secret) {
|
|
1868
|
+
res.status(409).json({ error: 'Provider management credential is unavailable.' });
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
const provider = String(account.rows[0].provider);
|
|
1872
|
+
const management = JSON.parse(decrypt(scope.rows[0].encrypted_secret));
|
|
1873
|
+
const username = String(account.rows[0].external_username);
|
|
1874
|
+
if (provider === 'npm') {
|
|
1875
|
+
const base = String(scope.rows[0].config?.url || '').replace(/\/+$/, '');
|
|
1876
|
+
const tokenResponse = await fetch(`${base}/api/tokens`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ identity: management.identity, secret: management.secret }) });
|
|
1877
|
+
if (!tokenResponse.ok) {
|
|
1878
|
+
res.status(502).json({ error: `NPM management authentication failed (HTTP ${tokenResponse.status}).` });
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
const token = (await tokenResponse.json()).token;
|
|
1882
|
+
if (!token) {
|
|
1883
|
+
res.status(502).json({ error: 'NPM did not return a management token.' });
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
const headers = { authorization: `Bearer ${token}`, 'content-type': 'application/json' };
|
|
1887
|
+
const usersResponse = await fetch(`${base}/api/users?expand=permissions`, { headers });
|
|
1888
|
+
if (!usersResponse.ok) {
|
|
1889
|
+
res.status(502).json({ error: `Unable to list NPM users (HTTP ${usersResponse.status}).` });
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
const users = await usersResponse.json();
|
|
1893
|
+
const wanted = username.toLowerCase();
|
|
1894
|
+
const target = users.find((u) => String(u.email || '').toLowerCase() === wanted || String(u.nickname || '').toLowerCase() === wanted || String(u.name || '').toLowerCase() === wanted);
|
|
1895
|
+
if (!target) {
|
|
1896
|
+
res.status(404).json({ error: `NPM user ${username} was not found.` });
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
const update = await fetch(`${base}/api/users/${target.id}/auth`, { method: 'PUT', headers, body: JSON.stringify({ type: 'password', secret: password }) });
|
|
1900
|
+
if (!update.ok) {
|
|
1901
|
+
res.status(502).json({ error: `NPM password reset failed (HTTP ${update.status}).` });
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
else if (provider === 'jenkins-dev' || provider === 'jenkins-prod') {
|
|
1906
|
+
const url = String(scope.rows[0].config?.url || '').replace(/\/+$/, '');
|
|
1907
|
+
const adminUsername = String(management.username || '');
|
|
1908
|
+
const adminSecret = String(management.secret || '');
|
|
1909
|
+
const headers = await jenkinsRequestHeaders(url, adminUsername, adminSecret);
|
|
1910
|
+
const script = `import hudson.security.HudsonPrivateSecurityRealm\ndef user = hudson.model.User.getById(${JSON.stringify(username)}, false)\nif (user == null) throw new IllegalStateException('Jenkins user not found')\nuser.addProperty(HudsonPrivateSecurityRealm.Details.fromPlainPassword(${JSON.stringify(password)}))\nuser.save()\nprintln 'updated'`;
|
|
1911
|
+
const response = await fetch(`${url}/scriptText`, { method: 'POST', headers: { ...headers, 'content-type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ script }).toString() });
|
|
1912
|
+
if (!response.ok) {
|
|
1913
|
+
res.status(502).json({ error: `Jenkins password reset failed (HTTP ${response.status}).` });
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
else {
|
|
1918
|
+
res.status(400).json({ error: 'Scope must be NPM or Jenkins.' });
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
await pool.query('UPDATE scope_external_accounts SET encrypted_secret=$1,updated_at=now() WHERE scope_id=$2 AND cloud_user_id=$3', [encrypt(password), scopeId, req.auth.id]);
|
|
1922
|
+
await audit(req.auth.id, 'scope.external-account.self-rotate', 'provider-scope', scopeId, reason, { provider, username });
|
|
1923
|
+
res.status(204).end();
|
|
1924
|
+
});
|
|
1702
1925
|
// Reusable provider credentials are scoped to the authenticated IndraQ user.
|
|
1703
1926
|
// A user configures AWS/NPM/Jenkins/GHCR once and can reuse that profile across
|
|
1704
1927
|
// every project they are allowed to access. Secrets are encrypted at rest and
|
|
@@ -1969,17 +2192,25 @@ app.get('/users/:id/resource-grants', ...allow('manager', 'admin'), async (req,
|
|
|
1969
2192
|
res.status(403).json({ error: 'Managers may inspect grants only for normal users.' });
|
|
1970
2193
|
return;
|
|
1971
2194
|
}
|
|
2195
|
+
const managerFilter = req.auth.role === 'manager'
|
|
2196
|
+
? ` AND (p.created_by=$2 OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id=p.id AND pm.user_id=$2 AND (pm.role='owner' OR pm.access_level='write')))`
|
|
2197
|
+
: '';
|
|
2198
|
+
const params = req.auth.role === 'manager' ? [userId, req.auth.id] : [userId];
|
|
1972
2199
|
const result = await pool.query(`SELECT g.id,g.project_resource_id,g.user_id,g.permission,g.created_at,
|
|
1973
2200
|
pr.project_id,p.name AS project_name,pr.resource_type,pr.provider,pr.environment,pr.external_id,pr.config
|
|
1974
2201
|
FROM project_resource_grants g
|
|
1975
2202
|
JOIN project_resources pr ON pr.id=g.project_resource_id
|
|
1976
2203
|
JOIN projects p ON p.id=pr.project_id
|
|
1977
|
-
WHERE g.user_id=$1
|
|
1978
|
-
ORDER BY p.name,pr.provider,pr.resource_type`,
|
|
2204
|
+
WHERE g.user_id=$1 AND p.archived_at IS NULL${managerFilter}
|
|
2205
|
+
ORDER BY p.name,pr.provider,pr.resource_type`, params);
|
|
1979
2206
|
res.json(result.rows);
|
|
1980
2207
|
});
|
|
1981
2208
|
app.get('/projects/:projectId/resource-grants', ...allow('manager', 'admin'), async (req, res) => {
|
|
1982
2209
|
const projectId = routeParam(req.params.projectId);
|
|
2210
|
+
if (!(await hasProjectWriteAccess(projectId, req.auth))) {
|
|
2211
|
+
res.status(403).json({ error: 'WRITE access to the project is required.' });
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
1983
2214
|
const userId = typeof req.query.userId === 'string' ? req.query.userId : undefined;
|
|
1984
2215
|
const result = await pool.query(`SELECT g.id,g.project_resource_id,g.user_id,g.permission,g.created_at,
|
|
1985
2216
|
u.email,u.name,pr.resource_type,pr.provider,pr.environment,pr.external_id,pr.config
|
|
@@ -1995,6 +2226,10 @@ app.post('/projects/:projectId/resources/:resourceId/grants', ...allow('manager'
|
|
|
1995
2226
|
if (!reason)
|
|
1996
2227
|
return;
|
|
1997
2228
|
const projectId = routeParam(req.params.projectId);
|
|
2229
|
+
if (!(await hasProjectWriteAccess(projectId, req.auth))) {
|
|
2230
|
+
res.status(403).json({ error: 'WRITE access to the project is required.' });
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
1998
2233
|
const resourceId = routeParam(req.params.resourceId);
|
|
1999
2234
|
const userId = String(req.body?.userId || '');
|
|
2000
2235
|
const permission = req.body?.permission === 'view' ? 'view' : 'manage';
|
|
@@ -2023,6 +2258,10 @@ app.delete('/projects/:projectId/resources/:resourceId/grants/:userId', ...allow
|
|
|
2023
2258
|
if (!reason)
|
|
2024
2259
|
return;
|
|
2025
2260
|
const projectId = routeParam(req.params.projectId);
|
|
2261
|
+
if (!(await hasProjectWriteAccess(projectId, req.auth))) {
|
|
2262
|
+
res.status(403).json({ error: 'WRITE access to the project is required.' });
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2026
2265
|
const resourceId = routeParam(req.params.resourceId);
|
|
2027
2266
|
const userId = routeParam(req.params.userId);
|
|
2028
2267
|
const target = await pool.query('SELECT email,role FROM users WHERE id=$1', [userId]);
|
|
@@ -2155,4 +2394,3 @@ const bootstrap = async () => {
|
|
|
2155
2394
|
await runMigrations();
|
|
2156
2395
|
await bootstrap();
|
|
2157
2396
|
app.listen(Number(process.env.PORT || 4010), () => console.log(`IndraQ Cloud API listening on ${process.env.PORT || 4010}`));
|
|
2158
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indraq/cloud-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@indraq/cloud-api",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.7.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@aws-sdk/client-iam": "^3.1123.0",
|
|
12
12
|
"bcryptjs": "^3.0.2",
|
package/cloud-api/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
2
|
-
CREATE TABLE IF NOT EXISTS users (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), email text UNIQUE NOT NULL, name text NOT NULL, password_hash text NOT NULL, role text NOT NULL CHECK (role IN ('user','manager','admin')), is_active boolean NOT NULL DEFAULT true, created_by uuid REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now());
|
|
3
|
-
CREATE TABLE IF NOT EXISTS projects (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, kind text NOT NULL, config jsonb NOT NULL DEFAULT '{}'::jsonb, created_by uuid NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now());
|
|
4
|
-
CREATE TABLE IF NOT EXISTS project_members (project_id uuid REFERENCES projects(id) ON DELETE CASCADE, user_id uuid REFERENCES users(id) ON DELETE CASCADE, role text NOT NULL DEFAULT 'user', PRIMARY KEY(project_id,user_id));
|
|
5
|
-
CREATE TABLE IF NOT EXISTS servers (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL UNIQUE, environment text NOT NULL, ipv4 inet NOT NULL, health_path text DEFAULT '/health', is_backup boolean NOT NULL DEFAULT false, created_by uuid NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now());
|
|
6
|
-
CREATE TABLE IF NOT EXISTS project_integrations (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, provider text NOT NULL, config jsonb NOT NULL DEFAULT '{}'::jsonb, encrypted_secret text, UNIQUE(project_id,provider));
|
|
7
|
-
CREATE TABLE IF NOT EXISTS audit_logs (id bigserial PRIMARY KEY, actor_id uuid REFERENCES users(id), action text NOT NULL, resource_type text NOT NULL, resource_id text, reason text, metadata jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now());
|
|
8
|
-
CREATE INDEX IF NOT EXISTS audit_logs_created_at_idx ON audit_logs(created_at DESC);
|
|
1
|
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
2
|
+
CREATE TABLE IF NOT EXISTS users (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), email text UNIQUE NOT NULL, name text NOT NULL, password_hash text NOT NULL, role text NOT NULL CHECK (role IN ('user','manager','admin')), is_active boolean NOT NULL DEFAULT true, created_by uuid REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now());
|
|
3
|
+
CREATE TABLE IF NOT EXISTS projects (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, kind text NOT NULL, config jsonb NOT NULL DEFAULT '{}'::jsonb, created_by uuid NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now());
|
|
4
|
+
CREATE TABLE IF NOT EXISTS project_members (project_id uuid REFERENCES projects(id) ON DELETE CASCADE, user_id uuid REFERENCES users(id) ON DELETE CASCADE, role text NOT NULL DEFAULT 'user', PRIMARY KEY(project_id,user_id));
|
|
5
|
+
CREATE TABLE IF NOT EXISTS servers (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL UNIQUE, environment text NOT NULL, ipv4 inet NOT NULL, health_path text DEFAULT '/health', is_backup boolean NOT NULL DEFAULT false, created_by uuid NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now());
|
|
6
|
+
CREATE TABLE IF NOT EXISTS project_integrations (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, provider text NOT NULL, config jsonb NOT NULL DEFAULT '{}'::jsonb, encrypted_secret text, UNIQUE(project_id,provider));
|
|
7
|
+
CREATE TABLE IF NOT EXISTS audit_logs (id bigserial PRIMARY KEY, actor_id uuid REFERENCES users(id), action text NOT NULL, resource_type text NOT NULL, resource_id text, reason text, metadata jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now());
|
|
8
|
+
CREATE INDEX IF NOT EXISTS audit_logs_created_at_idx ON audit_logs(created_at DESC);
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
CREATE TABLE IF NOT EXISTS user_integrations (
|
|
2
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3
|
-
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
4
|
-
provider text NOT NULL,
|
|
5
|
-
config jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
6
|
-
encrypted_secret text,
|
|
7
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
8
|
-
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
9
|
-
UNIQUE(user_id, provider)
|
|
10
|
-
);
|
|
11
|
-
|
|
12
|
-
CREATE INDEX IF NOT EXISTS user_integrations_user_idx
|
|
13
|
-
ON user_integrations(user_id, provider);
|
|
14
|
-
|
|
15
|
-
CREATE TABLE IF NOT EXISTS project_resources (
|
|
16
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
17
|
-
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
18
|
-
environment text NOT NULL DEFAULT 'dev',
|
|
19
|
-
resource_type text NOT NULL,
|
|
20
|
-
provider text NOT NULL,
|
|
21
|
-
provider_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
22
|
-
external_id text,
|
|
23
|
-
config jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
24
|
-
created_by uuid REFERENCES users(id),
|
|
25
|
-
updated_by uuid REFERENCES users(id),
|
|
26
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
27
|
-
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
28
|
-
UNIQUE(project_id, environment, resource_type)
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
CREATE INDEX IF NOT EXISTS project_resources_project_idx
|
|
32
|
-
ON project_resources(project_id, environment, resource_type);
|
|
33
|
-
|
|
34
|
-
CREATE INDEX IF NOT EXISTS project_resources_provider_user_idx
|
|
35
|
-
ON project_resources(provider_user_id, provider);
|
|
36
|
-
|
|
37
|
-
-- provider credentials belong to an authenticated IndraQ user and are
|
|
38
|
-
-- reusable across every project that user can access. Project resources keep
|
|
39
|
-
-- only infrastructure associations plus the user whose provider profile last
|
|
40
|
-
-- reconciled the resource.
|
|
1
|
+
CREATE TABLE IF NOT EXISTS user_integrations (
|
|
2
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3
|
+
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
4
|
+
provider text NOT NULL,
|
|
5
|
+
config jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
6
|
+
encrypted_secret text,
|
|
7
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
8
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
9
|
+
UNIQUE(user_id, provider)
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE INDEX IF NOT EXISTS user_integrations_user_idx
|
|
13
|
+
ON user_integrations(user_id, provider);
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS project_resources (
|
|
16
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
17
|
+
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
18
|
+
environment text NOT NULL DEFAULT 'dev',
|
|
19
|
+
resource_type text NOT NULL,
|
|
20
|
+
provider text NOT NULL,
|
|
21
|
+
provider_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
22
|
+
external_id text,
|
|
23
|
+
config jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
24
|
+
created_by uuid REFERENCES users(id),
|
|
25
|
+
updated_by uuid REFERENCES users(id),
|
|
26
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
27
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
28
|
+
UNIQUE(project_id, environment, resource_type)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE INDEX IF NOT EXISTS project_resources_project_idx
|
|
32
|
+
ON project_resources(project_id, environment, resource_type);
|
|
33
|
+
|
|
34
|
+
CREATE INDEX IF NOT EXISTS project_resources_provider_user_idx
|
|
35
|
+
ON project_resources(provider_user_id, provider);
|
|
36
|
+
|
|
37
|
+
-- provider credentials belong to an authenticated IndraQ user and are
|
|
38
|
+
-- reusable across every project that user can access. Project resources keep
|
|
39
|
+
-- only infrastructure associations plus the user whose provider profile last
|
|
40
|
+
-- reconciled the resource.
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
-- shared admin-managed named environments.
|
|
2
|
-
-- Environments are generic reusable targets. PRIMARY/SECONDARY is assigned only
|
|
3
|
-
-- when a project creates a FAILOVER Route53 record.
|
|
4
|
-
CREATE TABLE IF NOT EXISTS environments (
|
|
5
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
6
|
-
name text NOT NULL UNIQUE,
|
|
7
|
-
description text,
|
|
8
|
-
ipv4 inet NOT NULL,
|
|
9
|
-
region text,
|
|
10
|
-
health_path text NOT NULL DEFAULT '/health',
|
|
11
|
-
created_by uuid NOT NULL REFERENCES users(id),
|
|
12
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
13
|
-
updated_at timestamptz NOT NULL DEFAULT now()
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
CREATE INDEX IF NOT EXISTS environments_name_idx ON environments(name);
|
|
17
|
-
|
|
18
|
-
ALTER TABLE project_resources
|
|
19
|
-
ADD COLUMN IF NOT EXISTS environment_id uuid REFERENCES environments(id) ON DELETE SET NULL;
|
|
1
|
+
-- shared admin-managed named environments.
|
|
2
|
+
-- Environments are generic reusable targets. PRIMARY/SECONDARY is assigned only
|
|
3
|
+
-- when a project creates a FAILOVER Route53 record.
|
|
4
|
+
CREATE TABLE IF NOT EXISTS environments (
|
|
5
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
6
|
+
name text NOT NULL UNIQUE,
|
|
7
|
+
description text,
|
|
8
|
+
ipv4 inet NOT NULL,
|
|
9
|
+
region text,
|
|
10
|
+
health_path text NOT NULL DEFAULT '/health',
|
|
11
|
+
created_by uuid NOT NULL REFERENCES users(id),
|
|
12
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
13
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX IF NOT EXISTS environments_name_idx ON environments(name);
|
|
17
|
+
|
|
18
|
+
ALTER TABLE project_resources
|
|
19
|
+
ADD COLUMN IF NOT EXISTS environment_id uuid REFERENCES environments(id) ON DELETE SET NULL;
|