mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 08:27:22 +00:00
feat: finalized integration selection in project settings
This commit is contained in:
@@ -182,6 +182,7 @@ import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/
|
|||||||
import { secretVersionV2TagBridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
import { secretVersionV2TagBridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||||
import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal";
|
import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal";
|
||||||
import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service";
|
import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service";
|
||||||
|
import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||||
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
|
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
|
||||||
import { slackServiceFactory } from "@app/services/slack/slack-service";
|
import { slackServiceFactory } from "@app/services/slack/slack-service";
|
||||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
@@ -325,6 +326,7 @@ export const registerRoutes = async (
|
|||||||
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
||||||
|
|
||||||
const slackIntegrationDAL = slackIntegrationDALFactory(db);
|
const slackIntegrationDAL = slackIntegrationDALFactory(db);
|
||||||
|
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
|
||||||
|
|
||||||
const permissionService = permissionServiceFactory({
|
const permissionService = permissionServiceFactory({
|
||||||
permissionDAL,
|
permissionDAL,
|
||||||
@@ -725,7 +727,9 @@ export const registerRoutes = async (
|
|||||||
keyStore,
|
keyStore,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
certificateTemplateDAL
|
certificateTemplateDAL,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
slackIntegrationDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
@@ -1156,7 +1160,6 @@ export const registerRoutes = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const slackService = slackServiceFactory({
|
const slackService = slackServiceFactory({
|
||||||
projectDAL,
|
|
||||||
permissionService,
|
permissionService,
|
||||||
kmsService,
|
kmsService,
|
||||||
slackIntegrationDAL
|
slackIntegrationDAL
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
IntegrationsSchema,
|
IntegrationsSchema,
|
||||||
ProjectMembershipsSchema,
|
ProjectMembershipsSchema,
|
||||||
ProjectRolesSchema,
|
ProjectRolesSchema,
|
||||||
|
ProjectSlackConfigsSchema,
|
||||||
UserEncryptionKeysSchema,
|
UserEncryptionKeysSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
@@ -542,4 +543,82 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { serviceTokenData };
|
return { serviceTokenData };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:workspaceId/slack-config",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: ProjectSlackConfigsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slackIntegrationId: true,
|
||||||
|
isAccessRequestNotificationEnabled: true,
|
||||||
|
accessRequestChannels: true,
|
||||||
|
isSecretRequestNotificationEnabled: true,
|
||||||
|
secretRequestChannels: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const slackConfig = await server.services.project.getProjectSlackConfig({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
return slackConfig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PUT",
|
||||||
|
url: "/:workspaceId/slack-config",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
slackIntegrationId: z.string(),
|
||||||
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
|
accessRequestChannels: z.string(),
|
||||||
|
isSecretRequestNotificationEnabled: z.boolean(),
|
||||||
|
secretRequestChannels: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: ProjectSlackConfigsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slackIntegrationId: true,
|
||||||
|
isAccessRequestNotificationEnabled: true,
|
||||||
|
accessRequestChannels: true,
|
||||||
|
isSecretRequestNotificationEnabled: true,
|
||||||
|
secretRequestChannels: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const slackConfig = await server.services.project.updateProjectSlackConfig({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
return slackConfig;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ import { TProjectUserMembershipRoleDALFactory } from "../project-membership/proj
|
|||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||||
|
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectDALFactory } from "./project-dal";
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
|
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
|
||||||
@@ -43,6 +45,7 @@ import {
|
|||||||
TDeleteProjectDTO,
|
TDeleteProjectDTO,
|
||||||
TGetProjectDTO,
|
TGetProjectDTO,
|
||||||
TGetProjectKmsKey,
|
TGetProjectKmsKey,
|
||||||
|
TGetProjectSlackConfig,
|
||||||
TListProjectAlertsDTO,
|
TListProjectAlertsDTO,
|
||||||
TListProjectCasDTO,
|
TListProjectCasDTO,
|
||||||
TListProjectCertificateTemplatesDTO,
|
TListProjectCertificateTemplatesDTO,
|
||||||
@@ -54,6 +57,7 @@ import {
|
|||||||
TUpdateProjectDTO,
|
TUpdateProjectDTO,
|
||||||
TUpdateProjectKmsDTO,
|
TUpdateProjectKmsDTO,
|
||||||
TUpdateProjectNameDTO,
|
TUpdateProjectNameDTO,
|
||||||
|
TUpdateProjectSlackConfig,
|
||||||
TUpdateProjectVersionLimitDTO,
|
TUpdateProjectVersionLimitDTO,
|
||||||
TUpgradeProjectDTO
|
TUpgradeProjectDTO
|
||||||
} from "./project-types";
|
} from "./project-types";
|
||||||
@@ -76,6 +80,8 @@ type TProjectServiceFactoryDep = {
|
|||||||
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
|
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
||||||
|
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
|
||||||
|
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById">;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||||
@@ -126,7 +132,9 @@ export const projectServiceFactory = ({
|
|||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectBotDAL
|
projectBotDAL,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
slackIntegrationDAL
|
||||||
}: TProjectServiceFactoryDep) => {
|
}: TProjectServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Create workspace. Make user the admin
|
* Create workspace. Make user the admin
|
||||||
@@ -909,6 +917,113 @@ export const projectServiceFactory = ({
|
|||||||
return { secretManagerKmsKey: kmsKey };
|
return { secretManagerKmsKey: kmsKey };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProjectSlackConfig = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId
|
||||||
|
}: TGetProjectSlackConfig) => {
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Project not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
return projectSlackConfigDAL.findOne({
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProjectSlackConfig = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
slackIntegrationId,
|
||||||
|
isAccessRequestNotificationEnabled,
|
||||||
|
accessRequestChannels,
|
||||||
|
isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels
|
||||||
|
}: TUpdateProjectSlackConfig) => {
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Project not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const slackIntegration = await slackIntegrationDAL.findById(slackIntegrationId);
|
||||||
|
if (!slackIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Slack integration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
if (slackIntegration.orgId !== project.orgId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Selected slack integration is not in the same organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectSlackConfigDAL.transaction(async (tx) => {
|
||||||
|
const slackConfig = await projectSlackConfigDAL.findOne(
|
||||||
|
{
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (slackConfig) {
|
||||||
|
return projectSlackConfigDAL.updateById(
|
||||||
|
slackConfig.id,
|
||||||
|
{
|
||||||
|
slackIntegrationId,
|
||||||
|
isAccessRequestNotificationEnabled,
|
||||||
|
accessRequestChannels,
|
||||||
|
isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectSlackConfigDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
slackIntegrationId,
|
||||||
|
isAccessRequestNotificationEnabled,
|
||||||
|
accessRequestChannels,
|
||||||
|
isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createProject,
|
createProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
@@ -929,6 +1044,8 @@ export const projectServiceFactory = ({
|
|||||||
updateProjectKmsKey,
|
updateProjectKmsKey,
|
||||||
getProjectKmsBackup,
|
getProjectKmsBackup,
|
||||||
loadProjectKmsBackup,
|
loadProjectKmsBackup,
|
||||||
getProjectKmsKeys
|
getProjectKmsKeys,
|
||||||
|
getProjectSlackConfig,
|
||||||
|
updateProjectSlackConfig
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -123,3 +123,13 @@ export type TLoadProjectKmsBackupDTO = {
|
|||||||
export type TGetProjectKmsKey = TProjectPermission;
|
export type TGetProjectKmsKey = TProjectPermission;
|
||||||
|
|
||||||
export type TListProjectCertificateTemplatesDTO = TProjectPermission;
|
export type TListProjectCertificateTemplatesDTO = TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetProjectSlackConfig = TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateProjectSlackConfig = {
|
||||||
|
slackIntegrationId: string;
|
||||||
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
|
accessRequestChannels: string;
|
||||||
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
|
secretRequestChannels: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TProjectSlackConfigDALFactory = ReturnType<typeof projectSlackConfigDALFactory>;
|
||||||
|
|
||||||
|
export const projectSlackConfigDALFactory = (db: TDbClient) => {
|
||||||
|
const projectSlackConfigOrm = ormify(db, TableName.ProjectSlackConfigs);
|
||||||
|
|
||||||
|
return projectSlackConfigOrm;
|
||||||
|
};
|
||||||
@@ -38,7 +38,6 @@ export * from "./secretSharing";
|
|||||||
export * from "./secretSnapshots";
|
export * from "./secretSnapshots";
|
||||||
export * from "./serverDetails";
|
export * from "./serverDetails";
|
||||||
export * from "./serviceTokens";
|
export * from "./serviceTokens";
|
||||||
export * from "./slack";
|
|
||||||
export * from "./ssoConfig";
|
export * from "./ssoConfig";
|
||||||
export * from "./subscriptions";
|
export * from "./subscriptions";
|
||||||
export * from "./tags";
|
export * from "./tags";
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation";
|
|
||||||
export { useGetSlackIntegrationByProject } from "./queries";
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
|
||||||
|
|
||||||
import { slackKeys } 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/slack/${dto.id}`, dto);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
onSuccess: (_, { workspaceId }) => {
|
|
||||||
queryClient.invalidateQueries(slackKeys.getSlackIntegrationByProject(workspaceId));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDeleteSlackIntegration = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation<{}, {}, TDeleteSlackIntegrationDTO>({
|
|
||||||
mutationFn: async (dto) => {
|
|
||||||
const { data } = await apiRequest.delete(`/api/v1/slack/${dto.id}`);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
onSuccess: (_, { workspaceId }) => {
|
|
||||||
queryClient.invalidateQueries(slackKeys.getSlackIntegrationByProject(workspaceId));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
|
||||||
|
|
||||||
import { ProjectSlackIntegration } from "./types";
|
|
||||||
|
|
||||||
export const slackKeys = {
|
|
||||||
getSlackIntegrationByProject: (workspaceId?: string) => [
|
|
||||||
{ workspaceId },
|
|
||||||
"slack-integration-by-project"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchSlackIntegrationByProject = async (workspaceId?: string) => {
|
|
||||||
const { data } = await apiRequest.get<ProjectSlackIntegration>("/api/v1/slack", {
|
|
||||||
params: {
|
|
||||||
projectId: workspaceId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetSlackIntegrationByProject = (workspaceId?: string) =>
|
|
||||||
useQuery({
|
|
||||||
queryKey: slackKeys.getSlackIntegrationByProject(workspaceId),
|
|
||||||
queryFn: () => fetchSlackIntegrationByProject(workspaceId),
|
|
||||||
enabled: Boolean(workspaceId)
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
export type ProjectSlackIntegration = {
|
|
||||||
id: string;
|
|
||||||
teamName: string;
|
|
||||||
isAccessRequestNotificationEnabled: boolean;
|
|
||||||
accessRequestChannels: string;
|
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
|
||||||
secretRequestChannels: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TUpdateSlackIntegrationDTO = {
|
|
||||||
id: string;
|
|
||||||
workspaceId: string;
|
|
||||||
isAccessRequestNotificationEnabled?: boolean;
|
|
||||||
accessRequestChannels?: string;
|
|
||||||
isSecretRequestNotificationEnabled?: boolean;
|
|
||||||
secretRequestChannels?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TDeleteSlackIntegrationDTO = {
|
|
||||||
id: string;
|
|
||||||
workspaceId: string;
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation";
|
export {
|
||||||
|
useDeleteSlackIntegration,
|
||||||
|
useUpdateProjectSlackConfig,
|
||||||
|
useUpdateSlackIntegration
|
||||||
|
} from "./mutation";
|
||||||
export {
|
export {
|
||||||
fetchSlackInstallUrl,
|
fetchSlackInstallUrl,
|
||||||
useGetSlackIntegrationById,
|
useGetSlackIntegrationById,
|
||||||
|
|||||||
@@ -2,8 +2,13 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { workspaceKeys } from "../workspace/queries";
|
||||||
import { workflowIntegrationKeys } from "./queries";
|
import { workflowIntegrationKeys } from "./queries";
|
||||||
import { TDeleteSlackIntegrationDTO, TUpdateSlackIntegrationDTO } from "./types";
|
import {
|
||||||
|
TDeleteSlackIntegrationDTO,
|
||||||
|
TUpdateProjectSlackConfigDTO,
|
||||||
|
TUpdateSlackIntegrationDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const useUpdateSlackIntegration = () => {
|
export const useUpdateSlackIntegration = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -36,3 +41,20 @@ export const useDeleteSlackIntegration = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUpdateProjectSlackConfig = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (dto: TUpdateProjectSlackConfigDTO) => {
|
||||||
|
const { data } = await apiRequest.put(
|
||||||
|
`/api/v1/workspace/${dto.workspaceId}/slack-config`,
|
||||||
|
dto
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { workspaceId }) => {
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceSlackConfig(workspaceId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -20,3 +20,21 @@ export type TDeleteSlackIntegrationDTO = {
|
|||||||
id: string;
|
id: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProjectSlackConfig = {
|
||||||
|
id: string;
|
||||||
|
slackIntegrationId: string;
|
||||||
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
|
accessRequestChannels: string;
|
||||||
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
|
secretRequestChannels: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateProjectSlackConfigDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
slackIntegrationId: string;
|
||||||
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
|
accessRequestChannels: string;
|
||||||
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
|
secretRequestChannels: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export {
|
|||||||
useGetWorkspaceIndexStatus,
|
useGetWorkspaceIndexStatus,
|
||||||
useGetWorkspaceIntegrations,
|
useGetWorkspaceIntegrations,
|
||||||
useGetWorkspaceSecrets,
|
useGetWorkspaceSecrets,
|
||||||
|
useGetWorkspaceSlackConfig,
|
||||||
useGetWorkspaceUsers,
|
useGetWorkspaceUsers,
|
||||||
useListWorkspaceCas,
|
useListWorkspaceCas,
|
||||||
useListWorkspaceCertificates,
|
useListWorkspaceCertificates,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { TPkiCollection } from "../pkiCollections/types";
|
|||||||
import { EncryptedSecret } from "../secrets/types";
|
import { EncryptedSecret } from "../secrets/types";
|
||||||
import { userKeys } from "../users/queries";
|
import { userKeys } from "../users/queries";
|
||||||
import { TWorkspaceUser } from "../users/types";
|
import { TWorkspaceUser } from "../users/types";
|
||||||
|
import { ProjectSlackConfig } from "../workflowIntegrations/types";
|
||||||
import {
|
import {
|
||||||
CreateEnvironmentDTO,
|
CreateEnvironmentDTO,
|
||||||
CreateWorkspaceDTO,
|
CreateWorkspaceDTO,
|
||||||
@@ -71,7 +72,9 @@ export const workspaceKeys = {
|
|||||||
getWorkspacePkiCollections: (workspaceId: string) =>
|
getWorkspacePkiCollections: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-pki-collections"] as const,
|
[{ workspaceId }, "workspace-pki-collections"] as const,
|
||||||
getWorkspaceCertificateTemplates: (workspaceId: string) =>
|
getWorkspaceCertificateTemplates: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-certificate-templates"] as const
|
[{ workspaceId }, "workspace-certificate-templates"] as const,
|
||||||
|
getWorkspaceSlackConfig: (workspaceId: string) =>
|
||||||
|
[{ workspaceId }, "workspace-slack-config"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||||
@@ -667,3 +670,17 @@ export const useListWorkspaceCertificateTemplates = ({ workspaceId }: { workspac
|
|||||||
enabled: Boolean(workspaceId)
|
enabled: Boolean(workspaceId)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetWorkspaceSlackConfig = ({ workspaceId }: { workspaceId: string }) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<ProjectSlackConfig>(
|
||||||
|
`/api/v1/workspace/${workspaceId}/slack-config`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: Boolean(workspaceId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { useWorkspace } from "@app/context";
|
|||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import { EncryptionTab } from "./components/EncryptionTab";
|
import { EncryptionTab } from "./components/EncryptionTab";
|
||||||
import { NotificationTab } from "./components/NotificationSection";
|
|
||||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||||
import { WebhooksTab } from "./components/WebhooksTab";
|
import { WebhooksTab } from "./components/WebhooksTab";
|
||||||
|
import { WorkflowIntegrationTab } from "./components/WorkflowIntegrationSection";
|
||||||
|
|
||||||
export const ProjectSettingsPage = () => {
|
export const ProjectSettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -20,7 +20,7 @@ export const ProjectSettingsPage = () => {
|
|||||||
key: "tab-project-encryption",
|
key: "tab-project-encryption",
|
||||||
isHidden: currentWorkspace?.version !== ProjectVersion.V3
|
isHidden: currentWorkspace?.version !== ProjectVersion.V3
|
||||||
},
|
},
|
||||||
{ name: "Notification", key: "tab-project-notification" },
|
{ name: "Workflow Integrations", key: "tab-workflow-integrations" },
|
||||||
{ name: "Webhooks", key: "tab-project-webhooks" }
|
{ name: "Webhooks", key: "tab-project-webhooks" }
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ export const ProjectSettingsPage = () => {
|
|||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
)}
|
)}
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<NotificationTab />
|
<WorkflowIntegrationTab />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<WebhooksTab />
|
<WebhooksTab />
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./NotificationTab";
|
|
||||||
+90
-119
@@ -1,45 +1,45 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useRouter } from "next/router";
|
import Link from "next/link";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ContentLoader,
|
ContentLoader,
|
||||||
DeleteActionModal,
|
EmptyState,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
Switch
|
Switch
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
|
||||||
import {
|
import {
|
||||||
fetchSlackInstallUrl,
|
useGetSlackIntegrations,
|
||||||
useDeleteSlackIntegration,
|
useGetWorkspaceSlackConfig,
|
||||||
useGetSlackIntegrationByProject,
|
useUpdateProjectSlackConfig
|
||||||
useUpdateSlackIntegration
|
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
|
slackIntegrationId: z.string(),
|
||||||
isSecretRequestNotificationEnabled: z.boolean(),
|
isSecretRequestNotificationEnabled: z.boolean(),
|
||||||
secretRequestChannels: z.string(),
|
secretRequestChannels: z.string().default(""),
|
||||||
isAccessRequestNotificationEnabled: z.boolean(),
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
accessRequestChannels: z.string()
|
accessRequestChannels: z.string().default("")
|
||||||
});
|
});
|
||||||
|
|
||||||
type TSlackIntegrationForm = z.infer<typeof formSchema>;
|
type TSlackConfigForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export const NotificationTab = () => {
|
export const WorkflowIntegrationTab = () => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { data: slackIntegration, isLoading: isSlackIntegrationLoading } =
|
const { data: slackConfig, isLoading: isSlackConfigLoading } = useGetWorkspaceSlackConfig({
|
||||||
useGetSlackIntegrationByProject(currentWorkspace?.id);
|
workspaceId: currentWorkspace?.id ?? ""
|
||||||
const { mutateAsync: updateSlackIntegration } = useUpdateSlackIntegration();
|
});
|
||||||
const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration();
|
const { data: slackIntegrations } = useGetSlackIntegrations(currentWorkspace?.orgId);
|
||||||
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([
|
const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectSlackConfig();
|
||||||
"deleteSlackIntegration"
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -47,29 +47,27 @@ export const NotificationTab = () => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
formState: { isDirty, isSubmitting }
|
formState: { isDirty, isSubmitting }
|
||||||
} = useForm<TSlackIntegrationForm>({
|
} = useForm<TSlackConfigForm>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
isSecretRequestNotificationEnabled: slackIntegration?.isSecretRequestNotificationEnabled,
|
isAccessRequestNotificationEnabled: false,
|
||||||
secretRequestChannels: slackIntegration?.secretRequestChannels || "",
|
accessRequestChannels: "",
|
||||||
isAccessRequestNotificationEnabled: slackIntegration?.isAccessRequestNotificationEnabled,
|
isSecretRequestNotificationEnabled: false,
|
||||||
accessRequestChannels: slackIntegration?.accessRequestChannels || ""
|
secretRequestChannels: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const [isConnectToSlackLoading, setIsConnectToSlackLoading] = useToggle(false);
|
|
||||||
const [isReinstallLoading, setIsReinstallLoading] = useToggle(false);
|
|
||||||
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
|
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
|
||||||
|
const selectedSlackIntegrationId = watch("slackIntegrationId");
|
||||||
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
|
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
|
||||||
|
|
||||||
const handleIntegrationSave = async (data: TSlackIntegrationForm) => {
|
const handleIntegrationSave = async (data: TSlackConfigForm) => {
|
||||||
if (!currentWorkspace || !slackIntegration) {
|
if (!currentWorkspace) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await updateSlackIntegration({
|
|
||||||
workspaceId: currentWorkspace?.id,
|
await updateProjectSlackConfig({
|
||||||
id: slackIntegration?.id,
|
workspaceId: currentWorkspace.id,
|
||||||
...data
|
...data
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,96 +77,76 @@ export const NotificationTab = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIntegrationDelete = async () => {
|
|
||||||
if (!currentWorkspace || !slackIntegration) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await deleteSlackIntegration({
|
|
||||||
workspaceId: currentWorkspace.id,
|
|
||||||
id: slackIntegration.id
|
|
||||||
});
|
|
||||||
|
|
||||||
handlePopUpToggle("deleteSlackIntegration", false);
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
type: "success",
|
|
||||||
text: "Successfully deleted slack integration"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerSlackInstall = async () => {
|
|
||||||
const slackInstallUrl = await fetchSlackInstallUrl(currentWorkspace?.id);
|
|
||||||
if (slackInstallUrl) {
|
|
||||||
router.push(slackInstallUrl);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (slackIntegration) {
|
if (slackConfig) {
|
||||||
|
setValue("slackIntegrationId", slackConfig.slackIntegrationId);
|
||||||
setValue(
|
setValue(
|
||||||
"isSecretRequestNotificationEnabled",
|
"isSecretRequestNotificationEnabled",
|
||||||
slackIntegration.isSecretRequestNotificationEnabled
|
slackConfig.isSecretRequestNotificationEnabled
|
||||||
);
|
);
|
||||||
setValue("secretRequestChannels", slackIntegration.secretRequestChannels);
|
setValue("secretRequestChannels", slackConfig.secretRequestChannels);
|
||||||
setValue(
|
setValue(
|
||||||
"isAccessRequestNotificationEnabled",
|
"isAccessRequestNotificationEnabled",
|
||||||
slackIntegration.isAccessRequestNotificationEnabled
|
slackConfig.isAccessRequestNotificationEnabled
|
||||||
);
|
);
|
||||||
setValue("accessRequestChannels", slackIntegration.accessRequestChannels);
|
setValue("accessRequestChannels", slackConfig.accessRequestChannels);
|
||||||
}
|
}
|
||||||
}, [slackIntegration]);
|
}, [slackConfig]);
|
||||||
|
|
||||||
if (isSlackIntegrationLoading) {
|
if (isSlackConfigLoading) {
|
||||||
return <ContentLoader />;
|
return <ContentLoader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return !slackIntegrations?.length ? (
|
||||||
<>
|
<EmptyState title="You do not have any integrations configured.">
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<Link href={`/org/${currentWorkspace?.orgId}/settings?tab=workflowIntegrations`}>
|
||||||
<div className="flex justify-between">
|
<div className="mt-2 underline decoration-primary-800 underline-offset-4 duration-200 hover:cursor-pointer hover:text-mineshaft-100 hover:decoration-primary-600">
|
||||||
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">
|
Create one now
|
||||||
Slack Integration
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-4 text-gray-400">
|
</Link>
|
||||||
This integration allows you to send notifications to your Slack workspace in response to
|
</EmptyState>
|
||||||
events in your project.
|
) : (
|
||||||
</p>
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
{!slackIntegration && (
|
<div className="flex justify-between">
|
||||||
<Button
|
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Slack Integration</h2>
|
||||||
isLoading={isConnectToSlackLoading}
|
</div>
|
||||||
onClick={async () => {
|
<p className="mb-4 text-gray-400">
|
||||||
setIsConnectToSlackLoading.on();
|
This integration allows you to send notifications to your Slack workspace in response to
|
||||||
await triggerSlackInstall();
|
events in your project.
|
||||||
}}
|
</p>
|
||||||
>
|
<form onSubmit={handleSubmit(handleIntegrationSave)}>
|
||||||
Connect to Slack
|
<div className="max-w-md">
|
||||||
</Button>
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||||
)}
|
{(isAllowed) => (
|
||||||
{slackIntegration && (
|
<Controller
|
||||||
<form onSubmit={handleSubmit(handleIntegrationSave)}>
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<div>Connected Slack workspace: {slackIntegration.teamName}</div>
|
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||||
<div className="mt-2 mb-6">
|
<Select
|
||||||
<Button
|
{...field}
|
||||||
colorSchema="secondary"
|
isDisabled={!isAllowed}
|
||||||
size="xs"
|
onValueChange={onChange}
|
||||||
isLoading={isReinstallLoading}
|
defaultValue={slackConfig?.slackIntegrationId}
|
||||||
onClick={async () => {
|
className="w-3/4 bg-mineshaft-600"
|
||||||
setIsReinstallLoading.on();
|
>
|
||||||
await triggerSlackInstall();
|
{slackIntegrations?.map((slackIntegration) => (
|
||||||
}}
|
<SelectItem
|
||||||
>
|
value={slackIntegration.id}
|
||||||
Reinstall
|
key={`slack-integration-${slackIntegration.id}`}
|
||||||
</Button>
|
>
|
||||||
<Button
|
{slackIntegration.slug}
|
||||||
colorSchema="secondary"
|
</SelectItem>
|
||||||
size="xs"
|
))}
|
||||||
className="ml-2"
|
</Select>
|
||||||
onClick={() => handlePopUpOpen("deleteSlackIntegration")}
|
</FormControl>
|
||||||
>
|
)}
|
||||||
Delete
|
control={control}
|
||||||
</Button>
|
name="slackIntegrationId"
|
||||||
</div>
|
/>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
{selectedSlackIntegrationId && (
|
||||||
|
<>
|
||||||
<h2 className="mb-2 flex-1 text-lg font-semibold text-mineshaft-100">Events</h2>
|
<h2 className="mb-2 flex-1 text-lg font-semibold text-mineshaft-100">Events</h2>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -260,16 +238,9 @@ export const NotificationTab = () => {
|
|||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</form>
|
||||||
<DeleteActionModal
|
</div>
|
||||||
isOpen={popUp.deleteSlackIntegration.isOpen}
|
|
||||||
title="Are you sure want to delete your Slack integration?"
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteSlackIntegration", isOpen)}
|
|
||||||
deleteKey="confirm"
|
|
||||||
onDeleteApproved={handleIntegrationDelete}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./WorkflowIntegrationTab";
|
||||||
Reference in New Issue
Block a user