mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 13:27:22 +00:00
fix(microsoft-teams-integration): bug fixes
This commit is contained in:
@@ -51,6 +51,9 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
table.binary("encryptedAccessToken").nullable();
|
table.binary("encryptedAccessToken").nullable();
|
||||||
table.binary("encryptedBotAccessToken").nullable();
|
table.binary("encryptedBotAccessToken").nullable();
|
||||||
|
|
||||||
|
table.timestamp("accessTokenExpiresAt").nullable();
|
||||||
|
table.timestamp("botAccessTokenExpiresAt").nullable();
|
||||||
|
|
||||||
table.timestamps(true, true, true);
|
table.timestamps(true, true, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export const MicrosoftTeamsIntegrationsSchema = z.object({
|
|||||||
tenantId: z.string(),
|
tenantId: z.string(),
|
||||||
encryptedAccessToken: zodBuffer.nullable().optional(),
|
encryptedAccessToken: zodBuffer.nullable().optional(),
|
||||||
encryptedBotAccessToken: zodBuffer.nullable().optional(),
|
encryptedBotAccessToken: zodBuffer.nullable().optional(),
|
||||||
|
accessTokenExpiresAt: z.date().nullable().optional(),
|
||||||
|
botAccessTokenExpiresAt: z.date().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkfl
|
|||||||
.sendNotification({
|
.sendNotification({
|
||||||
notification,
|
notification,
|
||||||
target: data,
|
target: data,
|
||||||
tenantId: microsoftTeamsConfig.tenantId
|
tenantId: microsoftTeamsConfig.tenantId,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsConfig.id,
|
||||||
|
orgId: project.orgId
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error(error, "Error sending Microsoft Teams notification");
|
logger.error(error, "Error sending Microsoft Teams notification");
|
||||||
@@ -79,7 +81,9 @@ export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkfl
|
|||||||
.sendNotification({
|
.sendNotification({
|
||||||
notification,
|
notification,
|
||||||
target: data,
|
target: data,
|
||||||
tenantId: microsoftTeamsConfig.tenantId
|
tenantId: microsoftTeamsConfig.tenantId,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsConfig.id,
|
||||||
|
orgId: project.orgId
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error(error, "Error sending Microsoft Teams notification");
|
logger.error(error, "Error sending Microsoft Teams notification");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable class-methods-use-this */
|
/* eslint-disable class-methods-use-this */
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { TeamsActivityHandler, TurnContext } from "botbuilder";
|
import { TeamsActivityHandler, TurnContext } from "botbuilder";
|
||||||
|
import { Knex } from "knex";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
@@ -8,68 +9,218 @@ import { BadRequestError } from "@app/lib/errors";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
|
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||||
import { WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
import { WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||||
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||||
|
|
||||||
export const getMicrosoftTeamsAccessToken = async ({
|
export const getMicrosoftTeamsAccessToken = async (
|
||||||
tenantId,
|
{
|
||||||
clientId,
|
orgId,
|
||||||
clientSecret,
|
microsoftTeamsIntegrationId,
|
||||||
getBotFrameworkToken = false
|
tenantId,
|
||||||
}: {
|
clientId,
|
||||||
tenantId: string;
|
clientSecret,
|
||||||
clientId: string;
|
kmsService,
|
||||||
clientSecret: string;
|
microsoftTeamsIntegrationDAL,
|
||||||
getBotFrameworkToken?: boolean;
|
getBotFrameworkToken
|
||||||
}) => {
|
}: {
|
||||||
const details = getBotFrameworkToken
|
microsoftTeamsIntegrationId: string;
|
||||||
? {
|
orgId: string;
|
||||||
uri: "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token",
|
tenantId: string;
|
||||||
scope: "https://api.botframework.com/.default"
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne" | "update">;
|
||||||
|
getBotFrameworkToken?: boolean;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const details = getBotFrameworkToken
|
||||||
|
? {
|
||||||
|
uri: "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token",
|
||||||
|
scope: "https://api.botframework.com/.default"
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
uri: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
||||||
|
scope: "https://graph.microsoft.com/.default"
|
||||||
|
};
|
||||||
|
|
||||||
|
const integration = await microsoftTeamsIntegrationDAL.findOne(
|
||||||
|
{
|
||||||
|
id: microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
throw new BadRequestError({ message: "Microsoft Teams integration not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getBotFrameworkToken) {
|
||||||
|
const currentTime = new Date(new Date().getTime() + 5 * 60 * 1000);
|
||||||
|
|
||||||
|
if (
|
||||||
|
integration.encryptedBotAccessToken &&
|
||||||
|
integration.botAccessTokenExpiresAt &&
|
||||||
|
integration.botAccessTokenExpiresAt > currentTime
|
||||||
|
) {
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
const botAccessToken = decryptor({
|
||||||
|
cipherTextBlob: integration.encryptedBotAccessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
return botAccessToken.toString();
|
||||||
}
|
}
|
||||||
: {
|
} else {
|
||||||
uri: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
const currentTime = new Date(new Date().getTime() + 5 * 60 * 1000);
|
||||||
scope: "https://graph.microsoft.com/.default"
|
|
||||||
};
|
|
||||||
|
|
||||||
const tokenResponse = await axios.post<{ access_token: string }>(
|
if (
|
||||||
details.uri,
|
integration.encryptedAccessToken &&
|
||||||
new URLSearchParams({
|
integration.accessTokenExpiresAt &&
|
||||||
client_id: clientId,
|
integration.accessTokenExpiresAt > currentTime
|
||||||
client_secret: clientSecret,
|
) {
|
||||||
scope: details.scope,
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
grant_type: "client_credentials"
|
orgId,
|
||||||
})
|
type: KmsDataKey.Organization
|
||||||
);
|
});
|
||||||
|
|
||||||
return tokenResponse.data.access_token;
|
const accessToken = decryptor({
|
||||||
|
cipherTextBlob: integration.encryptedAccessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
return accessToken.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenResponse = await axios.post<{ access_token: string; expires_in: number }>(
|
||||||
|
details.uri,
|
||||||
|
new URLSearchParams({
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
scope: details.scope,
|
||||||
|
grant_type: "client_credentials"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (getBotFrameworkToken) {
|
||||||
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedBotAccessToken } = encryptor({
|
||||||
|
plainText: Buffer.from(tokenResponse.data.access_token)
|
||||||
|
});
|
||||||
|
|
||||||
|
const expiresAt = new Date(new Date().getTime() + tokenResponse.data.expires_in * 1000);
|
||||||
|
|
||||||
|
await microsoftTeamsIntegrationDAL.update(
|
||||||
|
{
|
||||||
|
id: microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
botAccessTokenExpiresAt: expiresAt,
|
||||||
|
encryptedBotAccessToken
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedAccessToken } = encryptor({
|
||||||
|
plainText: Buffer.from(tokenResponse.data.access_token)
|
||||||
|
});
|
||||||
|
|
||||||
|
const expiresAt = new Date(new Date().getTime() + tokenResponse.data.expires_in * 1000);
|
||||||
|
|
||||||
|
await microsoftTeamsIntegrationDAL.update(
|
||||||
|
{
|
||||||
|
id: microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessTokenExpiresAt: expiresAt,
|
||||||
|
encryptedAccessToken
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse.data.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
logger.error(
|
||||||
|
error.response?.data,
|
||||||
|
`getMicrosoftTeamsAccessToken: Error fetching Microsoft Teams access token [status-code=${error.response?.status}]`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(error, "getMicrosoftTeamsAccessToken: Error fetching Microsoft Teams access token");
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isBotInstalledInTenant = async ({
|
export const isBotInstalledInTenant = async (
|
||||||
tenantId,
|
{
|
||||||
botAppId,
|
tenantId,
|
||||||
botAppPassword,
|
botAppId,
|
||||||
botId
|
botAppPassword,
|
||||||
}: {
|
botId,
|
||||||
tenantId: string;
|
orgId,
|
||||||
botAppId: string;
|
kmsService,
|
||||||
botAppPassword: string;
|
microsoftTeamsIntegrationDAL,
|
||||||
botId: string;
|
microsoftTeamsIntegrationId
|
||||||
}) => {
|
}: {
|
||||||
|
tenantId: string;
|
||||||
|
botAppId: string;
|
||||||
|
botAppPassword: string;
|
||||||
|
botId: string;
|
||||||
|
orgId: string;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne" | "update">;
|
||||||
|
microsoftTeamsIntegrationId: string;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const botAccessToken = await getMicrosoftTeamsAccessToken({
|
const botAccessToken = await getMicrosoftTeamsAccessToken(
|
||||||
tenantId,
|
{
|
||||||
clientId: botAppId.toString(),
|
tenantId,
|
||||||
clientSecret: botAppPassword.toString(),
|
clientId: botAppId.toString(),
|
||||||
getBotFrameworkToken: true
|
clientSecret: botAppPassword.toString(),
|
||||||
}).catch(() => null);
|
getBotFrameworkToken: true,
|
||||||
|
orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
).catch(() => null);
|
||||||
|
|
||||||
const accessToken = await getMicrosoftTeamsAccessToken({
|
const accessToken = await getMicrosoftTeamsAccessToken(
|
||||||
tenantId,
|
{
|
||||||
clientId: botAppId.toString(),
|
orgId,
|
||||||
clientSecret: botAppPassword.toString()
|
tenantId,
|
||||||
}).catch(() => null);
|
clientId: botAppId.toString(),
|
||||||
|
clientSecret: botAppPassword.toString(),
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
).catch(() => null);
|
||||||
|
|
||||||
|
console.log("botAccessToken", botAccessToken);
|
||||||
|
console.log("accessToken", accessToken);
|
||||||
|
|
||||||
if (!botAccessToken || !accessToken) {
|
if (!botAccessToken || !accessToken) {
|
||||||
return {
|
return {
|
||||||
@@ -105,6 +256,12 @@ export const isBotInstalledInTenant = async ({
|
|||||||
|
|
||||||
const botInstalledInTenant = appsResponse.data.value.find((a) => a.externalId === botId);
|
const botInstalledInTenant = appsResponse.data.value.find((a) => a.externalId === botId);
|
||||||
|
|
||||||
|
for (const app of appsResponse.data.value) {
|
||||||
|
if (app.displayName.toLowerCase().includes("infisical")) {
|
||||||
|
console.log(`${app.displayName} - ${app.externalId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!botInstalledInTenant) {
|
if (!botInstalledInTenant) {
|
||||||
return {
|
return {
|
||||||
installed: false,
|
installed: false,
|
||||||
@@ -311,17 +468,16 @@ export class TeamsBot extends TeamsActivityHandler {
|
|||||||
await super.run(context);
|
await super.run(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMessageToChannel(tenantId: string, channelId: string, teamId: string, notification: TNotification) {
|
async sendMessageToChannel(
|
||||||
|
botAccessToken: string,
|
||||||
|
tenantId: string,
|
||||||
|
channelId: string,
|
||||||
|
teamId: string,
|
||||||
|
notification: TNotification
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const { adaptiveCard } = buildTeamsPayload(notification);
|
const { adaptiveCard } = buildTeamsPayload(notification);
|
||||||
|
|
||||||
const botToken = await getMicrosoftTeamsAccessToken({
|
|
||||||
tenantId,
|
|
||||||
clientId: this.botAppId,
|
|
||||||
clientSecret: this.botAppPassword,
|
|
||||||
getBotFrameworkToken: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const adaptiveCardActivity = {
|
const adaptiveCardActivity = {
|
||||||
type: "message",
|
type: "message",
|
||||||
attachments: [
|
attachments: [
|
||||||
@@ -349,7 +505,7 @@ export class TeamsBot extends TeamsActivityHandler {
|
|||||||
adaptiveCardActivity,
|
adaptiveCardActivity,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${botToken}`,
|
Authorization: `Bearer ${botAccessToken}`,
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,18 +520,12 @@ export class TeamsBot extends TeamsActivityHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// todo: filter out teams that the bot is not a member of
|
// todo: filter out teams that the bot is not a member of
|
||||||
async getTeamsAndChannels(tenantId: string, internalAppId: string) {
|
async getTeamsAndChannels(accessToken: string, tenantId: string, internalAppId: string) {
|
||||||
try {
|
try {
|
||||||
const token = await getMicrosoftTeamsAccessToken({
|
|
||||||
tenantId,
|
|
||||||
clientId: this.botAppId,
|
|
||||||
clientSecret: this.botAppPassword
|
|
||||||
});
|
|
||||||
|
|
||||||
const teamsResponse = await axios
|
const teamsResponse = await axios
|
||||||
.get<{ value: { displayName: string; id: string }[] }>(`https://graph.microsoft.com/v1.0/teams`, {
|
.get<{ value: { displayName: string; id: string }[] }>(`https://graph.microsoft.com/v1.0/teams`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -393,7 +543,7 @@ export class TeamsBot extends TeamsActivityHandler {
|
|||||||
`https://graph.microsoft.com/v1.0/teams/${team.id}/installedApps?$expand=teamsAppDefinition`,
|
`https://graph.microsoft.com/v1.0/teams/${team.id}/installedApps?$expand=teamsAppDefinition`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -412,7 +562,7 @@ export class TeamsBot extends TeamsActivityHandler {
|
|||||||
`https://graph.microsoft.com/v1.0/teams/${team.id}/channels`,
|
`https://graph.microsoft.com/v1.0/teams/${team.id}/channels`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { KmsDataKey } from "../kms/kms-types";
|
|||||||
import { TSuperAdminDALFactory } from "../super-admin/super-admin-dal";
|
import { TSuperAdminDALFactory } from "../super-admin/super-admin-dal";
|
||||||
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||||
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||||
import { isBotInstalledInTenant, TeamsBot } from "./microsoft-teams-fns";
|
import { getMicrosoftTeamsAccessToken, isBotInstalledInTenant, TeamsBot } from "./microsoft-teams-fns";
|
||||||
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||||
import {
|
import {
|
||||||
TCheckInstallationStatusDTO,
|
TCheckInstallationStatusDTO,
|
||||||
@@ -56,6 +56,7 @@ type TMicrosoftTeamsServiceFactoryDep = {
|
|||||||
| "findById"
|
| "findById"
|
||||||
| "findByIdWithWorkflowIntegrationDetails"
|
| "findByIdWithWorkflowIntegrationDetails"
|
||||||
| "findWithWorkflowIntegrationDetails"
|
| "findWithWorkflowIntegrationDetails"
|
||||||
|
| "update"
|
||||||
>;
|
>;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
|
||||||
@@ -131,16 +132,6 @@ export const microsoftTeamsServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
workflowIntegrationId
|
workflowIntegrationId
|
||||||
}: TCheckInstallationStatusDTO) => {
|
}: TCheckInstallationStatusDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorOrgId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId
|
|
||||||
);
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
|
||||||
|
|
||||||
const microsoftTeamsIntegration =
|
const microsoftTeamsIntegration =
|
||||||
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
||||||
|
|
||||||
@@ -150,6 +141,16 @@ export const microsoftTeamsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
microsoftTeamsIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
if (!serverCfg) {
|
if (!serverCfg) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -175,7 +176,11 @@ export const microsoftTeamsServiceFactory = ({
|
|||||||
tenantId: microsoftTeamsIntegration.tenantId,
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
botAppId: decryptedAppId.toString(),
|
botAppId: decryptedAppId.toString(),
|
||||||
botAppPassword: decryptedAppPassword.toString(),
|
botAppPassword: decryptedAppPassword.toString(),
|
||||||
botId: decryptedBotId.toString()
|
botId: decryptedBotId.toString(),
|
||||||
|
orgId: microsoftTeamsIntegration.orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsIntegration.id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!teamsBotInfo.installed) {
|
if (!teamsBotInfo.installed) {
|
||||||
@@ -258,12 +263,20 @@ export const microsoftTeamsServiceFactory = ({
|
|||||||
const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret);
|
const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret);
|
||||||
const botId = decryptWithRoot(encryptedMicrosoftTeamsBotId);
|
const botId = decryptWithRoot(encryptedMicrosoftTeamsBotId);
|
||||||
|
|
||||||
const teamsBotInfo = await isBotInstalledInTenant({
|
const teamsBotInfo = await isBotInstalledInTenant(
|
||||||
tenantId: microsoftTeamsIntegration.tenantId,
|
{
|
||||||
botAppId: botAppId.toString(),
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
botAppPassword: botAppPassword.toString(),
|
botAppId: botAppId.toString(),
|
||||||
botId: botId.toString()
|
botAppPassword: botAppPassword.toString(),
|
||||||
});
|
botId: botId.toString(),
|
||||||
|
orgId: workflowIntegration.orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsIntegration.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
if (teamsBotInfo.installed) {
|
if (teamsBotInfo.installed) {
|
||||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
orgId: workflowIntegration.orgId,
|
orgId: workflowIntegration.orgId,
|
||||||
@@ -451,16 +464,6 @@ export const microsoftTeamsServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getTeams = async ({ actorId, actor, actorOrgId, actorAuthMethod, workflowIntegrationId }: TGetTeamsDTO) => {
|
const getTeams = async ({ actorId, actor, actorOrgId, actorAuthMethod, workflowIntegrationId }: TGetTeamsDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorOrgId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId
|
|
||||||
);
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
|
||||||
|
|
||||||
const microsoftTeamsIntegration =
|
const microsoftTeamsIntegration =
|
||||||
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
||||||
|
|
||||||
@@ -470,6 +473,16 @@ export const microsoftTeamsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
microsoftTeamsIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
if (!teamsBot || !adapter) {
|
if (!teamsBot || !adapter) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Unable to get teams and channels because the Microsoft Teams bot is uninitialized"
|
message: "Unable to get teams and channels because the Microsoft Teams bot is uninitialized"
|
||||||
@@ -498,11 +511,15 @@ export const microsoftTeamsServiceFactory = ({
|
|||||||
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||||
const decryptedBotId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsBotId);
|
const decryptedBotId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsBotId);
|
||||||
|
|
||||||
const { installed, internalId } = await isBotInstalledInTenant({
|
const { installed, internalId, accessToken } = await isBotInstalledInTenant({
|
||||||
tenantId: microsoftTeamsIntegration.tenantId,
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
botAppId: decryptedAppId.toString(),
|
botAppId: decryptedAppId.toString(),
|
||||||
botAppPassword: decryptedAppPassword.toString(),
|
botAppPassword: decryptedAppPassword.toString(),
|
||||||
botId: decryptedBotId.toString()
|
botId: decryptedBotId.toString(),
|
||||||
|
orgId: actorOrgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsIntegration.id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!installed) {
|
if (!installed) {
|
||||||
@@ -511,7 +528,7 @@ export const microsoftTeamsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const teams = await teamsBot.getTeamsAndChannels(microsoftTeamsIntegration.tenantId, internalId);
|
const teams = await teamsBot.getTeamsAndChannels(accessToken, microsoftTeamsIntegration.tenantId, internalId);
|
||||||
|
|
||||||
return teams;
|
return teams;
|
||||||
};
|
};
|
||||||
@@ -580,15 +597,53 @@ export const microsoftTeamsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendNotification = async ({ tenantId, target, notification }: TSendNotificationDTO) => {
|
const sendNotification = async ({
|
||||||
|
tenantId,
|
||||||
|
target,
|
||||||
|
notification,
|
||||||
|
orgId,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
}: TSendNotificationDTO) => {
|
||||||
if (!teamsBot || !adapter) {
|
if (!teamsBot || !adapter) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Unable to send notification because the Microsoft Teams bot is uninitialized"
|
message: "Unable to send notification because the Microsoft Teams bot is uninitialized"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
if (!serverCfg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to get server configuration."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsAppId ||
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsClientSecret ||
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const botAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||||
|
const botAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||||
|
|
||||||
|
const botAccessToken = await getMicrosoftTeamsAccessToken({
|
||||||
|
tenantId,
|
||||||
|
clientId: botAppId.toString(),
|
||||||
|
clientSecret: botAppPassword.toString(),
|
||||||
|
getBotFrameworkToken: true,
|
||||||
|
orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
});
|
||||||
|
|
||||||
for await (const channelId of target.channelIds) {
|
for await (const channelId of target.channelIds) {
|
||||||
await teamsBot.sendMessageToChannel(tenantId, channelId, target.teamId, notification);
|
await teamsBot.sendMessageToChannel(botAccessToken, tenantId, channelId, target.teamId, notification);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export type TDeleteMicrosoftTeamsIntegrationDTO = {
|
|||||||
|
|
||||||
export type TSendNotificationDTO = {
|
export type TSendNotificationDTO = {
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
|
microsoftTeamsIntegrationId: string;
|
||||||
|
orgId: string;
|
||||||
target: {
|
target: {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
channelIds: string[];
|
channelIds: string[];
|
||||||
|
|||||||
+3
-4
@@ -86,7 +86,6 @@ export const AddWorkflowIntegrationModal = ({ isOpen, onToggle }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="relative" key={platform}>
|
<div className="relative" key={platform}>
|
||||||
<div
|
<div
|
||||||
key={platform}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex h-32 w-36 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",
|
"flex h-32 w-36 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",
|
||||||
isConfigured && "border-primary/50 opacity-60"
|
isConfigured && "border-primary/50 opacity-60"
|
||||||
@@ -110,7 +109,7 @@ export const AddWorkflowIntegrationModal = ({ isOpen, onToggle }: Props) => {
|
|||||||
{isConfigured && (
|
{isConfigured && (
|
||||||
<div className="absolute bottom-0 left-0 right-0 z-30 h-full">
|
<div className="absolute bottom-0 left-0 right-0 z-30 h-full">
|
||||||
<div className="relative h-full">
|
<div className="relative h-full">
|
||||||
<div className="absolute bottom-0 right-0 w-full flex-row items-center overflow-hidden whitespace-nowrap rounded-b-md bg-primary px-2 py-0.5 text-center text-xs text-black transition-all duration-300 group-hover:w-0 group-hover:p-0">
|
<div className="absolute bottom-0 right-0 w-full flex-row items-center overflow-hidden whitespace-nowrap rounded-b-md bg-primary px-2 py-0.5 text-center text-xs text-black">
|
||||||
Already Configured
|
Already Configured
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +125,7 @@ export const AddWorkflowIntegrationModal = ({ isOpen, onToggle }: Props) => {
|
|||||||
{wizardStep === WizardSteps.PlatformInputs &&
|
{wizardStep === WizardSteps.PlatformInputs &&
|
||||||
selectedPlatform === WorkflowIntegrationPlatform.SLACK && (
|
selectedPlatform === WorkflowIntegrationPlatform.SLACK && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="platform-inputs-step"
|
key="platform-inputs-step-slack"
|
||||||
transition={{ duration: 0.1 }}
|
transition={{ duration: 0.1 }}
|
||||||
initial={{ opacity: 0, translateX: 30 }}
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
@@ -138,7 +137,7 @@ export const AddWorkflowIntegrationModal = ({ isOpen, onToggle }: Props) => {
|
|||||||
{wizardStep === WizardSteps.PlatformInputs &&
|
{wizardStep === WizardSteps.PlatformInputs &&
|
||||||
selectedPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
selectedPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="platform-inputs-step"
|
key="platform-inputs-step-ms-teams"
|
||||||
transition={{ duration: 0.1 }}
|
transition={{ duration: 0.1 }}
|
||||||
initial={{ opacity: 0, translateX: 30 }}
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
|||||||
+4
@@ -2,6 +2,7 @@ import { Modal, ModalContent } from "@app/components/v2";
|
|||||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||||
|
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
@@ -23,6 +24,9 @@ export const EditWorkflowIntegrationModal = ({ isOpen, onClose, integration }: P
|
|||||||
{integration === WorkflowIntegrationPlatform.SLACK && (
|
{integration === WorkflowIntegrationPlatform.SLACK && (
|
||||||
<SlackIntegrationForm onClose={handleFormReset} />
|
<SlackIntegrationForm onClose={handleFormReset} />
|
||||||
)}
|
)}
|
||||||
|
{integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
||||||
|
<MicrosoftTeamsIntegrationForm onClose={handleFormReset} />
|
||||||
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
+4
-4
@@ -84,7 +84,7 @@ const formSchema = z
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type TSlackConfigForm = z.infer<typeof formSchema>;
|
type TMicrosoftTeamsConfigForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -109,7 +109,7 @@ export const MicrosoftTeamsIntegrationForm = ({ onClose }: Props) => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
formState: { isDirty, isSubmitting }
|
formState: { isDirty, isSubmitting }
|
||||||
} = useForm<TSlackConfigForm>({
|
} = useForm<TMicrosoftTeamsConfigForm>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
isAccessRequestNotificationEnabled: false,
|
isAccessRequestNotificationEnabled: false,
|
||||||
@@ -125,7 +125,7 @@ export const MicrosoftTeamsIntegrationForm = ({ onClose }: Props) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleIntegrationSave = async (data: TSlackConfigForm) => {
|
const handleIntegrationSave = async (data: TMicrosoftTeamsConfigForm) => {
|
||||||
try {
|
try {
|
||||||
if (!currentWorkspace) {
|
if (!currentWorkspace) {
|
||||||
return;
|
return;
|
||||||
@@ -374,7 +374,7 @@ export const MicrosoftTeamsIntegrationForm = ({ onClose }: Props) => {
|
|||||||
: [...(value || []), channel.channelId]
|
: [...(value || []), channel.channelId]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
key={`secret-requests-slack-channel-${channel.channelId}`}
|
key={`secret-requests-microsoft-teams-channel-${channel.channelId}`}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user