mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 03:26:27 +00:00
feat: finished org-level integration management flow
This commit is contained in:
Vendored
+8
@@ -196,6 +196,9 @@ import {
|
|||||||
TProjectRolesUpdate,
|
TProjectRolesUpdate,
|
||||||
TProjects,
|
TProjects,
|
||||||
TProjectsInsert,
|
TProjectsInsert,
|
||||||
|
TProjectSlackConfigs,
|
||||||
|
TProjectSlackConfigsInsert,
|
||||||
|
TProjectSlackConfigsUpdate,
|
||||||
TProjectsUpdate,
|
TProjectsUpdate,
|
||||||
TProjectUserAdditionalPrivilege,
|
TProjectUserAdditionalPrivilege,
|
||||||
TProjectUserAdditionalPrivilegeInsert,
|
TProjectUserAdditionalPrivilegeInsert,
|
||||||
@@ -787,6 +790,11 @@ declare module "knex/types/tables" {
|
|||||||
TSlackIntegrationsInsert,
|
TSlackIntegrationsInsert,
|
||||||
TSlackIntegrationsUpdate
|
TSlackIntegrationsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.ProjectSlackConfigs]: KnexOriginal.CompositeTableType<
|
||||||
|
TProjectSlackConfigs,
|
||||||
|
TProjectSlackConfigsInsert,
|
||||||
|
TProjectSlackConfigsUpdate
|
||||||
|
>;
|
||||||
[TableName.AdminSlackConfig]: KnexOriginal.CompositeTableType<
|
[TableName.AdminSlackConfig]: KnexOriginal.CompositeTableType<
|
||||||
TAdminSlackConfigs,
|
TAdminSlackConfigs,
|
||||||
TAdminSlackConfigsInsert,
|
TAdminSlackConfigsInsert,
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
if (!(await knex.schema.hasTable(TableName.SlackIntegrations))) {
|
if (!(await knex.schema.hasTable(TableName.SlackIntegrations))) {
|
||||||
await knex.schema.createTable(TableName.SlackIntegrations, (tb) => {
|
await knex.schema.createTable(TableName.SlackIntegrations, (tb) => {
|
||||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
tb.string("projectId").notNullable().unique();
|
tb.string("slug").notNullable();
|
||||||
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
tb.uuid("orgId").notNullable();
|
||||||
|
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
tb.string("description");
|
||||||
tb.string("teamId").notNullable();
|
tb.string("teamId").notNullable();
|
||||||
tb.string("teamName").notNullable();
|
tb.string("teamName").notNullable();
|
||||||
tb.string("slackUserId").notNullable();
|
tb.string("slackUserId").notNullable();
|
||||||
@@ -16,6 +18,20 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
tb.binary("encryptedBotAccessToken").notNullable();
|
tb.binary("encryptedBotAccessToken").notNullable();
|
||||||
tb.string("slackBotId").notNullable();
|
tb.string("slackBotId").notNullable();
|
||||||
tb.string("slackBotUserId").notNullable();
|
tb.string("slackBotUserId").notNullable();
|
||||||
|
tb.unique(["orgId", "slug"]);
|
||||||
|
tb.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SlackIntegrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.ProjectSlackConfigs))) {
|
||||||
|
await knex.schema.createTable(TableName.ProjectSlackConfigs, (tb) => {
|
||||||
|
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
tb.string("projectId").notNullable().unique();
|
||||||
|
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
tb.uuid("slackIntegrationId").notNullable();
|
||||||
|
tb.foreign("slackIntegrationId").references("id").inTable(TableName.SlackIntegrations).onDelete("CASCADE");
|
||||||
tb.boolean("isAccessRequestNotificationEnabled").notNullable().defaultTo(false);
|
tb.boolean("isAccessRequestNotificationEnabled").notNullable().defaultTo(false);
|
||||||
tb.string("accessRequestChannels").notNullable().defaultTo("");
|
tb.string("accessRequestChannels").notNullable().defaultTo("");
|
||||||
tb.boolean("isSecretRequestNotificationEnabled").notNullable().defaultTo(false);
|
tb.boolean("isSecretRequestNotificationEnabled").notNullable().defaultTo(false);
|
||||||
@@ -23,7 +39,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
tb.timestamps(true, true, true);
|
tb.timestamps(true, true, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
await createOnUpdateTrigger(knex, TableName.SlackIntegrations);
|
await createOnUpdateTrigger(knex, TableName.ProjectSlackConfigs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await knex.schema.hasTable(TableName.AdminSlackConfig))) {
|
if (!(await knex.schema.hasTable(TableName.AdminSlackConfig))) {
|
||||||
@@ -39,9 +55,12 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
await knex.schema.dropTableIfExists(TableName.SlackIntegrations);
|
await knex.schema.dropTableIfExists(TableName.ProjectSlackConfigs);
|
||||||
await dropOnUpdateTrigger(knex, TableName.SlackIntegrations);
|
await dropOnUpdateTrigger(knex, TableName.ProjectSlackConfigs);
|
||||||
|
|
||||||
await knex.schema.dropTableIfExists(TableName.AdminSlackConfig);
|
await knex.schema.dropTableIfExists(TableName.AdminSlackConfig);
|
||||||
await dropOnUpdateTrigger(knex, TableName.AdminSlackConfig);
|
await dropOnUpdateTrigger(knex, TableName.AdminSlackConfig);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SlackIntegrations);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SlackIntegrations);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export * from "./project-environments";
|
|||||||
export * from "./project-keys";
|
export * from "./project-keys";
|
||||||
export * from "./project-memberships";
|
export * from "./project-memberships";
|
||||||
export * from "./project-roles";
|
export * from "./project-roles";
|
||||||
|
export * from "./project-slack-configs";
|
||||||
export * from "./project-user-additional-privilege";
|
export * from "./project-user-additional-privilege";
|
||||||
export * from "./project-user-membership-roles";
|
export * from "./project-user-membership-roles";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export enum TableName {
|
|||||||
// @depreciated
|
// @depreciated
|
||||||
KmsKeyVersion = "kms_key_versions",
|
KmsKeyVersion = "kms_key_versions",
|
||||||
SlackIntegrations = "slack_integrations",
|
SlackIntegrations = "slack_integrations",
|
||||||
|
ProjectSlackConfigs = "project_slack_configs",
|
||||||
AdminSlackConfig = "admin_slack_configs"
|
AdminSlackConfig = "admin_slack_configs"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 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 ProjectSlackConfigsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
projectId: z.string(),
|
||||||
|
slackIntegrationId: z.string().uuid(),
|
||||||
|
isAccessRequestNotificationEnabled: z.boolean().default(false),
|
||||||
|
accessRequestChannels: z.string().default(""),
|
||||||
|
isSecretRequestNotificationEnabled: z.boolean().default(false),
|
||||||
|
secretRequestChannels: z.string().default(""),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TProjectSlackConfigs = z.infer<typeof ProjectSlackConfigsSchema>;
|
||||||
|
export type TProjectSlackConfigsInsert = Omit<z.input<typeof ProjectSlackConfigsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TProjectSlackConfigsUpdate = Partial<Omit<z.input<typeof ProjectSlackConfigsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -11,7 +11,9 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
|
|
||||||
export const SlackIntegrationsSchema = z.object({
|
export const SlackIntegrationsSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
projectId: z.string(),
|
slug: z.string(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
teamId: z.string(),
|
teamId: z.string(),
|
||||||
teamName: z.string(),
|
teamName: z.string(),
|
||||||
slackUserId: z.string(),
|
slackUserId: z.string(),
|
||||||
@@ -19,10 +21,6 @@ export const SlackIntegrationsSchema = z.object({
|
|||||||
encryptedBotAccessToken: zodBuffer,
|
encryptedBotAccessToken: zodBuffer,
|
||||||
slackBotId: z.string(),
|
slackBotId: z.string(),
|
||||||
slackBotUserId: z.string(),
|
slackBotUserId: z.string(),
|
||||||
isAccessRequestNotificationEnabled: z.boolean().default(false),
|
|
||||||
accessRequestChannels: z.string().default(""),
|
|
||||||
isSecretRequestNotificationEnabled: z.boolean().default(false),
|
|
||||||
secretRequestChannels: z.string().default(""),
|
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1453,18 +1453,17 @@ interface GetCertificateTemplateEstConfig {
|
|||||||
interface AttemptCreateSlackIntegration {
|
interface AttemptCreateSlackIntegration {
|
||||||
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
|
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
|
||||||
metadata: {
|
metadata: {
|
||||||
projectId?: string;
|
slug: string;
|
||||||
}; // no metadata
|
description?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateSlackIntegration {
|
interface UpdateSlackIntegration {
|
||||||
type: EventType.UPDATE_SLACK_INTEGRATION;
|
type: EventType.UPDATE_SLACK_INTEGRATION;
|
||||||
metadata: {
|
metadata: {
|
||||||
id: string;
|
id: string;
|
||||||
isAccessRequestNotificationEnabled: boolean;
|
slug: string;
|
||||||
accessRequestChannels: string;
|
description?: string;
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
|
||||||
secretRequestChannels: string;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,13 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||||
await server.register(registerSecretImportRouter, { prefix: "/secret-imports" });
|
await server.register(registerSecretImportRouter, { prefix: "/secret-imports" });
|
||||||
await server.register(registerSecretFolderRouter, { prefix: "/folders" });
|
await server.register(registerSecretFolderRouter, { prefix: "/folders" });
|
||||||
await server.register(registerSlackRouter, { prefix: "/slack" });
|
|
||||||
|
await server.register(
|
||||||
|
async (workflowIntegrationRouter) => {
|
||||||
|
await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" });
|
||||||
|
},
|
||||||
|
{ prefix: "/workflow-integrations" }
|
||||||
|
);
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (projectRouter) => {
|
async (projectRouter) => {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
|
|
||||||
|
|
||||||
export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -24,7 +23,8 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectId: z.string()
|
slug: z.string(),
|
||||||
|
description: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.string()
|
200: z.string()
|
||||||
@@ -37,15 +37,18 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.query.projectId
|
...req.query
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.query.projectId,
|
orgId: req.permission.orgId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION,
|
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION,
|
||||||
metadata: {}
|
metadata: {
|
||||||
|
slug: req.query.slug,
|
||||||
|
description: req.query.description
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,104 +68,25 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
|||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
querystring: z.object({
|
|
||||||
projectId: z.string()
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: SlackIntegrationsSchema.pick({
|
200: SlackIntegrationsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
teamName: true,
|
slug: true,
|
||||||
isAccessRequestNotificationEnabled: true,
|
description: true,
|
||||||
accessRequestChannels: true,
|
teamName: true
|
||||||
isSecretRequestNotificationEnabled: true,
|
}).array()
|
||||||
secretRequestChannels: true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const slackIntegration = await server.services.slack.getSlackIntegrationByProjectId({
|
const slackIntegrations = await server.services.slack.getSlackIntegrationsByOrg({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId
|
||||||
projectId: req.query.projectId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
return slackIntegrations;
|
||||||
...req.auditLogInfo,
|
|
||||||
projectId: req.query.projectId,
|
|
||||||
event: {
|
|
||||||
type: EventType.GET_SLACK_INTEGRATION,
|
|
||||||
metadata: {
|
|
||||||
id: slackIntegration?.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return slackIntegration;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "PATCH",
|
|
||||||
url: "/:slackIntegrationId",
|
|
||||||
config: {
|
|
||||||
rateLimit: writeLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
security: [
|
|
||||||
{
|
|
||||||
bearerAuth: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
params: z.object({
|
|
||||||
slackIntegrationId: z.string()
|
|
||||||
}),
|
|
||||||
body: z.object({
|
|
||||||
isAccessRequestNotificationEnabled: z.boolean().optional(),
|
|
||||||
accessRequestChannels: validateSlackChannelsField.optional(),
|
|
||||||
isSecretRequestNotificationEnabled: z.boolean().optional(),
|
|
||||||
secretRequestChannels: validateSlackChannelsField.optional()
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: SlackIntegrationsSchema.pick({
|
|
||||||
id: true,
|
|
||||||
teamName: true,
|
|
||||||
isAccessRequestNotificationEnabled: true,
|
|
||||||
accessRequestChannels: true,
|
|
||||||
isSecretRequestNotificationEnabled: true,
|
|
||||||
secretRequestChannels: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
||||||
handler: async (req) => {
|
|
||||||
const updatedSlackIntegration = await server.services.slack.updateSlackIntegration({
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
id: req.params.slackIntegrationId,
|
|
||||||
...req.body
|
|
||||||
});
|
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
|
||||||
...req.auditLogInfo,
|
|
||||||
projectId: updatedSlackIntegration.projectId,
|
|
||||||
event: {
|
|
||||||
type: EventType.UPDATE_SLACK_INTEGRATION,
|
|
||||||
metadata: {
|
|
||||||
id: updatedSlackIntegration.id,
|
|
||||||
isAccessRequestNotificationEnabled: updatedSlackIntegration.isAccessRequestNotificationEnabled,
|
|
||||||
accessRequestChannels: updatedSlackIntegration.accessRequestChannels,
|
|
||||||
isSecretRequestNotificationEnabled: updatedSlackIntegration.isSecretRequestNotificationEnabled,
|
|
||||||
secretRequestChannels: updatedSlackIntegration.secretRequestChannels
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedSlackIntegration;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -184,11 +108,9 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: SlackIntegrationsSchema.pick({
|
200: SlackIntegrationsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
teamName: true,
|
slug: true,
|
||||||
isAccessRequestNotificationEnabled: true,
|
description: true,
|
||||||
accessRequestChannels: true,
|
teamName: true
|
||||||
isSecretRequestNotificationEnabled: true,
|
|
||||||
secretRequestChannels: true
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -204,7 +126,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: deletedSlackIntegration.projectId,
|
orgId: deletedSlackIntegration.orgId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.DELETE_SLACK_INTEGRATION,
|
type: EventType.DELETE_SLACK_INTEGRATION,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -217,6 +139,111 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:slackIntegrationId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
slackIntegrationId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: SlackIntegrationsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
description: true,
|
||||||
|
teamName: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const slackIntegration = await server.services.slack.getSlackIntegrationById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.slackIntegrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: slackIntegration.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SLACK_INTEGRATION,
|
||||||
|
metadata: {
|
||||||
|
id: slackIntegration.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return slackIntegration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:slackIntegrationId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
slackIntegrationId: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
slug: z.string().optional(),
|
||||||
|
description: z.string().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: SlackIntegrationsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
description: true,
|
||||||
|
teamName: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const slackIntegration = await server.services.slack.updateSlackIntegration({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.slackIntegrationId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: slackIntegration.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_SLACK_INTEGRATION,
|
||||||
|
metadata: {
|
||||||
|
id: slackIntegration.id,
|
||||||
|
slug: slackIntegration.slug,
|
||||||
|
description: slackIntegration.description as string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return slackIntegration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/oauth_redirect",
|
url: "/oauth_redirect",
|
||||||
@@ -232,10 +259,10 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
successAsync: async (installation) => {
|
successAsync: async (installation) => {
|
||||||
const metadata = JSON.parse(installation.metadata || "") as {
|
const metadata = JSON.parse(installation.metadata || "") as {
|
||||||
projectId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
return res.redirect(`${appCfg.SITE_URL}/project/${metadata.projectId}/settings`);
|
return res.redirect(`${appCfg.SITE_URL}/org/${metadata.orgId}/settings`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,40 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import { InstallProvider } from "@slack/oauth";
|
import { InstallProvider } from "@slack/oauth";
|
||||||
|
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
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 { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
|
||||||
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
|
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
|
||||||
import {
|
import {
|
||||||
TCompleteSlackIntegrationDTO,
|
TCompleteSlackIntegrationDTO,
|
||||||
TDeleteSlackIntegrationDTO,
|
TDeleteSlackIntegrationDTO,
|
||||||
TGetSlackInstallUrlDTO,
|
TGetSlackInstallUrlDTO,
|
||||||
TGetSlackIntegrationByProjectIdDTO,
|
TGetSlackIntegrationByIdDTO,
|
||||||
|
TGetSlackIntegrationByOrgDTO,
|
||||||
TUpdateSlackIntegrationDTO
|
TUpdateSlackIntegrationDTO
|
||||||
} from "./slack-types";
|
} from "./slack-types";
|
||||||
|
|
||||||
type TSlackServiceFactoryDep = {
|
type TSlackServiceFactoryDep = {
|
||||||
slackIntegrationDAL: Pick<
|
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "find" | "findById" | "deleteById" | "updateById" | "create">;
|
||||||
TSlackIntegrationDALFactory,
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
"create" | "findOne" | "findById" | "updateById" | "deleteById" | "transaction"
|
|
||||||
>;
|
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSlackServiceFactory = ReturnType<typeof slackServiceFactory>;
|
export type TSlackServiceFactory = ReturnType<typeof slackServiceFactory>;
|
||||||
|
|
||||||
export const slackServiceFactory = ({
|
export const slackServiceFactory = ({
|
||||||
projectDAL,
|
|
||||||
permissionService,
|
permissionService,
|
||||||
slackIntegrationDAL,
|
slackIntegrationDAL,
|
||||||
kmsService
|
kmsService
|
||||||
}: TSlackServiceFactoryDep) => {
|
}: TSlackServiceFactoryDep) => {
|
||||||
const completeSlackIntegration = async ({
|
const completeSlackIntegration = async ({
|
||||||
projectId,
|
orgId,
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
teamId,
|
teamId,
|
||||||
teamName,
|
teamName,
|
||||||
slackUserId,
|
slackUserId,
|
||||||
@@ -46,59 +43,26 @@ export const slackServiceFactory = ({
|
|||||||
slackBotId,
|
slackBotId,
|
||||||
slackBotUserId
|
slackBotUserId
|
||||||
}: TCompleteSlackIntegrationDTO) => {
|
}: TCompleteSlackIntegrationDTO) => {
|
||||||
const project = await projectDAL.findById(projectId);
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundError({
|
|
||||||
message: "Project not found"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
orgId,
|
||||||
orgId: project.orgId
|
type: KmsDataKey.Organization
|
||||||
});
|
});
|
||||||
|
|
||||||
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
|
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
|
||||||
plainText: Buffer.from(botAccessToken, "utf8")
|
plainText: Buffer.from(botAccessToken, "utf8")
|
||||||
});
|
});
|
||||||
|
|
||||||
await slackIntegrationDAL.transaction(async (tx) => {
|
await slackIntegrationDAL.create({
|
||||||
const slackIntegration = await slackIntegrationDAL.findOne(
|
orgId,
|
||||||
{
|
slug,
|
||||||
projectId
|
description,
|
||||||
},
|
teamId,
|
||||||
tx
|
teamName,
|
||||||
);
|
slackUserId,
|
||||||
|
slackAppId,
|
||||||
if (slackIntegration) {
|
slackBotId,
|
||||||
return slackIntegrationDAL.updateById(
|
slackBotUserId,
|
||||||
slackIntegration.id,
|
encryptedBotAccessToken
|
||||||
{
|
|
||||||
teamId,
|
|
||||||
teamName,
|
|
||||||
slackUserId,
|
|
||||||
slackAppId,
|
|
||||||
slackBotId,
|
|
||||||
slackBotUserId,
|
|
||||||
encryptedBotAccessToken
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return slackIntegrationDAL.create(
|
|
||||||
{
|
|
||||||
projectId,
|
|
||||||
teamId,
|
|
||||||
teamName,
|
|
||||||
slackUserId,
|
|
||||||
slackAppId,
|
|
||||||
slackBotId,
|
|
||||||
slackBotUserId,
|
|
||||||
encryptedBotAccessToken
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,11 +89,15 @@ export const slackServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const metadata = JSON.parse(installation.metadata || "") as {
|
const metadata = JSON.parse(installation.metadata || "") as {
|
||||||
projectId: string;
|
orgId: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
return completeSlackIntegration({
|
return completeSlackIntegration({
|
||||||
projectId: metadata.projectId,
|
orgId: metadata.orgId,
|
||||||
|
slug: metadata.slug,
|
||||||
|
description: metadata.description,
|
||||||
teamId: installation.team?.id || "",
|
teamId: installation.team?.id || "",
|
||||||
teamName: installation.team?.name || "",
|
teamName: installation.team?.name || "",
|
||||||
slackUserId: installation.user.id,
|
slackUserId: installation.user.id,
|
||||||
@@ -152,56 +120,87 @@ export const slackServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInstallUrl = async ({ actorId, actor, actorOrgId, actorAuthMethod, projectId }: TGetSlackInstallUrlDTO) => {
|
const getInstallUrl = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
slug,
|
||||||
|
description
|
||||||
|
}: TGetSlackInstallUrlDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||||
const project = await projectDAL.findById(projectId);
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundError({
|
|
||||||
message: "Project not found"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const installer = await getSlackInstaller();
|
const installer = await getSlackInstaller();
|
||||||
const url = await installer.generateInstallUrl({
|
const url = await installer.generateInstallUrl({
|
||||||
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read", "im:read", "mpim:read"],
|
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read", "im:read", "mpim:read"],
|
||||||
metadata: JSON.stringify({
|
metadata: JSON.stringify({
|
||||||
projectId: project.id
|
slug,
|
||||||
|
description,
|
||||||
|
orgId: actorOrgId
|
||||||
}),
|
}),
|
||||||
redirectUri: `${appCfg.SITE_URL}/api/v1/slack/oauth_redirect`
|
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`
|
||||||
});
|
});
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSlackIntegrationByProjectId = async ({
|
const getSlackIntegrationsByOrg = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod
|
||||||
projectId
|
}: TGetSlackIntegrationByOrgDTO) => {
|
||||||
}: TGetSlackIntegrationByProjectIdDTO) => {
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||||
const slackIntegration = await slackIntegrationDAL.findOne({
|
|
||||||
projectId
|
const slackIntegrations = await slackIntegrationDAL.find({
|
||||||
|
orgId: actorOrgId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return slackIntegrations;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSlackIntegrationById = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
id
|
||||||
|
}: TGetSlackIntegrationByIdDTO) => {
|
||||||
|
const slackIntegration = await slackIntegrationDAL.findById(id);
|
||||||
|
if (!slackIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Slack integration not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
slackIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
return slackIntegration;
|
return slackIntegration;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,10 +210,8 @@ export const slackServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
id,
|
id,
|
||||||
isAccessRequestNotificationEnabled,
|
slug,
|
||||||
accessRequestChannels,
|
description
|
||||||
isSecretRequestNotificationEnabled,
|
|
||||||
secretRequestChannels
|
|
||||||
}: TUpdateSlackIntegrationDTO) => {
|
}: TUpdateSlackIntegrationDTO) => {
|
||||||
const slackIntegration = await slackIntegrationDAL.findById(id);
|
const slackIntegration = await slackIntegrationDAL.findById(id);
|
||||||
if (!slackIntegration) {
|
if (!slackIntegration) {
|
||||||
@@ -222,21 +219,20 @@ export const slackServiceFactory = ({
|
|||||||
message: "Slack integration not found"
|
message: "Slack integration not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
slackIntegration.projectId,
|
slackIntegration.orgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
return slackIntegrationDAL.updateById(slackIntegration.id, {
|
return slackIntegrationDAL.updateById(slackIntegration.id, {
|
||||||
isAccessRequestNotificationEnabled,
|
slug,
|
||||||
accessRequestChannels,
|
description
|
||||||
isSecretRequestNotificationEnabled,
|
|
||||||
secretRequestChannels
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -253,22 +249,24 @@ export const slackServiceFactory = ({
|
|||||||
message: "Slack integration not found"
|
message: "Slack integration not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
slackIntegration.projectId,
|
slackIntegration.orgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
return slackIntegrationDAL.deleteById(id);
|
return slackIntegrationDAL.deleteById(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getInstallUrl,
|
getInstallUrl,
|
||||||
getSlackIntegrationByProjectId,
|
getSlackIntegrationsByOrg,
|
||||||
|
getSlackIntegrationById,
|
||||||
completeSlackIntegration,
|
completeSlackIntegration,
|
||||||
getSlackInstaller,
|
getSlackInstaller,
|
||||||
updateSlackIntegration,
|
updateSlackIntegration,
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TGetSlackInstallUrlDTO = TProjectPermission;
|
export type TGetSlackInstallUrlDTO = {
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TGetSlackIntegrationByProjectIdDTO = TProjectPermission;
|
export type TGetSlackIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TUpdateSlackIntegrationDTO = {
|
export type TGetSlackIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
||||||
id: string;
|
|
||||||
isAccessRequestNotificationEnabled?: boolean;
|
export type TUpdateSlackIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
|
||||||
accessRequestChannels?: string;
|
TOrgPermission,
|
||||||
isSecretRequestNotificationEnabled?: boolean;
|
"orgId"
|
||||||
secretRequestChannels?: string;
|
>;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
|
||||||
|
|
||||||
export type TDeleteSlackIntegrationDTO = {
|
export type TDeleteSlackIntegrationDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TCompleteSlackIntegrationDTO = {
|
export type TCompleteSlackIntegrationDTO = {
|
||||||
projectId: string;
|
orgId: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
slackUserId: string;
|
slackUserId: string;
|
||||||
|
|||||||
@@ -45,4 +45,5 @@ export * from "./tags";
|
|||||||
export * from "./trustedIps";
|
export * from "./trustedIps";
|
||||||
export * from "./users";
|
export * from "./users";
|
||||||
export * from "./webhooks";
|
export * from "./webhooks";
|
||||||
|
export * from "./workflowIntegrations";
|
||||||
export * from "./workspace";
|
export * from "./workspace";
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation";
|
export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation";
|
||||||
export { fetchSlackInstallUrl, useGetSlackIntegrationByProject } from "./queries";
|
export { useGetSlackIntegrationByProject } from "./queries";
|
||||||
|
|||||||
@@ -11,16 +11,6 @@ export const slackKeys = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchSlackInstallUrl = async (workspaceId?: string) => {
|
|
||||||
const { data } = await apiRequest.get<string>("/api/v1/slack/install", {
|
|
||||||
params: {
|
|
||||||
projectId: workspaceId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchSlackIntegrationByProject = async (workspaceId?: string) => {
|
export const fetchSlackIntegrationByProject = async (workspaceId?: string) => {
|
||||||
const { data } = await apiRequest.get<ProjectSlackIntegration>("/api/v1/slack", {
|
const { data } = await apiRequest.get<ProjectSlackIntegration>("/api/v1/slack", {
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation";
|
||||||
|
export {
|
||||||
|
fetchSlackInstallUrl,
|
||||||
|
useGetSlackIntegrationById,
|
||||||
|
useGetSlackIntegrations
|
||||||
|
} from "./queries";
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { workflowIntegrationKeys } from "./queries";
|
||||||
|
import { TDeleteSlackIntegrationDTO, TUpdateSlackIntegrationDTO } from "./types";
|
||||||
|
|
||||||
|
export const useUpdateSlackIntegration = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, TUpdateSlackIntegrationDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
const { data } = await apiRequest.patch(`/api/v1/workflow-integrations/slack/${dto.id}`, dto);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId, id }) => {
|
||||||
|
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackWorkflowIntegration(id));
|
||||||
|
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackWorkflowIntegrations(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteSlackIntegration = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, TDeleteSlackIntegrationDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
const { data } = await apiRequest.delete(`/api/v1/workflow-integrations/slack/${dto.id}`);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId, id }) => {
|
||||||
|
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackWorkflowIntegration(id));
|
||||||
|
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackWorkflowIntegrations(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { SlackIntegration } from "./types";
|
||||||
|
|
||||||
|
export const workflowIntegrationKeys = {
|
||||||
|
getSlackWorkflowIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"],
|
||||||
|
getSlackWorkflowIntegration: (id?: string) => [{ id }, "slack-workflow-integration"]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchSlackInstallUrl = async ({
|
||||||
|
slug,
|
||||||
|
description
|
||||||
|
}: {
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.get<string>("/api/v1/workflow-integrations/slack/install", {
|
||||||
|
params: {
|
||||||
|
slug,
|
||||||
|
description
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchSlackIntegrations = async () => {
|
||||||
|
const { data } = await apiRequest.get<SlackIntegration[]>("/api/v1/workflow-integrations/slack");
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchSlackIntegrationById = async (id?: string) => {
|
||||||
|
const { data } = await apiRequest.get<SlackIntegration>(
|
||||||
|
`/api/v1/workflow-integrations/slack/${id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetSlackIntegrations = (orgId?: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: workflowIntegrationKeys.getSlackWorkflowIntegrations(orgId),
|
||||||
|
queryFn: () => fetchSlackIntegrations(),
|
||||||
|
enabled: Boolean(orgId)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useGetSlackIntegrationById = (id?: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: workflowIntegrationKeys.getSlackWorkflowIntegration(id),
|
||||||
|
queryFn: () => fetchSlackIntegrationById(id),
|
||||||
|
enabled: Boolean(id)
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export enum WorkflowIntegrationPlatform {
|
||||||
|
SLACK = "slack"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SlackIntegration = {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
teamName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateSlackIntegrationDTO = {
|
||||||
|
id: string;
|
||||||
|
orgId: string;
|
||||||
|
slug?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteSlackIntegrationDTO = {
|
||||||
|
id: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
@@ -5,11 +5,13 @@ import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
|||||||
import { OrgAuthTab } from "../OrgAuthTab";
|
import { OrgAuthTab } from "../OrgAuthTab";
|
||||||
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
||||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||||
|
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: "General", key: "tab-org-general" },
|
{ name: "General", key: "tab-org-general" },
|
||||||
{ name: "Security", key: "tab-org-security" },
|
{ name: "Security", key: "tab-org-security" },
|
||||||
{ name: "Encryption", key: "tab-org-encryption" },
|
{ name: "Encryption", key: "tab-org-encryption" },
|
||||||
|
{ name: "Workflow Integrations", key: "workflow-integrations" },
|
||||||
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
|
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
|
||||||
];
|
];
|
||||||
export const OrgTabGroup = () => {
|
export const OrgTabGroup = () => {
|
||||||
@@ -41,6 +43,9 @@ export const OrgTabGroup = () => {
|
|||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<OrgEncryptionTab />
|
<OrgEncryptionTab />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>
|
||||||
|
<OrgWorkflowIntegrationTab />
|
||||||
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<AuditLogStreamsTab />
|
<AuditLogStreamsTab />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
|||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
|
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
onToggle: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum WizardSteps {
|
||||||
|
SelectPlatform = "select-platform",
|
||||||
|
PlatformInputs = "platform-inputs"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLATFORM_LIST = [
|
||||||
|
{
|
||||||
|
icon: faSlack,
|
||||||
|
platform: WorkflowIntegrationPlatform.SLACK,
|
||||||
|
title: "Slack"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
|
||||||
|
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectPlatform);
|
||||||
|
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleFormReset = (state: boolean = false) => {
|
||||||
|
onToggle(state);
|
||||||
|
setWizardStep(WizardSteps.SelectPlatform);
|
||||||
|
setSelectedPlatform(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
|
||||||
|
<ModalContent
|
||||||
|
title="Add a workflow integration"
|
||||||
|
subTitle="Configure a workflow integration"
|
||||||
|
className="my-4"
|
||||||
|
>
|
||||||
|
<AnimatePresence exitBeforeEnter>
|
||||||
|
{wizardStep === WizardSteps.SelectPlatform && (
|
||||||
|
<motion.div
|
||||||
|
key="select-type-step"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<div className="mb-4 text-mineshaft-300">Select a platform</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{PLATFORM_LIST.map(({ icon, platform, title }) => (
|
||||||
|
<div
|
||||||
|
key={platform}
|
||||||
|
className="flex h-28 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedPlatform(platform);
|
||||||
|
setWizardStep(WizardSteps.PlatformInputs);
|
||||||
|
}}
|
||||||
|
onKeyDown={(evt) => {
|
||||||
|
if (evt.key === "Enter") {
|
||||||
|
setSelectedPlatform(platform);
|
||||||
|
setWizardStep(WizardSteps.PlatformInputs);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={icon} size="lg" />
|
||||||
|
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
{wizardStep === WizardSteps.PlatformInputs &&
|
||||||
|
selectedPlatform === WorkflowIntegrationPlatform.SLACK && (
|
||||||
|
<motion.div
|
||||||
|
key="slack-platform"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<SlackIntegrationForm onClose={() => onToggle(false)} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
|
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
id: string;
|
||||||
|
workflowPlatform: WorkflowIntegrationPlatform;
|
||||||
|
onOpenChange: (state: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IntegrationFormDetails = ({ isOpen, id, onOpenChange, workflowPlatform }: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent title="Workflow integration details">
|
||||||
|
{workflowPlatform === WorkflowIntegrationPlatform.SLACK && (
|
||||||
|
<SlackIntegrationForm id={id} onClose={() => onOpenChange(false)} />
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+188
@@ -0,0 +1,188 @@
|
|||||||
|
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { faEllipsis, faGear, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
|
import { withPermission } from "@app/hoc";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useDeleteSlackIntegration, useGetSlackIntegrations } from "@app/hooks/api";
|
||||||
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
|
import { AddWorkflowIntegrationForm } from "./AddWorkflowIntegrationForm";
|
||||||
|
import { IntegrationFormDetails } from "./IntegrationFormDetails";
|
||||||
|
|
||||||
|
export const OrgWorkflowIntegrationTab = withPermission(
|
||||||
|
() => {
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
|
"addWorkflowIntegration",
|
||||||
|
"integrationDetails",
|
||||||
|
"removeIntegration"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { data: slackIntegrations, isLoading: isSlackIntegrationsLoading } =
|
||||||
|
useGetSlackIntegrations(currentOrg?.id);
|
||||||
|
const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration();
|
||||||
|
|
||||||
|
const handleRemoveIntegration = async () => {
|
||||||
|
if (!currentOrg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { platform, id } = popUp.removeIntegration.data;
|
||||||
|
if (platform === WorkflowIntegrationPlatform.SLACK) {
|
||||||
|
await deleteSlackIntegration({
|
||||||
|
id,
|
||||||
|
orgId: currentOrg?.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpClose("removeIntegration");
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully deleted integration",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isIntegrationsLoading = isSlackIntegrationsLoading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Workflow Integrations</p>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Settings}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("addWorkflowIntegration");
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<p className="mb-4 text-gray-400">
|
||||||
|
Connect Infisical to other platforms for notification and workflow integrations.
|
||||||
|
</p>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Td>Provider</Td>
|
||||||
|
<Td>Alias</Td>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isIntegrationsLoading && (
|
||||||
|
<TableSkeleton columns={2} innerKey="integrations-loading" />
|
||||||
|
)}
|
||||||
|
{!isIntegrationsLoading && slackIntegrations && slackIntegrations.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<EmptyState title="No workflow integrations found" icon={faGear} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
{slackIntegrations?.map((slackIntegration) => (
|
||||||
|
<Tr key={slackIntegration.id}>
|
||||||
|
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
|
||||||
|
<FontAwesomeIcon icon={faSlack} />
|
||||||
|
<div className="ml-2">SLACK</div>
|
||||||
|
</Td>
|
||||||
|
<Td>{slackIntegration.slug}</Td>
|
||||||
|
<Td>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
handlePopUpOpen("integrationDetails", {
|
||||||
|
id: slackIntegration.id,
|
||||||
|
platform: WorkflowIntegrationPlatform.SLACK
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
More details
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
an={OrgPermissionSubjects.Settings}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={!isAllowed}
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
handlePopUpOpen("removeIntegration", {
|
||||||
|
id: slackIntegration.id,
|
||||||
|
slug: slackIntegration.slug,
|
||||||
|
platform: WorkflowIntegrationPlatform.SLACK
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<AddWorkflowIntegrationForm
|
||||||
|
isOpen={popUp.addWorkflowIntegration.isOpen}
|
||||||
|
onToggle={(state) => handlePopUpToggle("addWorkflowIntegration", state)}
|
||||||
|
/>
|
||||||
|
<IntegrationFormDetails
|
||||||
|
isOpen={popUp.integrationDetails?.isOpen}
|
||||||
|
workflowPlatform={popUp.integrationDetails?.data?.platform}
|
||||||
|
id={popUp.integrationDetails?.data?.id}
|
||||||
|
onOpenChange={(state) => handlePopUpToggle("integrationDetails", state)}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeIntegration.isOpen}
|
||||||
|
title={`Are you sure want to remove ${popUp?.removeIntegration?.data?.slug}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeIntegration", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={handleRemoveIntegration}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Settings }
|
||||||
|
);
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
fetchSlackInstallUrl,
|
||||||
|
useGetSlackIntegrationById,
|
||||||
|
useUpdateSlackIntegration
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id?: string;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const slackFormSchema = z.object({
|
||||||
|
slug: z.string(),
|
||||||
|
description: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TSlackFormData = z.infer<typeof slackFormSchema>;
|
||||||
|
|
||||||
|
export const SlackIntegrationForm = ({ id, onClose }: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = useForm<TSlackFormData>({
|
||||||
|
resolver: zodResolver(slackFormSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const [isConnectLoading, setIsConnectLoading] = useToggle(false);
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { data: slackIntegration } = useGetSlackIntegrationById(id);
|
||||||
|
const { mutateAsync: updateSlackIntegration } = useUpdateSlackIntegration();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (slackIntegration) {
|
||||||
|
setValue("slug", slackIntegration.slug);
|
||||||
|
setValue("description", slackIntegration.description ?? "");
|
||||||
|
}
|
||||||
|
}, [slackIntegration]);
|
||||||
|
|
||||||
|
const triggerSlackInstall = async (slug: string, description?: string) => {
|
||||||
|
setIsConnectLoading.on();
|
||||||
|
const slackInstallUrl = await fetchSlackInstallUrl({
|
||||||
|
slug,
|
||||||
|
description
|
||||||
|
});
|
||||||
|
if (slackInstallUrl) {
|
||||||
|
router.push(slackInstallUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSlackFormSubmit = async ({ slug, description }: TSlackFormData) => {
|
||||||
|
if (id && slackIntegration) {
|
||||||
|
if (!currentOrg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateSlackIntegration({
|
||||||
|
id,
|
||||||
|
orgId: currentOrg?.id,
|
||||||
|
slug,
|
||||||
|
description
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated Slack integration",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await triggerSlackInstall(slug, description);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleSlackFormSubmit)} autoComplete="off">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="slug"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Alias" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Description" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{slackIntegration && (
|
||||||
|
<FormControl label="Slack workspace">
|
||||||
|
<Input value={slackIntegration?.teamName} isReadOnly className="bg-white/[0.07]" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
<div className="mt-6 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting || isConnectLoading} isDisabled={!isDirty}>
|
||||||
|
{slackIntegration ? "Save" : "Connect"}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user