mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 20:27:12 +00:00
feat(infisical-pg): completed integration, integration auth and project bot api migration
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
import { initDbConnection } from "./src/db";
|
||||||
|
import { TableName } from "./src/db/schemas";
|
||||||
|
import { selectAllTableCols } from "./src/lib/knex";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
const db = initDbConnection(process.env.DB_CONNECTION_URI);
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
const folders = db
|
||||||
|
.withRecursive("parent", (qb) => {
|
||||||
|
qb.select({
|
||||||
|
depth: 1,
|
||||||
|
path: db.raw("'/'")
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(db, TableName.SecretFolder))
|
||||||
|
.from(TableName.SecretFolder)
|
||||||
|
.join(
|
||||||
|
TableName.Environment,
|
||||||
|
`${TableName.SecretFolder}.envId`,
|
||||||
|
`${TableName.Environment}.id`
|
||||||
|
)
|
||||||
|
.where({
|
||||||
|
projectId: "01c10de1-8743-490f-9c8a-7a19c4dc72a9",
|
||||||
|
parentId: null
|
||||||
|
})
|
||||||
|
.where(`${TableName.Environment}.slug`, "dev")
|
||||||
|
.union((qb) =>
|
||||||
|
qb
|
||||||
|
.select({
|
||||||
|
depth: db.raw("parent.depth + 1"),
|
||||||
|
path: db.raw(
|
||||||
|
"CONCAT((CASE WHEN parent.path = '/' THEN '' ELSE parent.path END),'/', secret_folders.name)"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(db, TableName.SecretFolder))
|
||||||
|
.whereRaw(
|
||||||
|
`depth = array_position(ARRAY[${[1, 2]
|
||||||
|
.map((_) => "?")
|
||||||
|
.join(",")}]::varchar[], secret_folders.name,depth)`,
|
||||||
|
[...["ui", "design"]]
|
||||||
|
)
|
||||||
|
.from(TableName.SecretFolder)
|
||||||
|
.join("parent", "parent.id", `${TableName.SecretFolder}.parentId`)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.select("*")
|
||||||
|
.from("parent")
|
||||||
|
.orderBy("depth", "desc")
|
||||||
|
.first();
|
||||||
|
console.log(folders.toSQL());
|
||||||
|
console.log(JSON.stringify(await folders, null, 4));
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
Generated
+159
@@ -18,6 +18,7 @@
|
|||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/swagger": "^8.12.0",
|
"@fastify/swagger": "^8.12.0",
|
||||||
"@fastify/swagger-ui": "^1.10.1",
|
"@fastify/swagger-ui": "^1.10.1",
|
||||||
|
"@octokit/rest": "^20.0.2",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"argon2": "^0.31.2",
|
"argon2": "^0.31.2",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
@@ -943,6 +944,149 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@octokit/auth-token": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/core": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-cZUy1gUvd4vttMic7C0lwPed8IYXWYp8kHIMatyhY8t8n3Cpw2ILczkV5pGMPqef7v0bLo0pOHrEHarsau2Ydg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/auth-token": "^4.0.0",
|
||||||
|
"@octokit/graphql": "^7.0.0",
|
||||||
|
"@octokit/request": "^8.0.2",
|
||||||
|
"@octokit/request-error": "^5.0.0",
|
||||||
|
"@octokit/types": "^12.0.0",
|
||||||
|
"before-after-hook": "^2.2.0",
|
||||||
|
"universal-user-agent": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/endpoint": {
|
||||||
|
"version": "9.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz",
|
||||||
|
"integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/types": "^12.0.0",
|
||||||
|
"universal-user-agent": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/graphql": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/request": "^8.0.1",
|
||||||
|
"@octokit/types": "^12.0.0",
|
||||||
|
"universal-user-agent": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/openapi-types": {
|
||||||
|
"version": "19.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz",
|
||||||
|
"integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw=="
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/plugin-paginate-rest": {
|
||||||
|
"version": "9.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz",
|
||||||
|
"integrity": "sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/types": "^12.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@octokit/core": ">=5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/plugin-request-log": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-2uJI1COtYCq8Z4yNSnM231TgH50bRkheQ9+aH8TnZanB6QilOnx8RMD2qsnamSOXtDj0ilxvevf5fGsBhBBzKA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@octokit/core": ">=5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/plugin-rest-endpoint-methods": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-ePbgBMYtGoRNXDyKGvr9cyHjQ163PbwD0y1MkDJCpkO2YH4OeXX40c4wYHKikHGZcpGPbcRLuy0unPUuafco8Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/types": "^12.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@octokit/core": ">=5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/request": {
|
||||||
|
"version": "8.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz",
|
||||||
|
"integrity": "sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/endpoint": "^9.0.0",
|
||||||
|
"@octokit/request-error": "^5.0.0",
|
||||||
|
"@octokit/types": "^12.0.0",
|
||||||
|
"universal-user-agent": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/request-error": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/types": "^12.0.0",
|
||||||
|
"deprecation": "^2.0.0",
|
||||||
|
"once": "^1.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/rest": {
|
||||||
|
"version": "20.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ux8NDgEraQ/DMAU1PlAohyfBBXDwhnX2j33Z1nJNziqAfHi70PuxkFYIcIt8aIAxtRE7KVuKp8lSR8pA0J5iOQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/core": "^5.0.0",
|
||||||
|
"@octokit/plugin-paginate-rest": "^9.0.0",
|
||||||
|
"@octokit/plugin-request-log": "^4.0.0",
|
||||||
|
"@octokit/plugin-rest-endpoint-methods": "^10.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@octokit/types": {
|
||||||
|
"version": "12.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz",
|
||||||
|
"integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/openapi-types": "^19.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@phc/format": {
|
"node_modules/@phc/format": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||||
@@ -2544,6 +2688,11 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/before-after-hook": {
|
||||||
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
|
||||||
|
},
|
||||||
"node_modules/big-integer": {
|
"node_modules/big-integer": {
|
||||||
"version": "1.6.52",
|
"version": "1.6.52",
|
||||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
|
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
|
||||||
@@ -3075,6 +3224,11 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/deprecation": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
|
||||||
@@ -8764,6 +8918,11 @@
|
|||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/universal-user-agent": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="
|
||||||
|
},
|
||||||
"node_modules/untildify": {
|
"node_modules/untildify": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/swagger": "^8.12.0",
|
"@fastify/swagger": "^8.12.0",
|
||||||
"@fastify/swagger-ui": "^1.10.1",
|
"@fastify/swagger-ui": "^1.10.1",
|
||||||
|
"@octokit/rest": "^20.0.2",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"argon2": "^0.31.2",
|
"argon2": "^0.31.2",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
|
|||||||
Vendored
+6
@@ -8,9 +8,12 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
|||||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||||
import { AuthMode } from "@app/services/auth/auth-signup-type";
|
import { AuthMode } from "@app/services/auth/auth-signup-type";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
||||||
|
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||||
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||||
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||||
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||||
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||||
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
||||||
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
||||||
@@ -69,7 +72,10 @@ declare module "fastify" {
|
|||||||
projectRole: TProjectRoleServiceFactory;
|
projectRole: TProjectRoleServiceFactory;
|
||||||
secret: TSecretServiceFactory;
|
secret: TSecretServiceFactory;
|
||||||
secretImport: TSecretImportServiceFactory;
|
secretImport: TSecretImportServiceFactory;
|
||||||
|
projectBot: TProjectBotServiceFactory;
|
||||||
folder: TSecretFolderServiceFactory;
|
folder: TSecretFolderServiceFactory;
|
||||||
|
integration: TIntegrationServiceFactory;
|
||||||
|
integrationAuth: TIntegrationAuthServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
|
|||||||
Vendored
+24
@@ -16,6 +16,12 @@ import {
|
|||||||
TIncidentContacts,
|
TIncidentContacts,
|
||||||
TIncidentContactsInsert,
|
TIncidentContactsInsert,
|
||||||
TIncidentContactsUpdate,
|
TIncidentContactsUpdate,
|
||||||
|
TIntegrationAuths,
|
||||||
|
TIntegrationAuthsInsert,
|
||||||
|
TIntegrationAuthsUpdate,
|
||||||
|
TIntegrations,
|
||||||
|
TIntegrationsInsert,
|
||||||
|
TIntegrationsUpdate,
|
||||||
TOrganizations,
|
TOrganizations,
|
||||||
TOrganizationsInsert,
|
TOrganizationsInsert,
|
||||||
TOrganizationsUpdate,
|
TOrganizationsUpdate,
|
||||||
@@ -25,6 +31,9 @@ import {
|
|||||||
TOrgRoles,
|
TOrgRoles,
|
||||||
TOrgRolesInsert,
|
TOrgRolesInsert,
|
||||||
TOrgRolesUpdate,
|
TOrgRolesUpdate,
|
||||||
|
TProjectBots,
|
||||||
|
TProjectBotsInsert,
|
||||||
|
TProjectBotsUpdate,
|
||||||
TProjectEnvironments,
|
TProjectEnvironments,
|
||||||
TProjectEnvironmentsInsert,
|
TProjectEnvironmentsInsert,
|
||||||
TProjectEnvironmentsUpdate,
|
TProjectEnvironmentsUpdate,
|
||||||
@@ -136,6 +145,11 @@ declare module "knex/types/tables" {
|
|||||||
TProjectEnvironmentsInsert,
|
TProjectEnvironmentsInsert,
|
||||||
TProjectEnvironmentsUpdate
|
TProjectEnvironmentsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.ProjectBot]: Knex.CompositeTableType<
|
||||||
|
TProjectBots,
|
||||||
|
TProjectBotsInsert,
|
||||||
|
TProjectBotsUpdate
|
||||||
|
>;
|
||||||
[TableName.ProjectRoles]: Knex.CompositeTableType<
|
[TableName.ProjectRoles]: Knex.CompositeTableType<
|
||||||
TProjectRoles,
|
TProjectRoles,
|
||||||
TProjectRolesInsert,
|
TProjectRolesInsert,
|
||||||
@@ -172,6 +186,16 @@ declare module "knex/types/tables" {
|
|||||||
TSecretImportsInsert,
|
TSecretImportsInsert,
|
||||||
TSecretImportsUpdate
|
TSecretImportsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.Integration]: Knex.CompositeTableType<
|
||||||
|
TIntegrations,
|
||||||
|
TIntegrationsInsert,
|
||||||
|
TIntegrationsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.IntegrationAuth]: Knex.CompositeTableType<
|
||||||
|
TIntegrationAuths,
|
||||||
|
TIntegrationAuthsInsert,
|
||||||
|
TIntegrationAuthsUpdate
|
||||||
|
>;
|
||||||
[TableName.JnSecretTag]: Knex.CompositeTableType<
|
[TableName.JnSecretTag]: Knex.CompositeTableType<
|
||||||
TSecretTagJunction,
|
TSecretTagJunction,
|
||||||
TSecretTagJunctionInsert,
|
TSecretTagJunctionInsert,
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.ProjectBot))) {
|
||||||
|
await knex.schema.createTable(TableName.ProjectBot, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("name").notNullable();
|
||||||
|
t.boolean("isActive").defaultTo(false).notNullable();
|
||||||
|
t.text("encryptedPrivateKey").notNullable();
|
||||||
|
t.text("publicKey").notNullable();
|
||||||
|
t.text("iv").notNullable();
|
||||||
|
t.text("tag").notNullable();
|
||||||
|
t.string("algorithm").notNullable();
|
||||||
|
t.string("keyEncoding").notNullable();
|
||||||
|
t.text("encryptedProjectKey");
|
||||||
|
t.text("encryptedProjectKeyNonce");
|
||||||
|
// one to one relationship
|
||||||
|
t.uuid("projectId").notNullable().unique();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.uuid("senderId");
|
||||||
|
t.foreign("senderId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.ProjectBot);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.ProjectBot);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ProjectBot);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.IntegrationAuth))) {
|
||||||
|
await knex.schema.createTable(TableName.IntegrationAuth, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("integration").notNullable();
|
||||||
|
t.string("teamId"); // vercel-specific
|
||||||
|
t.string("url"); // for self hosted
|
||||||
|
t.string("namespace"); // hashicorp specific
|
||||||
|
t.string("accountId"); // netlify
|
||||||
|
t.string("refreshCiphertext");
|
||||||
|
t.string("refreshIV");
|
||||||
|
t.string("refreshTag");
|
||||||
|
t.string("accessIdCiphertext");
|
||||||
|
t.string("accessIdIV");
|
||||||
|
t.string("accessIdTag");
|
||||||
|
t.string("accessCiphertext");
|
||||||
|
t.string("accessIV");
|
||||||
|
t.string("accessTag");
|
||||||
|
t.datetime("accessExpiresAt");
|
||||||
|
t.jsonb("metadata");
|
||||||
|
t.string("algorithm").notNullable();
|
||||||
|
t.string("keyEncoding").notNullable();
|
||||||
|
t.uuid("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.IntegrationAuth);
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.Integration))) {
|
||||||
|
await knex.schema.createTable(TableName.Integration, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.boolean("isActive").notNullable();
|
||||||
|
t.string("url"); // self hosted
|
||||||
|
t.string("app"); // name of app in provider
|
||||||
|
t.string("appId");
|
||||||
|
t.string("targetEnvironment");
|
||||||
|
t.string("targetEnvironmentId");
|
||||||
|
t.string("targetService"); // railway - qovery specific
|
||||||
|
t.string("targetServiceId");
|
||||||
|
t.string("owner"); // github specific
|
||||||
|
t.string("path"); // aws parameter store / vercel preview branch
|
||||||
|
t.string("region"); // aws
|
||||||
|
t.string("scope"); // qovery specific scope
|
||||||
|
t.string("integration").notNullable();
|
||||||
|
t.jsonb("metadata");
|
||||||
|
t.uuid("integrationAuthId").notNullable();
|
||||||
|
t.foreign("integrationAuthId")
|
||||||
|
.references("id")
|
||||||
|
.inTable(TableName.IntegrationAuth)
|
||||||
|
.onDelete("CASCADE");
|
||||||
|
t.uuid("envId").notNullable();
|
||||||
|
t.string("secretPath").defaultTo("/").notNullable();
|
||||||
|
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await createOnUpdateTrigger(knex, TableName.Integration);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IntegrationAuth);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.Integration);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.IntegrationAuth);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.Integration);
|
||||||
|
}
|
||||||
@@ -3,10 +3,13 @@ export * from "./auth-token-sessions";
|
|||||||
export * from "./auth-tokens";
|
export * from "./auth-tokens";
|
||||||
export * from "./backup-private-key";
|
export * from "./backup-private-key";
|
||||||
export * from "./incident-contacts";
|
export * from "./incident-contacts";
|
||||||
|
export * from "./integration-auths";
|
||||||
|
export * from "./integrations";
|
||||||
export * from "./models";
|
export * from "./models";
|
||||||
export * from "./org-memberships";
|
export * from "./org-memberships";
|
||||||
export * from "./org-roles";
|
export * from "./org-roles";
|
||||||
export * from "./organizations";
|
export * from "./organizations";
|
||||||
|
export * from "./project-bots";
|
||||||
export * from "./project-environments";
|
export * from "./project-environments";
|
||||||
export * from "./project-keys";
|
export * from "./project-keys";
|
||||||
export * from "./project-memberships";
|
export * from "./project-memberships";
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const IntegrationAuthsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
integration: z.string(),
|
||||||
|
teamId: z.string().nullable().optional(),
|
||||||
|
url: z.string().nullable().optional(),
|
||||||
|
namespace: z.string().nullable().optional(),
|
||||||
|
accountId: z.string().nullable().optional(),
|
||||||
|
refreshCiphertext: z.string().nullable().optional(),
|
||||||
|
refreshIV: z.string().nullable().optional(),
|
||||||
|
refreshTag: z.string().nullable().optional(),
|
||||||
|
accessIdCiphertext: z.string().nullable().optional(),
|
||||||
|
accessIdIV: z.string().nullable().optional(),
|
||||||
|
accessIdTag: z.string().nullable().optional(),
|
||||||
|
accessCiphertext: z.string().nullable().optional(),
|
||||||
|
accessIV: z.string().nullable().optional(),
|
||||||
|
accessTag: z.string().nullable().optional(),
|
||||||
|
accessExpiresAt: z.date().nullable().optional(),
|
||||||
|
metadata: z.unknown().nullable().optional(),
|
||||||
|
algorithm: z.string(),
|
||||||
|
keyEncoding: z.string(),
|
||||||
|
projectId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIntegrationAuths = z.infer<typeof IntegrationAuthsSchema>;
|
||||||
|
export type TIntegrationAuthsInsert = Omit<TIntegrationAuths, TImmutableDBKeys>;
|
||||||
|
export type TIntegrationAuthsUpdate = Partial<Omit<TIntegrationAuths, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const IntegrationsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
isActive: z.boolean(),
|
||||||
|
url: z.string().nullable().optional(),
|
||||||
|
app: z.string().nullable().optional(),
|
||||||
|
appId: z.string().nullable().optional(),
|
||||||
|
targetEnvironment: z.string().nullable().optional(),
|
||||||
|
targetEnvironmentId: z.string().nullable().optional(),
|
||||||
|
targetService: z.string().nullable().optional(),
|
||||||
|
targetServiceId: z.string().nullable().optional(),
|
||||||
|
owner: z.string().nullable().optional(),
|
||||||
|
path: z.string().nullable().optional(),
|
||||||
|
region: z.string().nullable().optional(),
|
||||||
|
scope: z.string().nullable().optional(),
|
||||||
|
integration: z.string(),
|
||||||
|
metadata: z.unknown().nullable().optional(),
|
||||||
|
integrationAuthId: z.string().uuid(),
|
||||||
|
envId: z.string().uuid(),
|
||||||
|
secretPath: z.string().default('/'),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIntegrations = z.infer<typeof IntegrationsSchema>;
|
||||||
|
export type TIntegrationsInsert = Omit<TIntegrations, TImmutableDBKeys>;
|
||||||
|
export type TIntegrationsUpdate = Partial<Omit<TIntegrations, TImmutableDBKeys>>;
|
||||||
@@ -14,6 +14,7 @@ export enum TableName {
|
|||||||
SuperAdmin = "super_admin",
|
SuperAdmin = "super_admin",
|
||||||
ApiKey = "api_keys",
|
ApiKey = "api_keys",
|
||||||
Project = "projects",
|
Project = "projects",
|
||||||
|
ProjectBot = "project_bots",
|
||||||
Environment = "project_environments",
|
Environment = "project_environments",
|
||||||
ProjectMembership = "project_memberships",
|
ProjectMembership = "project_memberships",
|
||||||
ProjectRoles = "project_roles",
|
ProjectRoles = "project_roles",
|
||||||
@@ -24,6 +25,8 @@ export enum TableName {
|
|||||||
SecretFolder = "secret_folders",
|
SecretFolder = "secret_folders",
|
||||||
SecretImport = "secret_imports",
|
SecretImport = "secret_imports",
|
||||||
SecretTag = "secret_tags",
|
SecretTag = "secret_tags",
|
||||||
|
Integration = "integrations",
|
||||||
|
IntegrationAuth = "integration_auths",
|
||||||
JnSecretTag = "secret_tag_junction",
|
JnSecretTag = "secret_tag_junction",
|
||||||
JnSecretVersionTag = "secret_version_tag_junction"
|
JnSecretVersionTag = "secret_version_tag_junction"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const ProjectBotsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
isActive: z.boolean().default(false),
|
||||||
|
encryptedPrivateKey: z.string(),
|
||||||
|
publicKey: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
algorithm: z.string(),
|
||||||
|
keyEncoding: z.string(),
|
||||||
|
encryptedProjectKey: z.string().optional().nullable(),
|
||||||
|
encryptedProjectKeyNonce: z.string().optional().nullable(),
|
||||||
|
projectId: z.string().uuid(),
|
||||||
|
senderId: z.string().uuid().optional().nullable(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TProjectBots = z.infer<typeof ProjectBotsSchema>;
|
||||||
|
export type TProjectBotsInsert = Omit<TProjectBots, TImmutableDBKeys>;
|
||||||
|
export type TProjectBotsUpdate = Partial<Omit<TProjectBots, TImmutableDBKeys>>;
|
||||||
@@ -3,6 +3,8 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { zpStr } from "../zod";
|
import { zpStr } from "../zod";
|
||||||
|
|
||||||
|
export const GITLAB_URL = "https://gitlab.com";
|
||||||
|
|
||||||
const zodStrBool = z
|
const zodStrBool = z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.optional()
|
.optional()
|
||||||
@@ -46,7 +48,34 @@ const envSchema = z
|
|||||||
CLIENT_SECRET_GITHUB_LOGIN: zpStr(z.string().optional()),
|
CLIENT_SECRET_GITHUB_LOGIN: zpStr(z.string().optional()),
|
||||||
CLIENT_ID_GITLAB_LOGIN: zpStr(z.string().optional()),
|
CLIENT_ID_GITLAB_LOGIN: zpStr(z.string().optional()),
|
||||||
CLIENT_SECRET_GITLAB_LOGIN: zpStr(z.string().optional()),
|
CLIENT_SECRET_GITLAB_LOGIN: zpStr(z.string().optional()),
|
||||||
CLIENT_GITLAB_LOGIN_URL: zpStr(z.string().optional())
|
CLIENT_GITLAB_LOGIN_URL: zpStr(z.string().optional().default(GITLAB_URL)),
|
||||||
|
// integration client secrets
|
||||||
|
// heroku
|
||||||
|
CLIENT_ID_HEROKU: zpStr(z.string().optional()),
|
||||||
|
CLIENT_SECRET_HEROKU: zpStr(z.string().optional()),
|
||||||
|
// vercel
|
||||||
|
CLIENT_ID_VERCEL: zpStr(z.string().optional()),
|
||||||
|
CLIENT_SECRET_VERCEL: zpStr(z.string().optional()),
|
||||||
|
CLIENT_SLUG_VERCEL: zpStr(z.string().optional()),
|
||||||
|
// netlify
|
||||||
|
CLIENT_ID_NETLIFY: zpStr(z.string().optional()),
|
||||||
|
CLIENT_SECRET_NETLIFY: zpStr(z.string().optional()),
|
||||||
|
// bit bucket
|
||||||
|
CLIENT_ID_BITBUCKET: zpStr(z.string().optional()),
|
||||||
|
CLIENT_SECRET_BITBUCKET: zpStr(z.string().optional()),
|
||||||
|
// gcp secret manager
|
||||||
|
CLIENT_ID_GCP_SECRET_MANAGER: zpStr(z.string().optional()),
|
||||||
|
CLIENT_SECRET_GCP_SECRET_MANAGER: zpStr(z.string().optional()),
|
||||||
|
// github
|
||||||
|
CLIENT_ID_GITHUB: zpStr(z.string().optional()),
|
||||||
|
CLIENT_SECRET_GITHUB: zpStr(z.string().optional()),
|
||||||
|
// azure
|
||||||
|
CLIENT_ID_AZURE: zpStr(z.string().optional()),
|
||||||
|
CLIENT_SECRET_AZURE: zpStr(z.string().optional()),
|
||||||
|
// google
|
||||||
|
CLIENT_ID_GITLAB: zpStr(z.string().optional()),
|
||||||
|
CLIENT_SECRET_GITLAB: zpStr(z.string().optional()),
|
||||||
|
URL_GITLAB_URL: zpStr(z.string().optional().default(GITLAB_URL))
|
||||||
})
|
})
|
||||||
.transform((data) => ({ ...data, isSmtpConfigured: Boolean(data.SMTP_HOST) }));
|
.transform((data) => ({ ...data, isSmtpConfigured: Boolean(data.SMTP_HOST) }));
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,15 @@ export const decryptAsymmetric = ({
|
|||||||
return naclUtils.encodeUTF8(plaintext);
|
return naclUtils.encodeUTF8(plaintext);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateAsymmetricKeyPair = () => {
|
||||||
|
const pair = nacl.box.keyPair();
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey: naclUtils.encodeBase64(pair.publicKey),
|
||||||
|
privateKey: naclUtils.encodeBase64(pair.secretKey)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type TGenSecretBlindIndex = {
|
export type TGenSecretBlindIndex = {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
keyEncoding: SecretKeyEncoding;
|
keyEncoding: SecretKeyEncoding;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export {
|
|||||||
decryptSymmetric128BitHexKeyUTF8,
|
decryptSymmetric128BitHexKeyUTF8,
|
||||||
encryptAsymmetric,
|
encryptAsymmetric,
|
||||||
encryptSymmetric,
|
encryptSymmetric,
|
||||||
encryptSymmetric128BitHexKeyUTF8
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
|
generateAsymmetricKeyPair
|
||||||
} from "./encryption";
|
} from "./encryption";
|
||||||
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Knex } from "knex";
|
|
||||||
import { Tables } from "knex/types/tables";
|
import { Tables } from "knex/types/tables";
|
||||||
|
|
||||||
export const selectAllTableCols = <Tname extends keyof Tables>(db: Knex, tableName: Tname) =>
|
export const selectAllTableCols = <Tname extends keyof Tables>(tableName: Tname) =>
|
||||||
db.ref("*").withSchema(tableName) as unknown as keyof Tables[Tname];
|
`${tableName}.*` as keyof Tables[Tname]["base"];
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
|
|||||||
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
||||||
import { tokenDalFactory } from "@app/services/auth-token/auth-token-dal";
|
import { tokenDalFactory } from "@app/services/auth-token/auth-token-dal";
|
||||||
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { integrationDalFactory } from "@app/services/integration/integration-dal";
|
||||||
|
import { integrationServiceFactory } from "@app/services/integration/integration-service";
|
||||||
|
import { integrationAuthDalFactory } from "@app/services/integration-auth/integration-auth-dal";
|
||||||
|
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||||
import { incidentContactDalFactory } from "@app/services/org/incident-contacts-dal";
|
import { incidentContactDalFactory } from "@app/services/org/incident-contacts-dal";
|
||||||
import { orgDalFactory } from "@app/services/org/org-dal";
|
import { orgDalFactory } from "@app/services/org/org-dal";
|
||||||
import { orgRoleDalFactory } from "@app/services/org/org-role-dal";
|
import { orgRoleDalFactory } from "@app/services/org/org-role-dal";
|
||||||
@@ -20,6 +24,8 @@ import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
|||||||
import { orgServiceFactory } from "@app/services/org/org-service";
|
import { orgServiceFactory } from "@app/services/org/org-service";
|
||||||
import { projectDalFactory } from "@app/services/project/project-dal";
|
import { projectDalFactory } from "@app/services/project/project-dal";
|
||||||
import { projectServiceFactory } from "@app/services/project/project-service";
|
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||||
|
import { projectBotDalFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
|
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { projectEnvDalFactory } from "@app/services/project-env/project-env-dal";
|
import { projectEnvDalFactory } from "@app/services/project-env/project-env-dal";
|
||||||
import { projectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
import { projectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||||
import { projectKeyDalFactory } from "@app/services/project-key/project-key-dal";
|
import { projectKeyDalFactory } from "@app/services/project-key/project-key-dal";
|
||||||
@@ -67,6 +73,7 @@ export const registerRoutes = async (
|
|||||||
const projectRoleDal = projectRoleDalFactory(db);
|
const projectRoleDal = projectRoleDalFactory(db);
|
||||||
const projectEnvDal = projectEnvDalFactory(db);
|
const projectEnvDal = projectEnvDalFactory(db);
|
||||||
const projectKeyDal = projectKeyDalFactory(db);
|
const projectKeyDal = projectKeyDalFactory(db);
|
||||||
|
const projectBotDal = projectBotDalFactory(db);
|
||||||
|
|
||||||
const secretDal = secretDalFactory(db);
|
const secretDal = secretDalFactory(db);
|
||||||
const folderDal = secretFolderDalFactory(db);
|
const folderDal = secretFolderDalFactory(db);
|
||||||
@@ -74,6 +81,9 @@ export const registerRoutes = async (
|
|||||||
const secretVersionDal = secretVersionDalFactory(db);
|
const secretVersionDal = secretVersionDalFactory(db);
|
||||||
const secretBlindIndexDal = secretBlindIndexDalFactory(db);
|
const secretBlindIndexDal = secretBlindIndexDalFactory(db);
|
||||||
|
|
||||||
|
const integrationDal = integrationDalFactory(db);
|
||||||
|
const integrationAuthDal = integrationAuthDalFactory(db);
|
||||||
|
|
||||||
// ee db layer ops
|
// ee db layer ops
|
||||||
const permissionDal = permissionDalFactory(db);
|
const permissionDal = permissionDalFactory(db);
|
||||||
|
|
||||||
@@ -159,6 +169,20 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
secretImportDal
|
secretImportDal
|
||||||
});
|
});
|
||||||
|
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDal });
|
||||||
|
const integrationService = integrationServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
folderDal,
|
||||||
|
integrationDal,
|
||||||
|
integrationAuthDal
|
||||||
|
});
|
||||||
|
const integrationAuthService = integrationAuthServiceFactory({
|
||||||
|
integrationAuthDal,
|
||||||
|
integrationDal,
|
||||||
|
permissionService,
|
||||||
|
projectBotDal,
|
||||||
|
projectBotService
|
||||||
|
});
|
||||||
|
|
||||||
await superAdminService.initServerCfg();
|
await superAdminService.initServerCfg();
|
||||||
// inject all services
|
// inject all services
|
||||||
@@ -180,7 +204,10 @@ export const registerRoutes = async (
|
|||||||
projectRole: projectRoleService,
|
projectRole: projectRoleService,
|
||||||
secret: secretService,
|
secret: secretService,
|
||||||
folder: folderService,
|
folder: folderService,
|
||||||
secretImport: secretImportService
|
secretImport: secretImportService,
|
||||||
|
projectBot: projectBotService,
|
||||||
|
integration: integrationService,
|
||||||
|
integrationAuth: integrationAuthService
|
||||||
});
|
});
|
||||||
|
|
||||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ProjectBotsSchema } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/:projectId",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
bot: ProjectBotsSchema.pick({
|
||||||
|
name: true,
|
||||||
|
projectId: true,
|
||||||
|
isActive: true,
|
||||||
|
publicKey: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const bot = await server.services.projectBot.findBotByProjectId({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
return { bot };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:botId/active",
|
||||||
|
method: "PATCH",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
isActive: z.boolean(),
|
||||||
|
botKey: z
|
||||||
|
.object({
|
||||||
|
nonce: z.string().trim().optional(),
|
||||||
|
encryptedKey: z.string().trim().optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
botId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
bot: ProjectBotsSchema.pick({
|
||||||
|
name: true,
|
||||||
|
projectId: true,
|
||||||
|
isActive: true,
|
||||||
|
publicKey: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const bot = await server.services.projectBot.setBotActiveState({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
botId: req.params.botId,
|
||||||
|
botKey: req.body.botKey,
|
||||||
|
isActive: req.body.isActive
|
||||||
|
});
|
||||||
|
return { bot };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { registerAdminRouter } from "./admin-router";
|
import { registerAdminRouter } from "./admin-router";
|
||||||
import { registerAuthRoutes } from "./auth-router";
|
import { registerAuthRoutes } from "./auth-router";
|
||||||
|
import { registerProjectBotRouter } from "./bot-router";
|
||||||
|
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
||||||
|
import { registerIntegrationRouter } from "./integration-router";
|
||||||
import { registerInviteOrgRouter } from "./invite-org-router";
|
import { registerInviteOrgRouter } from "./invite-org-router";
|
||||||
import { registerOrgRouter } from "./organization-router";
|
import { registerOrgRouter } from "./organization-router";
|
||||||
import { registerPasswordRouter } from "./password-router";
|
import { registerPasswordRouter } from "./password-router";
|
||||||
@@ -34,4 +37,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(registerProjectBotRouter, { prefix: "/bot" });
|
||||||
|
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
||||||
|
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,565 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { IntegrationAuthsSchema } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/integration-options",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
integrationOptions: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
image: z.string(),
|
||||||
|
isAvailable: z.boolean().optional(),
|
||||||
|
type: z.string(),
|
||||||
|
clientId: z.string().optional(),
|
||||||
|
docsLink: z.string().optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async () => {
|
||||||
|
const integrationOptions = await server.services.integrationAuth.getIntegrationOptions();
|
||||||
|
return { integrationOptions };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const integrationPublicSchema = IntegrationAuthsSchema.pick({
|
||||||
|
projectId: true,
|
||||||
|
integration: true,
|
||||||
|
teamId: true,
|
||||||
|
url: true,
|
||||||
|
namespace: true,
|
||||||
|
accountId: true,
|
||||||
|
metadata: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
integrationAuth: integrationPublicSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const integrationAuth = await server.services.integrationAuth.getIntegrationAuth({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId
|
||||||
|
});
|
||||||
|
return { integrationAuth };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId",
|
||||||
|
method: "DELETE",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
integrationAuth: integrationPublicSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const integrationAuth = await server.services.integrationAuth.deleteIntegrationAuth({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId
|
||||||
|
});
|
||||||
|
return { integrationAuth };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/oauth-token",
|
||||||
|
method: "POST",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
workspaceId: z.string().trim(),
|
||||||
|
integration: z.string().trim(),
|
||||||
|
accessId: z.string().trim().optional(),
|
||||||
|
accessToken: z.string().trim().optional(),
|
||||||
|
url: z.string().url().trim().optional(),
|
||||||
|
namespace: z.string().trim().optional(),
|
||||||
|
refreshToken: z.string().trim().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
integrationAuth: integrationPublicSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const integrationAuth = await server.services.integrationAuth.saveIntegrationToken({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.body.workspaceId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
return { integrationAuth };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/apps",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
teamId: z.string().trim().optional(),
|
||||||
|
workspaceSlug: z.string().trim().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
apps: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
appId: z.string().optional(),
|
||||||
|
owner: z.string().optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const apps = await server.services.integrationAuth.getIntegrationApps({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
...req.query
|
||||||
|
});
|
||||||
|
return { apps };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/teams",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
teams: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string().optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const teams = await server.services.integrationAuth.getIntegrationAuthTeams({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId
|
||||||
|
});
|
||||||
|
return { teams };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/vercel/branches",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
appId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
branches: z.string().array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const branches = await server.services.integrationAuth.getVercelBranches({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
appId: req.query.appId
|
||||||
|
});
|
||||||
|
return { branches };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/checkly/groups",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
accountId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
groups: z.object({ name: z.string(), groupId: z.number() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const groups = await server.services.integrationAuth.getChecklyGroups({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
accountId: req.query.accountId
|
||||||
|
});
|
||||||
|
return { groups };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/qovery/orgs",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
orgs: z.object({ name: z.string(), orgId: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const orgs = await server.services.integrationAuth.getQoveryOrgs({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId
|
||||||
|
});
|
||||||
|
return { orgs };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/qovery/projects",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
orgId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
projects: z.object({ name: z.string(), projectId: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const projects = await server.services.integrationAuth.getQoveryProjects({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
orgId: req.query.orgId
|
||||||
|
});
|
||||||
|
return { projects };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/qovery/environments",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
environments: z.object({ name: z.string(), environmentId: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const environments = await server.services.integrationAuth.getQoveryEnvs({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
projectId: req.query.projectId
|
||||||
|
});
|
||||||
|
return { environments };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/qovery/apps",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
environmentId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
apps: z.object({ name: z.string(), appId: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const apps = await server.services.integrationAuth.getQoveryApps({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
environmentId: req.query.environmentId
|
||||||
|
});
|
||||||
|
return { apps };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/qovery/containers",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
environmentId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
containers: z.object({ name: z.string(), appId: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const containers = await server.services.integrationAuth.getQoveryContainers({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
environmentId: req.query.environmentId
|
||||||
|
});
|
||||||
|
return { containers };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/qovery/jobs",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
environmentId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
jobs: z.object({ name: z.string(), appId: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const jobs = await server.services.integrationAuth.getQoveryJobs({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
environmentId: req.query.environmentId
|
||||||
|
});
|
||||||
|
return { jobs };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/railway/environments",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
appId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
environments: z.object({ name: z.string(), environmentId: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const environments = await server.services.integrationAuth.getRailwayEnvironments({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
appId: req.query.appId
|
||||||
|
});
|
||||||
|
return { environments };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/railway/services",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
appId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
services: z.object({ name: z.string(), serviceId: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const services = await server.services.integrationAuth.getRailwayServices({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
appId: req.query.appId
|
||||||
|
});
|
||||||
|
return { services };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/bitbucket/workspaces",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspaces: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
uuid: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
is_private: z.boolean(),
|
||||||
|
created_on: z.string(),
|
||||||
|
updated_on: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspaces = await server.services.integrationAuth.getBitbucketWorkspaces({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId
|
||||||
|
});
|
||||||
|
return { workspaces };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/northflank/secret-groups",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
appId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretGroups: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
groupId: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretGroups = await server.services.integrationAuth.getNorthFlankSecretGroups({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
appId: req.query.appId
|
||||||
|
});
|
||||||
|
return { secretGroups };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationAuthId/teamcity/build-configs",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
appId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
buildConfigs: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
buildConfigId: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const buildConfigs = await server.services.integrationAuth.getTeamcityBuildConfigs({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
appId: req.query.appId
|
||||||
|
});
|
||||||
|
return { buildConfigs };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { IntegrationsSchema } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
integrationAuthId: z.string().trim(),
|
||||||
|
app: z.string().trim().optional(),
|
||||||
|
isActive: z.boolean(),
|
||||||
|
appId: z.string().trim().optional(),
|
||||||
|
secretPath: z.string().trim().default("/"),
|
||||||
|
sourceEnvironment: z.string().trim(),
|
||||||
|
targetEnvironment: z.string().trim().optional(),
|
||||||
|
targetEnvironmentId: z.string().trim().optional(),
|
||||||
|
targetService: z.string().trim().optional(),
|
||||||
|
targetServiceId: z.string().trim().optional(),
|
||||||
|
owner: z.string().trim().optional(),
|
||||||
|
path: z.string().trim().optional(),
|
||||||
|
region: z.string().trim().optional(),
|
||||||
|
scope: z.string().trim().optional(),
|
||||||
|
metadata: z
|
||||||
|
.object({
|
||||||
|
secretPrefix: z.string().optional(),
|
||||||
|
secretSuffix: z.string().optional(),
|
||||||
|
secretGCPLabel: z
|
||||||
|
.object({
|
||||||
|
labelName: z.string(),
|
||||||
|
labelValue: z.string()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
integration: IntegrationsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const integration = await server.services.integration.createIntegration({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
return { integration };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationId",
|
||||||
|
method: "PATCH",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
app: z.string().trim(),
|
||||||
|
appId: z.string().trim(),
|
||||||
|
isActive: z.boolean(),
|
||||||
|
secretPath: z.string().trim().default("/"),
|
||||||
|
targetEnvironment: z.string().trim(),
|
||||||
|
owner: z.string().trim(),
|
||||||
|
environment: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
integration: IntegrationsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const integration = await server.services.integration.updateIntegration({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
return { integration };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:integrationId",
|
||||||
|
method: "DELETE",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
integration: IntegrationsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const integration = await server.services.integration.deleteIntegration({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
id: req.params.integrationId
|
||||||
|
});
|
||||||
|
return { integration };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(akhilmhdh-pg): manual sync
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TIntegrationAuthDalFactory = ReturnType<typeof integrationAuthDalFactory>;
|
||||||
|
|
||||||
|
export const integrationAuthDalFactory = (db: TDbClient) => {
|
||||||
|
const integrationAuthOrm = ormify(db, TableName.IntegrationAuth);
|
||||||
|
return integrationAuthOrm;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,134 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TGetIntegrationAuthDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TOauthExchangeDTO = {
|
||||||
|
integration: string;
|
||||||
|
code: string;
|
||||||
|
url: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TSaveIntegrationAccessTokenDTO = {
|
||||||
|
integration: string;
|
||||||
|
accessId?: string;
|
||||||
|
accessToken?: string;
|
||||||
|
url?: string;
|
||||||
|
namespace?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TIntegrationAuthAppsDTO = {
|
||||||
|
id: string;
|
||||||
|
teamId?: string;
|
||||||
|
workspaceSlug?: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthTeamsDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthVercelBranchesDTO = {
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthChecklyGroupsDTO = {
|
||||||
|
id: string;
|
||||||
|
accountId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthQoveryOrgsDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthQoveryProjectDTO = {
|
||||||
|
id: string;
|
||||||
|
orgId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthQoveryEnvironmentsDTO = {
|
||||||
|
id: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TIntegrationAuthQoveryScopesDTO = {
|
||||||
|
id: string;
|
||||||
|
environmentId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthRailwayEnvDTO = {
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthRailwayServicesDTO = {
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthBitbucketWorkspaceDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthNorthflankSecretGroupDTO = {
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteIntegrationAuthDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TVercelBranches = {
|
||||||
|
ref: string;
|
||||||
|
lastCommit: string;
|
||||||
|
isProtected: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TChecklyGroups = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TQoveryProjects = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TQoveryEnvironments = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBitbucketWorkspace = {
|
||||||
|
type: string;
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
is_private: boolean;
|
||||||
|
created_on: string;
|
||||||
|
updated_on: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TNorthflankSecretGroup = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
priority: number;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTeamCityBuildConfig = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
projectName: string;
|
||||||
|
projectId: string;
|
||||||
|
href: string;
|
||||||
|
webUrl: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
|
export enum Integrations {
|
||||||
|
AZURE_KEY_VAULT = "azure-key-vault",
|
||||||
|
AWS_PARAMETER_STORE = "aws-parameter-store",
|
||||||
|
AWS_SECRET_MANAGER = "aws-secret-manager",
|
||||||
|
GCP_SECRET_MANAGER = "gcp-secret-manager",
|
||||||
|
HEROKU = "heroku",
|
||||||
|
VERCEL = "vercel",
|
||||||
|
NETLIFY = "netlify",
|
||||||
|
GITHUB = "github",
|
||||||
|
GITLAB = "gitlab",
|
||||||
|
RENDER = "render",
|
||||||
|
RAILWAY = "railway",
|
||||||
|
FLYIO = "flyio",
|
||||||
|
LARAVELFORGE = "laravel-forge",
|
||||||
|
CIRCLECI = "circleci",
|
||||||
|
TRAVISCI = "travisci",
|
||||||
|
TEAMCITY = "teamcity",
|
||||||
|
SUPABASE = "supabase",
|
||||||
|
CHECKLY = "checkly",
|
||||||
|
QOVERY = "qovery",
|
||||||
|
TERRAFORM_CLOUD = "terraform-cloud",
|
||||||
|
HASHICORP_VAULT = "hashicorp-vault",
|
||||||
|
CLOUDFLARE_PAGES = "cloudflare-pages",
|
||||||
|
CLOUDFLARE_WORKERS = "cloudflare-workers",
|
||||||
|
BITBUCKET = "bitbucket",
|
||||||
|
CODEFRESH = "codefresh",
|
||||||
|
WINDMILL = "windmill",
|
||||||
|
DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform",
|
||||||
|
CLOUD_66 = "cloud-66",
|
||||||
|
NORTHFLANK = "northflank",
|
||||||
|
HASURA_CLOUD = "hasura-cloud"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum IntegrationType {
|
||||||
|
OAUTH2 = "oauth2"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum IntegrationUrls {
|
||||||
|
// integration oauth endpoints
|
||||||
|
GCP_TOKEN_URL = "https://oauth2.googleapis.com/token",
|
||||||
|
AZURE_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||||
|
HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token",
|
||||||
|
VERCEL_TOKEN_URL = "https://api.vercel.com/v2/oauth/access_token",
|
||||||
|
NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token",
|
||||||
|
GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token",
|
||||||
|
GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token",
|
||||||
|
BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token",
|
||||||
|
|
||||||
|
// integration apps endpoints
|
||||||
|
GCP_API_URL = "https://cloudresourcemanager.googleapis.com",
|
||||||
|
HEROKU_API_URL = "https://api.heroku.com",
|
||||||
|
GITLAB_URL = "https://gitlab.com",
|
||||||
|
GITLAB_API_URL = `${GITLAB_URL}/api`,
|
||||||
|
GITHUB_API_URL = "https://api.github.com",
|
||||||
|
VERCEL_API_URL = "https://api.vercel.com",
|
||||||
|
NETLIFY_API_URL = "https://api.netlify.com",
|
||||||
|
RENDER_API_URL = "https://api.render.com",
|
||||||
|
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
|
||||||
|
FLYIO_API_URL = "https://api.fly.io/graphql",
|
||||||
|
CIRCLECI_API_URL = "https://circleci.com/api",
|
||||||
|
TRAVISCI_API_URL = "https://api.travis-ci.com",
|
||||||
|
SUPABASE_API_URL = "https://api.supabase.com",
|
||||||
|
LARAVELFORGE_API_URL = "https://forge.laravel.com",
|
||||||
|
CHECKLY_API_URL = "https://api.checklyhq.com",
|
||||||
|
QOVERY_API_URL = "https://api.qovery.com",
|
||||||
|
TERRAFORM_CLOUD_API_URL = "https://app.terraform.io",
|
||||||
|
CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com",
|
||||||
|
CLOUDFLARE_WORKERS_API_URL = "https://api.cloudflare.com",
|
||||||
|
BITBUCKET_API_URL = "https://api.bitbucket.org",
|
||||||
|
CODEFRESH_API_URL = "https://g.codefresh.io/api",
|
||||||
|
WINDMILL_API_URL = "https://app.windmill.dev/api",
|
||||||
|
DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com",
|
||||||
|
CLOUD_66_API_URL = "https://app.cloud66.com/api",
|
||||||
|
NORTHFLANK_API_URL = "https://api.northflank.com",
|
||||||
|
HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql",
|
||||||
|
|
||||||
|
GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com",
|
||||||
|
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
|
||||||
|
GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com",
|
||||||
|
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getIntegrationOptions = async () => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const INTEGRATION_OPTIONS = [
|
||||||
|
{
|
||||||
|
name: "Heroku",
|
||||||
|
slug: "heroku",
|
||||||
|
image: "Heroku.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "oauth",
|
||||||
|
clientId: appCfg.CLIENT_ID_HEROKU,
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Vercel",
|
||||||
|
slug: "vercel",
|
||||||
|
image: "Vercel.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "oauth",
|
||||||
|
clientId: "",
|
||||||
|
clientSlug: appCfg.CLIENT_ID_VERCEL,
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Netlify",
|
||||||
|
slug: "netlify",
|
||||||
|
image: "Netlify.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "oauth",
|
||||||
|
clientId: appCfg.CLIENT_ID_NETLIFY,
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitHub",
|
||||||
|
slug: "github",
|
||||||
|
image: "GitHub.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "oauth",
|
||||||
|
clientId: appCfg.CLIENT_ID_BITBUCKET,
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Render",
|
||||||
|
slug: "render",
|
||||||
|
image: "Render.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Railway",
|
||||||
|
slug: "railway",
|
||||||
|
image: "Railway.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Fly.io",
|
||||||
|
slug: "flyio",
|
||||||
|
image: "Flyio.svg",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AWS Parameter Store",
|
||||||
|
slug: "aws-parameter-store",
|
||||||
|
image: "Amazon Web Services.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "custom",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Laravel Forge",
|
||||||
|
slug: "laravel-forge",
|
||||||
|
image: "Laravel Forge.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AWS Secrets Manager",
|
||||||
|
slug: "aws-secret-manager",
|
||||||
|
image: "Amazon Web Services.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "custom",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Azure Key Vault",
|
||||||
|
slug: "azure-key-vault",
|
||||||
|
image: "Microsoft Azure.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "oauth",
|
||||||
|
clientId: appCfg.CLIENT_ID_AZURE,
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Circle CI",
|
||||||
|
slug: "circleci",
|
||||||
|
image: "Circle CI.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitLab",
|
||||||
|
slug: "gitlab",
|
||||||
|
image: "GitLab.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "custom",
|
||||||
|
clientId: appCfg.CLIENT_ID_GITLAB,
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Terraform Cloud",
|
||||||
|
slug: "terraform-cloud",
|
||||||
|
image: "Terraform Cloud.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
cliendId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Travis CI",
|
||||||
|
slug: "travisci",
|
||||||
|
image: "Travis CI.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TeamCity",
|
||||||
|
slug: "teamcity",
|
||||||
|
image: "TeamCity.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Supabase",
|
||||||
|
slug: "supabase",
|
||||||
|
image: "Supabase.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Checkly",
|
||||||
|
slug: "checkly",
|
||||||
|
image: "Checkly.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Qovery",
|
||||||
|
slug: "qovery",
|
||||||
|
image: "Qovery.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HashiCorp Vault",
|
||||||
|
slug: "hashicorp-vault",
|
||||||
|
image: "Vault.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GCP Secret Manager",
|
||||||
|
slug: "gcp-secret-manager",
|
||||||
|
image: "Google Cloud Platform.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "oauth",
|
||||||
|
clientId: appCfg.CLIENT_ID_GCP_SECRET_MANAGER,
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cloudflare Pages",
|
||||||
|
slug: "cloudflare-pages",
|
||||||
|
image: "Cloudflare.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cloudflare Workers",
|
||||||
|
slug: "cloudflare-workers",
|
||||||
|
image: "Cloudflare.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "BitBucket",
|
||||||
|
slug: "bitbucket",
|
||||||
|
image: "BitBucket.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "oauth",
|
||||||
|
clientId: appCfg.CLIENT_ID_BITBUCKET,
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Codefresh",
|
||||||
|
slug: "codefresh",
|
||||||
|
image: "Codefresh.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Windmill",
|
||||||
|
slug: "windmill",
|
||||||
|
image: "Windmill.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Digital Ocean App Platform",
|
||||||
|
slug: "digital-ocean-app-platform",
|
||||||
|
image: "Digital Ocean.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cloud 66",
|
||||||
|
slug: "cloud-66",
|
||||||
|
image: "Cloud 66.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Northflank",
|
||||||
|
slug: "northflank",
|
||||||
|
image: "Northflank.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hasura Cloud",
|
||||||
|
slug: "hasura-cloud",
|
||||||
|
image: "Hasura.svg",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return INTEGRATION_OPTIONS;
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { Integrations,IntegrationUrls } from "./integration-list";
|
||||||
|
|
||||||
|
type Team = {
|
||||||
|
name: string;
|
||||||
|
teamId: string;
|
||||||
|
};
|
||||||
|
const getTeamsGitLab = async ({ url, accessToken }: { url: string; accessToken: string }) => {
|
||||||
|
const gitLabApiUrl = url ? `${url}/api` : IntegrationUrls.GITLAB_API_URL;
|
||||||
|
|
||||||
|
let teams: Team[] = [];
|
||||||
|
const res = (
|
||||||
|
await request.get(`${gitLabApiUrl}/v4/groups`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
teams = res.map((t: any) => ({
|
||||||
|
name: t.name,
|
||||||
|
teamId: t.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
return teams;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTeams = async ({
|
||||||
|
accessToken,
|
||||||
|
url,
|
||||||
|
integration
|
||||||
|
}: {
|
||||||
|
accessToken: string;
|
||||||
|
url?: string;
|
||||||
|
integration: string;
|
||||||
|
}) => {
|
||||||
|
switch (integration) {
|
||||||
|
case Integrations.GITLAB:
|
||||||
|
return getTeamsGitLab({
|
||||||
|
url: url as string,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: "Integration doesn't have team support" });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,725 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { Integrations,IntegrationUrls } from "./integration-list";
|
||||||
|
|
||||||
|
type ExchangeCodeAzureResponse = {
|
||||||
|
token_type: string;
|
||||||
|
scope: string;
|
||||||
|
expires_in: number;
|
||||||
|
ext_expires_in: number;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
id_token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExchangeCodeGCPResponse = {
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
scope: string;
|
||||||
|
token_type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExchangeCodeHerokuResponse = {
|
||||||
|
token_type: string;
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
user_id: string;
|
||||||
|
session_nonce?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExchangeCodeVercelResponse = {
|
||||||
|
token_type: string;
|
||||||
|
access_token: string;
|
||||||
|
installation_id: string;
|
||||||
|
user_id: string;
|
||||||
|
team_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExchangeCodeNetlifyResponse = {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
refresh_token: string;
|
||||||
|
scope: string;
|
||||||
|
created_at: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExchangeCodeGithubResponse = {
|
||||||
|
access_token: string;
|
||||||
|
scope: string;
|
||||||
|
token_type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExchangeCodeGitlabResponse = {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
scope: string;
|
||||||
|
created_at: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExchangeCodeBitBucketResponse = {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
scopes: string;
|
||||||
|
state: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const exchangeCodeGCP = async ({ code }: { code: string }) => {
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_SECRET_GCP_SECRET_MANAGER || !appCfg.CLIENT_ID_GCP_SECRET_MANAGER) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ExchangeCodeGCPResponse = (
|
||||||
|
await request.post(
|
||||||
|
IntegrationUrls.GCP_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code,
|
||||||
|
client_id: appCfg.CLIENT_ID_GCP_SECRET_MANAGER,
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_GCP_SECRET_MANAGER,
|
||||||
|
redirect_uri: `${appCfg.SITE_URL}/integrations/gcp-secret-manager/oauth2/callback`
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: res.refresh_token,
|
||||||
|
accessExpiresAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const exchangeCodeAzure = async ({ code }: { code: string }) => {
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_ID_AZURE || !appCfg.CLIENT_SECRET_AZURE) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
const res: ExchangeCodeAzureResponse = (
|
||||||
|
await request.post(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code,
|
||||||
|
scope: "https://vault.azure.net/.default openid offline_access",
|
||||||
|
client_id: appCfg.CLIENT_ID_AZURE,
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_AZURE,
|
||||||
|
redirect_uri: `${appCfg.SITE_URL}/integrations/azure-key-vault/oauth2/callback`
|
||||||
|
} as any)
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: res.refresh_token,
|
||||||
|
accessExpiresAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const exchangeCodeHeroku = async ({ code }: { code: string }) => {
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_SECRET_HEROKU) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ExchangeCodeHerokuResponse = (
|
||||||
|
await request.post(
|
||||||
|
IntegrationUrls.HEROKU_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code,
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_HEROKU
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: res.refresh_token,
|
||||||
|
accessExpiresAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||||
|
* code-token exchange
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {Object} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj2
|
||||||
|
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||||
|
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||||
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
|
*/
|
||||||
|
const exchangeCodeVercel = async ({ code }: { code: string }) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_ID_VERCEL || !appCfg.CLIENT_SECRET_VERCEL) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ExchangeCodeVercelResponse = (
|
||||||
|
await request.post(
|
||||||
|
IntegrationUrls.VERCEL_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
code,
|
||||||
|
client_id: appCfg.CLIENT_ID_VERCEL,
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_VERCEL,
|
||||||
|
redirect_uri: `${appCfg.SITE_URL}/integrations/vercel/oauth2/callback`
|
||||||
|
} as any)
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: null,
|
||||||
|
accessExpiresAt: null,
|
||||||
|
teamId: res.team_id
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||||
|
* code-token exchange
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {Object} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj2
|
||||||
|
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||||
|
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||||
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
|
*/
|
||||||
|
const exchangeCodeNetlify = async ({ code }: { code: string }) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_ID_NETLIFY || !appCfg.CLIENT_SECRET_NETLIFY) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ExchangeCodeNetlifyResponse = (
|
||||||
|
await request.post(
|
||||||
|
IntegrationUrls.NETLIFY_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code,
|
||||||
|
client_id: appCfg.CLIENT_ID_NETLIFY,
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_NETLIFY,
|
||||||
|
redirect_uri: `${appCfg.SITE_URL}/integrations/netlify/oauth2/callback`
|
||||||
|
} as any)
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
// akhilmhdh: commented out by me. Not sure why its being called but never used in prev codebase
|
||||||
|
// const res2 = await request.get("https://api.netlify.com/api/v1/sites", {
|
||||||
|
// headers: {
|
||||||
|
// Authorization: `Bearer ${res.access_token}`
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
const res3 = (
|
||||||
|
await request.get("https://api.netlify.com/api/v1/accounts", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${res.access_token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
const accountId = res3[0].id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: res.refresh_token,
|
||||||
|
accountId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const exchangeCodeGithub = async ({ code }: { code: string }) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_ID_GITHUB || !appCfg.CLIENT_SECRET_GITHUB) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ExchangeCodeGithubResponse = (
|
||||||
|
await request.get(IntegrationUrls.GITHUB_TOKEN_URL, {
|
||||||
|
params: {
|
||||||
|
client_id: appCfg.CLIENT_ID_GITHUB,
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_GITHUB,
|
||||||
|
code,
|
||||||
|
redirect_uri: `${appCfg.SITE_URL}/integrations/github/oauth2/callback`
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: null,
|
||||||
|
accessExpiresAt: null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Gitlab
|
||||||
|
* code-token exchange
|
||||||
|
*/
|
||||||
|
const exchangeCodeGitlab = async ({ code, url }: { code: string; url?: string }) => {
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_ID_GITLAB || !appCfg.CLIENT_SECRET_GITLAB) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ExchangeCodeGitlabResponse = (
|
||||||
|
await request.post(
|
||||||
|
url ? `${url}/oauth/token` : IntegrationUrls.GITLAB_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code,
|
||||||
|
client_id: appCfg.CLIENT_ID_GITLAB,
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_GITLAB,
|
||||||
|
redirect_uri: `${appCfg.SITE_URL}/integrations/gitlab/oauth2/callback`
|
||||||
|
} as any),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: res.refresh_token,
|
||||||
|
accessExpiresAt,
|
||||||
|
url
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for BitBucket
|
||||||
|
* code-token exchange
|
||||||
|
*/
|
||||||
|
const exchangeCodeBitBucket = async ({ code }: { code: string }) => {
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ExchangeCodeBitBucketResponse = (
|
||||||
|
await request.post(
|
||||||
|
IntegrationUrls.BITBUCKET_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code,
|
||||||
|
client_id: appCfg.CLIENT_ID_BITBUCKET,
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_BITBUCKET,
|
||||||
|
redirect_uri: `${appCfg.SITE_URL}/integrations/bitbucket/oauth2/callback`
|
||||||
|
} as any),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: res.refresh_token,
|
||||||
|
accessExpiresAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type TExchangeReturn = {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken?: string | null;
|
||||||
|
accessExpiresAt?: Date | null;
|
||||||
|
url?: string;
|
||||||
|
teamId?: string;
|
||||||
|
accountId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
|
||||||
|
* code-token exchange for integration named [integration]
|
||||||
|
*/
|
||||||
|
export const exchangeCode = async ({
|
||||||
|
integration,
|
||||||
|
code,
|
||||||
|
url
|
||||||
|
}: {
|
||||||
|
integration: string;
|
||||||
|
code: string;
|
||||||
|
url?: string;
|
||||||
|
}): Promise<TExchangeReturn> => {
|
||||||
|
switch (integration) {
|
||||||
|
case Integrations.GCP_SECRET_MANAGER:
|
||||||
|
return exchangeCodeGCP({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
case Integrations.AZURE_KEY_VAULT:
|
||||||
|
return exchangeCodeAzure({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
case Integrations.HEROKU:
|
||||||
|
return exchangeCodeHeroku({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
case Integrations.VERCEL:
|
||||||
|
return exchangeCodeVercel({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
case Integrations.NETLIFY:
|
||||||
|
return exchangeCodeNetlify({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
case Integrations.GITHUB:
|
||||||
|
return exchangeCodeGithub({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
case Integrations.GITLAB:
|
||||||
|
return exchangeCodeGitlab({
|
||||||
|
code,
|
||||||
|
url
|
||||||
|
});
|
||||||
|
case Integrations.BITBUCKET:
|
||||||
|
return exchangeCodeBitBucket({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: "Unknown integration" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type RefreshTokenAzureResponse = {
|
||||||
|
token_type: string;
|
||||||
|
scope: string;
|
||||||
|
expires_in: number;
|
||||||
|
ext_expires_in: 4871;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RefreshTokenHerokuResponse = {
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
token_type: string;
|
||||||
|
user_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RefreshTokenGitLabResponse = {
|
||||||
|
token_type: string;
|
||||||
|
scope: string;
|
||||||
|
expires_in: number;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
created_at: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RefreshTokenBitBucketResponse = {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
scopes: string;
|
||||||
|
state: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ServiceAccountAccessTokenGCPSecretManagerResponse = {
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
token_type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RefreshTokenGCPSecretManagerResponse = {
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
scope: string;
|
||||||
|
token_type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||||
|
* Azure integration
|
||||||
|
*/
|
||||||
|
const exchangeRefreshAzure = async ({ refreshToken }: { refreshToken: string }) => {
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_ID_AZURE || !appCfg.CLIENT_SECRET_AZURE) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
client_id: appCfg.CLIENT_ID_AZURE,
|
||||||
|
scope: "openid offline_access",
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_AZURE
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: data.access_token,
|
||||||
|
refreshToken: data.refresh_token,
|
||||||
|
accessExpiresAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||||
|
* Heroku integration
|
||||||
|
*/
|
||||||
|
const exchangeRefreshHeroku = async ({ refreshToken }: { refreshToken: string }) => {
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_SECRET_HEROKU) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
data
|
||||||
|
}: {
|
||||||
|
data: RefreshTokenHerokuResponse;
|
||||||
|
} = await request.post(
|
||||||
|
IntegrationUrls.HEROKU_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_HEROKU
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: data.access_token,
|
||||||
|
refreshToken: data.refresh_token,
|
||||||
|
accessExpiresAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||||
|
* GitLab integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.refreshToken - refresh token to use to get new access token for GitLab
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const exchangeRefreshGitLab = async ({
|
||||||
|
refreshToken,
|
||||||
|
url
|
||||||
|
}: {
|
||||||
|
url?: string | null;
|
||||||
|
refreshToken: string;
|
||||||
|
}) => {
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_ID_GITLAB || !appCfg.CLIENT_SECRET_GITLAB) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
data
|
||||||
|
}: {
|
||||||
|
data: RefreshTokenGitLabResponse;
|
||||||
|
} = await request.post(
|
||||||
|
url ? `${url}/oauth/token` : IntegrationUrls.GITLAB_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
client_id: appCfg.CLIENT_ID_GITLAB,
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_GITLAB,
|
||||||
|
redirect_uri: `${appCfg.SITE_URL}/integrations/gitlab/oauth2/callback`
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: data.access_token,
|
||||||
|
refreshToken: data.refresh_token,
|
||||||
|
accessExpiresAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||||
|
* BitBucket integration
|
||||||
|
*/
|
||||||
|
const exchangeRefreshBitBucket = async ({ refreshToken }: { refreshToken: string }) => {
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_SECRET_BITBUCKET || !appCfg.CLIENT_ID_BITBUCKET) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
data
|
||||||
|
}: {
|
||||||
|
data: RefreshTokenBitBucketResponse;
|
||||||
|
} = await request.post(
|
||||||
|
IntegrationUrls.BITBUCKET_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
client_id: appCfg.CLIENT_ID_BITBUCKET,
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_BITBUCKET,
|
||||||
|
redirect_uri: `${appCfg.SITE_URL}/integrations/bitbucket/oauth2/callback`
|
||||||
|
} as any),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: data.access_token,
|
||||||
|
refreshToken: data.refresh_token,
|
||||||
|
accessExpiresAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||||
|
* GCP Secret Manager integration
|
||||||
|
*/
|
||||||
|
const exchangeRefreshGCPSecretManager = async ({
|
||||||
|
refreshToken,
|
||||||
|
metadata = {}
|
||||||
|
}: {
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
refreshToken: string;
|
||||||
|
}) => {
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
|
||||||
|
if (metadata?.authMethod === "serviceAccount") {
|
||||||
|
const serviceAccount = JSON.parse(refreshToken);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
iss: serviceAccount.client_email,
|
||||||
|
aud: serviceAccount.token_uri,
|
||||||
|
scope: IntegrationUrls.GCP_CLOUD_PLATFORM_SCOPE,
|
||||||
|
iat: Math.floor(Date.now() / 1000),
|
||||||
|
exp: Math.floor(Date.now() / 1000) + 3600
|
||||||
|
};
|
||||||
|
|
||||||
|
const token = jwt.sign(payload, serviceAccount.private_key, { algorithm: "RS256" });
|
||||||
|
|
||||||
|
const { data }: { data: ServiceAccountAccessTokenGCPSecretManagerResponse } =
|
||||||
|
await request.post(
|
||||||
|
IntegrationUrls.GCP_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||||
|
assertion: token
|
||||||
|
}).toString(),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: data.access_token,
|
||||||
|
refreshToken,
|
||||||
|
accessExpiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.CLIENT_SECRET_GCP_SECRET_MANAGER || !appCfg.CLIENT_ID_GCP_SECRET_MANAGER) {
|
||||||
|
throw new BadRequestError({ message: "Missing client id and client secret" });
|
||||||
|
}
|
||||||
|
const { data }: { data: RefreshTokenGCPSecretManagerResponse } = await request.post(
|
||||||
|
IntegrationUrls.GCP_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
client_id: appCfg.CLIENT_ID_GCP_SECRET_MANAGER,
|
||||||
|
client_secret: appCfg.CLIENT_SECRET_GCP_SECRET_MANAGER,
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
grant_type: "refresh_token"
|
||||||
|
} as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: data.access_token,
|
||||||
|
refreshToken,
|
||||||
|
accessExpiresAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for integration
|
||||||
|
*/
|
||||||
|
export const exchangeRefresh = async (
|
||||||
|
integration: string,
|
||||||
|
refreshToken: string,
|
||||||
|
url?: string | null,
|
||||||
|
metadata?: Record<string, string>
|
||||||
|
): Promise<{
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
accessExpiresAt: Date;
|
||||||
|
}> => {
|
||||||
|
switch (integration) {
|
||||||
|
case Integrations.AZURE_KEY_VAULT:
|
||||||
|
return exchangeRefreshAzure({
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
case Integrations.HEROKU:
|
||||||
|
return exchangeRefreshHeroku({
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
case Integrations.GITLAB:
|
||||||
|
return exchangeRefreshGitLab({
|
||||||
|
refreshToken,
|
||||||
|
url
|
||||||
|
});
|
||||||
|
case Integrations.BITBUCKET:
|
||||||
|
return exchangeRefreshBitBucket({
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
case Integrations.GCP_SECRET_MANAGER:
|
||||||
|
return exchangeRefreshGCPSecretManager({
|
||||||
|
refreshToken,
|
||||||
|
metadata
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw new Error("Failed to exchange token for incompatible integration");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName,TIntegrations } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TIntegrationDalFactory = ReturnType<typeof integrationDalFactory>;
|
||||||
|
|
||||||
|
export const integrationDalFactory = (db: TDbClient) => {
|
||||||
|
const integrationOrm = ormify(db, TableName.Integration);
|
||||||
|
|
||||||
|
const integrationFindQuery = (tx: Knex, filter: Partial<TIntegrations>) =>
|
||||||
|
tx(TableName.Integration)
|
||||||
|
.where(filter)
|
||||||
|
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||||
|
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||||
|
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||||
|
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
||||||
|
.select(selectAllTableCols(TableName.Integration));
|
||||||
|
|
||||||
|
const find = async (filter: Partial<TIntegrations>, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const docs = await integrationFindQuery(tx || db, filter);
|
||||||
|
return docs.map(({ envId, envSlug, envName, ...el }) => ({
|
||||||
|
...el,
|
||||||
|
environment: {
|
||||||
|
id: envId,
|
||||||
|
slug: envSlug,
|
||||||
|
name: envName
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find by id integrations" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findOne = async (filter: Partial<TIntegrations>, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const doc = await integrationFindQuery(tx || db, filter).first();
|
||||||
|
if (!doc) return;
|
||||||
|
|
||||||
|
const { envName: name, envSlug: slug, envId: id, ...el } = doc;
|
||||||
|
return { ...el, environment: { id, name, slug } };
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find one integrations" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const doc = await integrationFindQuery(tx || db, { id }).first();
|
||||||
|
if (!doc) return;
|
||||||
|
|
||||||
|
const { envName: name, envSlug: slug, envId, ...el } = doc;
|
||||||
|
return { ...el, environment: { id: envId, name, slug } };
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find by id integrations" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...integrationOrm, find, findOne, findById };
|
||||||
|
};
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TIntegrationAuthDalFactory } from "../integration-auth/integration-auth-dal";
|
||||||
|
import { TSecretFolderDalFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TIntegrationDalFactory } from "./integration-dal";
|
||||||
|
import {
|
||||||
|
TCreateIntegrationDTO,
|
||||||
|
TDeleteIntegrationDTO,
|
||||||
|
TUpdateIntegrationDTO
|
||||||
|
} from "./integration-types";
|
||||||
|
|
||||||
|
type TIntegrationServiceFactoryDep = {
|
||||||
|
integrationDal: TIntegrationDalFactory;
|
||||||
|
integrationAuthDal: TIntegrationAuthDalFactory;
|
||||||
|
folderDal: Pick<TSecretFolderDalFactory, "findBySecretPath">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIntegrationServiceFactory = ReturnType<typeof integrationServiceFactory>;
|
||||||
|
|
||||||
|
export const integrationServiceFactory = ({
|
||||||
|
integrationDal,
|
||||||
|
integrationAuthDal,
|
||||||
|
folderDal,
|
||||||
|
permissionService
|
||||||
|
}: TIntegrationServiceFactoryDep) => {
|
||||||
|
const createIntegration = async ({
|
||||||
|
app,
|
||||||
|
actor,
|
||||||
|
path,
|
||||||
|
appId,
|
||||||
|
owner,
|
||||||
|
scope,
|
||||||
|
actorId,
|
||||||
|
region,
|
||||||
|
isActive,
|
||||||
|
metadata,
|
||||||
|
secretPath,
|
||||||
|
targetService,
|
||||||
|
targetServiceId,
|
||||||
|
integrationAuthId,
|
||||||
|
sourceEnvironment,
|
||||||
|
targetEnvironment,
|
||||||
|
targetEnvironmentId
|
||||||
|
}: TCreateIntegrationDTO) => {
|
||||||
|
const integrationAuth = await integrationAuthDal.findById(integrationAuthId);
|
||||||
|
if (!integrationAuth) throw new BadRequestError({ message: "Integration auth not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
integrationAuth.projectId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.Integrations
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDal.findBySecretPath(
|
||||||
|
integrationAuth.projectId,
|
||||||
|
sourceEnvironment,
|
||||||
|
secretPath
|
||||||
|
);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder path not found" });
|
||||||
|
|
||||||
|
const integration = await integrationDal.create({
|
||||||
|
envId: folder.envId,
|
||||||
|
secretPath,
|
||||||
|
isActive,
|
||||||
|
integrationAuthId,
|
||||||
|
targetEnvironmentId,
|
||||||
|
targetEnvironment,
|
||||||
|
targetServiceId,
|
||||||
|
targetService,
|
||||||
|
metadata,
|
||||||
|
region,
|
||||||
|
scope,
|
||||||
|
owner,
|
||||||
|
appId,
|
||||||
|
path,
|
||||||
|
app,
|
||||||
|
integration: integrationAuth.integration
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(akhilmhdh-pg): audit log
|
||||||
|
return integration;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateIntegration = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
targetEnvironment,
|
||||||
|
app,
|
||||||
|
id,
|
||||||
|
appId,
|
||||||
|
owner,
|
||||||
|
isActive,
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
|
}: TUpdateIntegrationDTO) => {
|
||||||
|
const integration = await integrationDal.findById(id);
|
||||||
|
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
integration.projectId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.Integrations
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDal.findBySecretPath(integration.projectId, environment, secretPath);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder path not found" });
|
||||||
|
|
||||||
|
const updatedIntegration = await integrationDal.updateById(id, {
|
||||||
|
envId: folder.envId,
|
||||||
|
isActive,
|
||||||
|
app,
|
||||||
|
appId,
|
||||||
|
targetEnvironment,
|
||||||
|
owner,
|
||||||
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedIntegration;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteIntegration = async ({ actorId, id, actor }: TDeleteIntegrationDTO) => {
|
||||||
|
const integration = await integrationDal.findById(id);
|
||||||
|
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
integration.projectId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
ProjectPermissionSub.Integrations
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletedIntegration = await integrationDal.deleteById(id);
|
||||||
|
return deletedIntegration;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createIntegration,
|
||||||
|
updateIntegration,
|
||||||
|
deleteIntegration
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TCreateIntegrationDTO = {
|
||||||
|
secretPath: string;
|
||||||
|
integrationAuthId: string;
|
||||||
|
app?: string;
|
||||||
|
isActive: boolean;
|
||||||
|
appId?: string;
|
||||||
|
sourceEnvironment: string;
|
||||||
|
targetEnvironment?: string;
|
||||||
|
targetEnvironmentId?: string;
|
||||||
|
targetService?: string;
|
||||||
|
targetServiceId?: string;
|
||||||
|
owner?: string;
|
||||||
|
path?: string;
|
||||||
|
region?: string;
|
||||||
|
scope?: string;
|
||||||
|
metadata?: {
|
||||||
|
secretPrefix?: string;
|
||||||
|
secretSuffix?: string;
|
||||||
|
secretGCPLabel?: {
|
||||||
|
labelName: string;
|
||||||
|
labelValue: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateIntegrationDTO = {
|
||||||
|
id: string;
|
||||||
|
app: string;
|
||||||
|
appId: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
secretPath: string;
|
||||||
|
targetEnvironment: string;
|
||||||
|
owner: string;
|
||||||
|
environment: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteIntegrationDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName,TProjectBots } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TProjectBotDalFactory = ReturnType<typeof projectBotDalFactory>;
|
||||||
|
|
||||||
|
export const projectBotDalFactory = (db: TDbClient) => {
|
||||||
|
const projectBotOrm = ormify(db, TableName.ProjectBot);
|
||||||
|
|
||||||
|
const findOne = async (filter: Partial<TProjectBots>, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const bot = await (tx || db)(TableName.ProjectBot)
|
||||||
|
.where(filter)
|
||||||
|
.join(TableName.Users, `${TableName.ProjectBot}.senderId`, `${TableName.Users}.id`)
|
||||||
|
.join(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
`${TableName.UserEncryptionKey}.userId`,
|
||||||
|
`${TableName.Users}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.ProjectBot))
|
||||||
|
.select(db.ref("publicKey").withSchema(TableName.UserEncryptionKey).as("senderPubKey"))
|
||||||
|
.first();
|
||||||
|
if (!bot) return bot;
|
||||||
|
const { senderPubKey, ...el } = bot;
|
||||||
|
return { ...el, sender: { publicKey: senderPubKey } };
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find on project bot" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...projectBotOrm, findOne };
|
||||||
|
};
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import {
|
||||||
|
decryptAsymmetric,
|
||||||
|
decryptSymmetric,
|
||||||
|
decryptSymmetric128BitHexKeyUTF8,
|
||||||
|
encryptSymmetric,
|
||||||
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
|
generateAsymmetricKeyPair
|
||||||
|
} from "@app/lib/crypto";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { TProjectBotDalFactory } from "./project-bot-dal";
|
||||||
|
import { TSetActiveStateDTO } from "./project-bot-types";
|
||||||
|
|
||||||
|
type TProjectBotServiceFactoryDep = {
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
projectBotDal: TProjectBotDalFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TProjectBotServiceFactory = ReturnType<typeof projectBotServiceFactory>;
|
||||||
|
|
||||||
|
export const projectBotServiceFactory = ({
|
||||||
|
projectBotDal,
|
||||||
|
permissionService
|
||||||
|
}: TProjectBotServiceFactoryDep) => {
|
||||||
|
const getBotKey = async (projectId: string) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const encryptionKey = appCfg.ENCRYPTION_KEY;
|
||||||
|
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
|
||||||
|
|
||||||
|
const bot = await projectBotDal.findOne({ projectId });
|
||||||
|
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
|
||||||
|
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
|
||||||
|
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
|
||||||
|
throw new BadRequestError({ message: "Encryption key missing" });
|
||||||
|
|
||||||
|
if (rootEncryptionKey && bot.keyEncoding === SecretKeyEncoding.BASE64) {
|
||||||
|
const privateKeyBot = decryptSymmetric({
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
ciphertext: bot.encryptedPrivateKey,
|
||||||
|
key: rootEncryptionKey
|
||||||
|
});
|
||||||
|
return decryptAsymmetric({
|
||||||
|
ciphertext: bot.encryptedProjectKey,
|
||||||
|
privateKey: privateKeyBot,
|
||||||
|
nonce: bot.encryptedProjectKeyNonce,
|
||||||
|
publicKey: bot.sender.publicKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (encryptionKey && bot.keyEncoding === SecretKeyEncoding.UTF8) {
|
||||||
|
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
ciphertext: bot.encryptedPrivateKey,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
return decryptAsymmetric({
|
||||||
|
ciphertext: bot.encryptedProjectKey,
|
||||||
|
privateKey: privateKeyBot,
|
||||||
|
nonce: bot.encryptedProjectKeyNonce,
|
||||||
|
publicKey: bot.sender.publicKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to obtain bot copy of workspace key needed for operation"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const findBotByProjectId = async ({ actorId, actor, projectId }: TProjectPermission) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.Integrations
|
||||||
|
);
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const bot = await projectBotDal.transaction(async (tx) => {
|
||||||
|
const doc = await projectBotDal.findOne({ projectId }, tx);
|
||||||
|
if (doc) return doc;
|
||||||
|
|
||||||
|
const { publicKey, privateKey } = generateAsymmetricKeyPair();
|
||||||
|
if (appCfg.ROOT_ENCRYPTION_KEY) {
|
||||||
|
const { iv, tag, ciphertext } = encryptSymmetric(privateKey, appCfg.ROOT_ENCRYPTION_KEY);
|
||||||
|
return projectBotDal.create(
|
||||||
|
{
|
||||||
|
name: "Infisical Bot",
|
||||||
|
projectId,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||||
|
keyEncoding: SecretKeyEncoding.BASE64
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (appCfg.ENCRYPTION_KEY) {
|
||||||
|
const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(
|
||||||
|
privateKey,
|
||||||
|
appCfg.ENCRYPTION_KEY
|
||||||
|
);
|
||||||
|
return projectBotDal.create(
|
||||||
|
{
|
||||||
|
name: "Infisical Bot",
|
||||||
|
projectId,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||||
|
keyEncoding: SecretKeyEncoding.UTF8
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new BadRequestError({ message: "Failed to create bot due to missing encryption key" });
|
||||||
|
});
|
||||||
|
return bot;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setBotActiveState = async ({
|
||||||
|
actor,
|
||||||
|
botId,
|
||||||
|
botKey,
|
||||||
|
actorId,
|
||||||
|
isActive
|
||||||
|
}: TSetActiveStateDTO) => {
|
||||||
|
const bot = await projectBotDal.findOne({ id: botId });
|
||||||
|
if (!bot) throw new BadRequestError({ message: "Bot not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
bot.projectId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.Integrations
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
if (!botKey?.nonce || !botKey?.encryptionKey) {
|
||||||
|
throw new BadRequestError({ message: "Failed to set bot active - missing bot key" });
|
||||||
|
}
|
||||||
|
const doc = await projectBotDal.updateById(botId, {
|
||||||
|
isActive: true,
|
||||||
|
encryptedProjectKey: botKey.encryptionKey,
|
||||||
|
encryptedProjectKeyNonce: botKey.nonce,
|
||||||
|
senderId: actorId
|
||||||
|
});
|
||||||
|
if (!doc) throw new BadRequestError({ message: "Failed to update bot active state" });
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = await projectBotDal.updateById(botId, {
|
||||||
|
isActive: false,
|
||||||
|
encryptedProjectKey: null,
|
||||||
|
encryptedProjectKeyNonce: null
|
||||||
|
});
|
||||||
|
if (!doc) throw new BadRequestError({ message: "Failed to update bot active state" });
|
||||||
|
return doc;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
findBotByProjectId,
|
||||||
|
setBotActiveState,
|
||||||
|
getBotKey
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TSetActiveStateDTO = {
|
||||||
|
isActive: boolean;
|
||||||
|
botKey?: {
|
||||||
|
nonce?: string;
|
||||||
|
encryptionKey?: string;
|
||||||
|
};
|
||||||
|
botId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
@@ -37,7 +37,7 @@ const sqlFindFolderByPathQuery = (
|
|||||||
depth: 1,
|
depth: 1,
|
||||||
path: db.raw("'/'")
|
path: db.raw("'/'")
|
||||||
})
|
})
|
||||||
.select(selectAllTableCols(db, TableName.SecretFolder))
|
.select(selectAllTableCols(TableName.SecretFolder))
|
||||||
.from(TableName.SecretFolder)
|
.from(TableName.SecretFolder)
|
||||||
.join(
|
.join(
|
||||||
TableName.Environment,
|
TableName.Environment,
|
||||||
@@ -60,7 +60,7 @@ const sqlFindFolderByPathQuery = (
|
|||||||
"CONCAT((CASE WHEN parent.path = '/' THEN '' ELSE parent.path END),'/', secret_folders.name)"
|
"CONCAT((CASE WHEN parent.path = '/' THEN '' ELSE parent.path END),'/', secret_folders.name)"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.select(selectAllTableCols(db, TableName.SecretFolder))
|
.select(selectAllTableCols(TableName.SecretFolder))
|
||||||
.whereRaw(
|
.whereRaw(
|
||||||
`depth = array_position(ARRAY[${pathSegments
|
`depth = array_position(ARRAY[${pathSegments
|
||||||
.map(() => "?")
|
.map(() => "?")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TProjectEnvDalFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDalFactory } from "../project-env/project-env-dal";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDalFactory } from "./secret-folder-dal";
|
import { TSecretFolderDalFactory } from "./secret-folder-dal";
|
||||||
import {
|
import {
|
||||||
TCreateFolderDTO,
|
TCreateFolderDTO,
|
||||||
TDeleteFolderDTO,
|
TDeleteFolderDTO,
|
||||||
|
|||||||
Reference in New Issue
Block a user