Compare commits

...

4 Commits

Author SHA1 Message Date
Nawaz Dhandala
268d13786f refactor: Define types for connectWithSlack and workspace display name functions in SlackIntegration and WorkspaceNotificationRuleTable 2026-02-12 19:04:36 +00:00
Nawaz Dhandala
79ab00bc29 refactor: Improve code formatting and readability across multiple files 2026-02-12 18:59:21 +00:00
Nawaz Dhandala
95a0ddc49f feat: Add migration for updating indexes and default values in OnCallDutyPolicyScheduleLayer 2026-02-12 18:58:49 +00:00
Nawaz Dhandala
9759d839d9 feat: Add workspaceProjectAuthTokenId for multi-workspace support
- Updated MicrosoftTeamsUtil and SlackUtil to include workspaceProjectAuthTokenId in various methods for better handling of multi-workspace scenarios.
- Modified WorkspaceUtil to utilize workspaceProjectAuthTokenId when fetching project auth tokens.
- Enhanced WorkspaceBase interface to support workspaceProjectAuthTokenId.
- Updated NotificationRuleWorkspaceChannel and WorkspaceMessagePayload to include workspaceProjectAuthTokenId.
- Adjusted SlackChannelCacheModal and SlackIntegration components to handle workspaceProjectAuthTokenId.
- Implemented changes in WorkspaceNotificationRulesTable to support workspaceProjectAuthTokenId selection.
- Added database migration to introduce workspaceProjectAuthTokenId and workspaceProjectId columns in relevant tables.
2026-02-12 18:57:15 +00:00
19 changed files with 1552 additions and 567 deletions

View File

@@ -321,6 +321,43 @@ class WorkspaceNotificationRule extends BaseModel {
})
public workspaceType?: WorkspaceType = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.EditWorkspaceNotificationRule,
],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Workspace Project Auth Token ID",
description:
"Workspace project auth token ID for this rule (used when multiple workspaces are connected)",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public workspaceProjectAuthTokenId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,

View File

@@ -24,6 +24,7 @@ export interface MiscData {
export interface SlackMiscData extends MiscData {
userId: string;
teamId?: string;
}
@TenantColumn("projectId")
@@ -153,6 +154,29 @@ class WorkspaceUserAuthToken extends BaseModel {
})
public workspaceType?: WorkspaceType = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
title: "Workspace Project ID",
description:
"Project ID in the Workspace (e.g., Slack team ID, Microsoft Teams team ID)",
required: false,
unique: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: true,
})
@Index()
public workspaceProjectId?: string = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],

View File

@@ -422,7 +422,25 @@ export default class MicrosoftTeamsAPI {
logger.debug("User Profile: ");
logger.debug(userProfile);
await WorkspaceUserAuthTokenService.refreshAuthToken({
const existingProjectAuth: WorkspaceProjectAuthToken | null =
await WorkspaceProjectAuthTokenService.getProjectAuth({
projectId: new ObjectID(projectId),
workspaceType: WorkspaceType.MicrosoftTeams,
});
const userAuthData: {
projectId: ObjectID;
userId: ObjectID;
workspaceType: WorkspaceType;
authToken: string;
workspaceUserId: string;
miscData: {
userId: string;
displayName?: string;
email?: string;
};
workspaceProjectId?: string;
} = {
projectId: new ObjectID(projectId),
userId: new ObjectID(userId),
workspaceType: WorkspaceType.MicrosoftTeams,
@@ -435,15 +453,16 @@ export default class MicrosoftTeamsAPI {
(userProfile["mail"] as string) ||
(userProfile["userPrincipalName"] as string),
},
});
};
if (existingProjectAuth?.workspaceProjectId) {
userAuthData.workspaceProjectId =
existingProjectAuth.workspaceProjectId;
}
await WorkspaceUserAuthTokenService.refreshAuthToken(userAuthData);
// Check if admin consent is already granted
const existingProjectAuth: WorkspaceProjectAuthToken | null =
await WorkspaceProjectAuthTokenService.getProjectAuth({
projectId: new ObjectID(projectId),
workspaceType: WorkspaceType.MicrosoftTeams,
});
if (
existingProjectAuth &&
(existingProjectAuth.miscData as any)?.adminConsentGranted
@@ -776,6 +795,22 @@ export default class MicrosoftTeamsAPI {
miscData: mergedMiscData,
});
await WorkspaceUserAuthTokenService.updateBy({
query: {
projectId: new ObjectID(projectId),
userId: new ObjectID(userId),
workspaceType: WorkspaceType.MicrosoftTeams,
},
data: {
workspaceProjectId: tenantId,
},
skip: 0,
limit: 1,
props: {
isRoot: true,
},
});
return Response.redirect(
req,
res,

View File

@@ -275,16 +275,39 @@ export default class SlackAPI {
},
});
await WorkspaceUserAuthTokenService.refreshAuthToken({
const userMiscData: {
userId: string;
teamId?: string;
} = {
userId: slackUserId || "",
};
if (slackTeamId) {
userMiscData.teamId = slackTeamId;
}
const userAuthData: {
projectId: ObjectID;
userId: ObjectID;
workspaceType: WorkspaceType;
authToken: string;
workspaceUserId: string;
miscData: typeof userMiscData;
workspaceProjectId?: string;
} = {
projectId: new ObjectID(projectId),
userId: new ObjectID(userId),
workspaceType: WorkspaceType.Slack,
authToken: slackUserAccessToken || "",
workspaceUserId: slackUserId || "",
miscData: {
userId: slackUserId || "",
},
});
miscData: userMiscData,
};
if (slackTeamId) {
userAuthData.workspaceProjectId = slackTeamId;
}
await WorkspaceUserAuthTokenService.refreshAuthToken(userAuthData);
// return back to dashboard after successful auth.
Response.redirect(req, res, slackIntegrationPageUrl);
@@ -441,15 +464,36 @@ export default class SlackAPI {
*/
/*
* check if the team id matches the project id.
* get project auth.
* check if the team id matches the project workspace.
* get project auth based on team id.
*/
const teamIdFromSlack: string | undefined =
idToken["https://slack.com/team_id"]?.toString();
// If state is provided, enforce workspace selection.
const expectedTeamId: string | undefined =
req.query["state"]?.toString();
if (expectedTeamId && teamIdFromSlack) {
if (expectedTeamId !== teamIdFromSlack) {
return Response.redirect(
req,
res,
slackIntegrationPageUrl.addQueryParam(
"error",
"Looks like you are trying to sign in to a different slack workspace. Please try again and sign in to the selected workspace.",
),
);
}
}
const projectAuth: WorkspaceProjectAuthToken | null =
await WorkspaceProjectAuthTokenService.findOneBy({
query: {
projectId: new ObjectID(projectId),
workspaceType: WorkspaceType.Slack,
workspaceProjectId: teamIdFromSlack,
},
select: {
workspaceProjectId: true,
@@ -460,19 +504,16 @@ export default class SlackAPI {
},
});
// cehck if the workspace project id is same as the team id.
// check if the workspace project id is same as the team id.
if (projectAuth) {
logger.debug("Project Auth: ");
logger.debug(projectAuth.workspaceProjectId);
logger.debug("Response Team ID: ");
logger.debug(idToken["https://slack.com/team_id"]);
logger.debug(teamIdFromSlack);
logger.debug("Response User ID: ");
logger.debug(idToken["https://slack.com/user_id"]);
if (
projectAuth.workspaceProjectId?.toString() !==
idToken["https://slack.com/team_id"]?.toString()
) {
if (projectAuth.workspaceProjectId?.toString() !== teamIdFromSlack) {
const teamName: string | undefined = (
projectAuth.miscData as SlackMiscData
)?.teamName;
@@ -495,7 +536,7 @@ export default class SlackAPI {
res,
slackIntegrationPageUrl.addQueryParam(
"error",
"Looks like this OneUptime project is not connected to any slack workspace. Please try again and sign in to the workspace",
"Looks like this OneUptime project is not connected to the selected slack workspace. Please connect the workspace first.",
),
);
}
@@ -524,7 +565,9 @@ export default class SlackAPI {
workspaceUserId: slackUserId || "",
miscData: {
userId: slackUserId || "",
teamId: teamIdFromSlack || "",
},
workspaceProjectId: teamIdFromSlack || "",
});
// return back to dashboard after successful auth.
@@ -714,12 +757,29 @@ export default class SlackAPI {
);
}
const workspaceProjectAuthTokenId: string | undefined =
req.query["workspaceProjectAuthTokenId"]?.toString();
// Get Slack project auth
const projectAuthQuery: {
projectId: ObjectID;
workspaceType: WorkspaceType;
workspaceProjectAuthTokenId?: ObjectID;
} = {
projectId: props.tenantId,
workspaceType: WorkspaceType.Slack,
};
if (workspaceProjectAuthTokenId) {
projectAuthQuery.workspaceProjectAuthTokenId = new ObjectID(
workspaceProjectAuthTokenId,
);
}
const projectAuth: WorkspaceProjectAuthToken | null =
await WorkspaceProjectAuthTokenService.getProjectAuth({
projectId: props.tenantId,
workspaceType: WorkspaceType.Slack,
});
await WorkspaceProjectAuthTokenService.getProjectAuth(
projectAuthQuery,
);
if (!projectAuth || !projectAuth.authToken) {
return Response.sendErrorResponse(
@@ -736,17 +796,28 @@ export default class SlackAPI {
let updatedProjectAuth: WorkspaceProjectAuthToken | null = projectAuth;
if (!(projectAuth.miscData as SlackMiscData)?.channelCache) {
await SlackUtil.getAllWorkspaceChannels({
const getChannelsData: {
authToken: string;
projectId: ObjectID;
workspaceProjectAuthTokenId?: ObjectID;
} = {
authToken: projectAuth.authToken,
projectId: props.tenantId,
});
};
if (workspaceProjectAuthTokenId) {
getChannelsData.workspaceProjectAuthTokenId = new ObjectID(
workspaceProjectAuthTokenId,
);
}
await SlackUtil.getAllWorkspaceChannels(getChannelsData);
// Re-fetch to return the latest cached object
updatedProjectAuth =
await WorkspaceProjectAuthTokenService.getProjectAuth({
projectId: props.tenantId,
workspaceType: WorkspaceType.Slack,
});
await WorkspaceProjectAuthTokenService.getProjectAuth(
projectAuthQuery,
);
}
const channelCache: {

View File

@@ -0,0 +1,78 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1770919024300 implements MigrationInterface {
public name = "MigrationName1770919024300";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "WorkspaceNotificationRule" ADD "workspaceProjectAuthTokenId" uuid`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceUserAuthToken" ADD "workspaceProjectId" character varying(500)`,
);
await queryRunner.query(
`WITH latest_auth AS (
SELECT DISTINCT ON ("projectId", "workspaceType")
"_id",
"projectId",
"workspaceType",
"workspaceProjectId"
FROM "WorkspaceProjectAuthToken"
ORDER BY "projectId", "workspaceType", "createdAt" DESC
)
UPDATE "WorkspaceNotificationRule" AS r
SET "workspaceProjectAuthTokenId" = latest_auth."_id"
FROM latest_auth
WHERE r."projectId" = latest_auth."projectId"
AND r."workspaceType" = latest_auth."workspaceType"`,
);
await queryRunner.query(
`WITH latest_auth AS (
SELECT DISTINCT ON ("projectId", "workspaceType")
"projectId",
"workspaceType",
"workspaceProjectId"
FROM "WorkspaceProjectAuthToken"
ORDER BY "projectId", "workspaceType", "createdAt" DESC
)
UPDATE "WorkspaceUserAuthToken" AS u
SET "workspaceProjectId" = latest_auth."workspaceProjectId"
FROM latest_auth
WHERE u."projectId" = latest_auth."projectId"
AND u."workspaceType" = latest_auth."workspaceType"`,
);
await queryRunner.query(
`DELETE FROM "WorkspaceNotificationRule" WHERE "workspaceProjectAuthTokenId" IS NULL`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceNotificationRule" ALTER COLUMN "workspaceProjectAuthTokenId" SET NOT NULL`,
);
await queryRunner.query(
`CREATE INDEX "IDX_workspace_notification_rule_workspace_project_auth_token_id" ON "WorkspaceNotificationRule" ("workspaceProjectAuthTokenId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_workspace_user_auth_token_workspace_project_id" ON "WorkspaceUserAuthToken" ("workspaceProjectId")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "IDX_workspace_user_auth_token_workspace_project_id"`,
);
await queryRunner.query(
`DROP INDEX "IDX_workspace_notification_rule_workspace_project_auth_token_id"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceNotificationRule" DROP COLUMN "workspaceProjectAuthTokenId"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceUserAuthToken" DROP COLUMN "workspaceProjectId"`,
);
}
}

View File

@@ -0,0 +1,47 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1770922660423 implements MigrationInterface {
public name = "MigrationName1770922660423";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "public"."IDX_workspace_user_auth_token_workspace_project_id"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_workspace_notification_rule_workspace_project_auth_token_id"`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
);
await queryRunner.query(
`CREATE INDEX "IDX_50f3ab2c779757f0f72733b9f5" ON "WorkspaceUserAuthToken" ("workspaceProjectId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_5691297e1b384944dea798b07a" ON "WorkspaceNotificationRule" ("workspaceProjectAuthTokenId") `,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "public"."IDX_5691297e1b384944dea798b07a"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_50f3ab2c779757f0f72733b9f5"`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
await queryRunner.query(
`CREATE INDEX "IDX_workspace_notification_rule_workspace_project_auth_token_id" ON "WorkspaceNotificationRule" ("workspaceProjectAuthTokenId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_workspace_user_auth_token_workspace_project_id" ON "WorkspaceUserAuthToken" ("workspaceProjectId") `,
);
}
}

View File

@@ -258,6 +258,8 @@ import { MigrationName1770728946893 } from "./1770728946893-MigrationName";
import { MigrationName1770732721195 } from "./1770732721195-MigrationName";
import { MigrationName1770833704656 } from "./1770833704656-MigrationName";
import { MigrationName1770834237090 } from "./1770834237090-MigrationName";
import { MigrationName1770919024300 } from "./1770919024300-MigrationName";
import { MigrationName1770922660423 } from "./1770922660423-MigrationName";
export default [
InitialMigration,
@@ -520,4 +522,6 @@ export default [
MigrationName1770732721195,
MigrationName1770833704656,
MigrationName1770834237090,
MigrationName1770919024300,
MigrationName1770922660423,
];

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,8 @@ export class Service extends DatabaseService<Model> {
public async getProjectAuth(data: {
projectId: ObjectID;
workspaceType: WorkspaceType;
workspaceProjectAuthTokenId?: ObjectID;
workspaceProjectId?: string;
}): Promise<Model | null> {
if (!data.projectId) {
throw new BadDataException("projectId is required");
@@ -26,12 +28,30 @@ export class Service extends DatabaseService<Model> {
throw new BadDataException("workspaceType is required");
}
const query: {
projectId: ObjectID;
workspaceType: WorkspaceType;
_id?: ObjectID;
workspaceProjectId?: string;
} = {
projectId: data.projectId,
workspaceType: data.workspaceType,
};
if (data.workspaceProjectAuthTokenId) {
query._id = data.workspaceProjectAuthTokenId;
}
if (data.workspaceProjectId) {
query.workspaceProjectId = data.workspaceProjectId;
}
return await this.findOneBy({
query: {
projectId: data.projectId,
workspaceType: data.workspaceType,
...query,
},
select: {
_id: true,
authToken: true,
workspaceProjectId: true,
miscData: true,
@@ -46,16 +66,29 @@ export class Service extends DatabaseService<Model> {
@CaptureSpan()
public async getProjectAuths(data: {
projectId: ObjectID;
workspaceType?: WorkspaceType;
}): Promise<Array<Model>> {
if (!data.projectId) {
throw new BadDataException("projectId is required");
}
const query: {
projectId: ObjectID;
workspaceType?: WorkspaceType;
} = {
projectId: data.projectId,
};
if (data.workspaceType) {
query.workspaceType = data.workspaceType;
}
return await this.findBy({
query: {
projectId: data.projectId,
...query,
},
select: {
_id: true,
authToken: true,
workspaceProjectId: true,
miscData: true,
@@ -84,6 +117,7 @@ export class Service extends DatabaseService<Model> {
authToken: string;
workspaceProjectId: string;
miscData: WorkspaceMiscData;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<void> {
if (!data.projectId) {
throw new BadDataException("projectId is required");
@@ -105,11 +139,26 @@ export class Service extends DatabaseService<Model> {
throw new BadDataException("miscData is required");
}
const query: {
projectId: ObjectID;
workspaceType: WorkspaceType;
workspaceProjectId?: string;
_id?: ObjectID;
} = {
projectId: data.projectId,
workspaceType: data.workspaceType,
};
if (data.workspaceProjectId) {
query.workspaceProjectId = data.workspaceProjectId;
}
if (data.workspaceProjectAuthTokenId) {
query._id = data.workspaceProjectAuthTokenId;
}
let projectAuth: Model | null = await this.findOneBy({
query: {
projectId: data.projectId,
workspaceType: data.workspaceType,
},
query: query,
select: {
_id: true,
},

View File

@@ -16,18 +16,31 @@ export class Service extends DatabaseService<Model> {
projectId: ObjectID;
userId: ObjectID;
workspaceType: WorkspaceType;
workspaceProjectId?: string;
}): Promise<Model | null> {
const query: {
userId: ObjectID;
projectId: ObjectID;
workspaceType: WorkspaceType;
workspaceProjectId?: string;
} = {
userId: data.userId,
projectId: data.projectId,
workspaceType: data.workspaceType,
};
if (data.workspaceProjectId) {
query.workspaceProjectId = data.workspaceProjectId;
}
return await this.findOneBy({
query: {
userId: data.userId,
projectId: data.projectId,
workspaceType: data.workspaceType,
},
query: query,
select: {
authToken: true,
workspaceUserId: true,
miscData: true,
workspaceType: true,
workspaceProjectId: true,
},
props: {
isRoot: true,
@@ -40,15 +53,27 @@ export class Service extends DatabaseService<Model> {
projectId: ObjectID;
userId: ObjectID;
workspaceType: WorkspaceType;
workspaceProjectId?: string;
}): Promise<boolean> {
const query: {
projectId: ObjectID;
userId: ObjectID;
workspaceType: WorkspaceType;
workspaceProjectId?: string;
} = {
projectId: data.projectId,
userId: data.userId,
workspaceType: data.workspaceType,
};
if (data.workspaceProjectId) {
query.workspaceProjectId = data.workspaceProjectId;
}
return (
(
await this.countBy({
query: {
projectId: data.projectId,
userId: data.userId,
workspaceType: data.workspaceType,
},
query: query,
skip: 0,
limit: 1,
props: {
@@ -67,13 +92,25 @@ export class Service extends DatabaseService<Model> {
authToken: string;
workspaceUserId: string;
miscData: SlackMiscData;
workspaceProjectId?: string;
}): Promise<void> {
const query: {
projectId: ObjectID;
userId: ObjectID;
workspaceType: WorkspaceType;
workspaceProjectId?: string;
} = {
projectId: data.projectId,
userId: data.userId,
workspaceType: data.workspaceType,
};
if (data.workspaceProjectId) {
query.workspaceProjectId = data.workspaceProjectId;
}
let userAuth: Model | null = await this.findOneBy({
query: {
projectId: data.projectId,
userId: data.userId,
workspaceType: data.workspaceType,
},
query: query,
select: {
_id: true,
},
@@ -92,6 +129,10 @@ export class Service extends DatabaseService<Model> {
userAuth.workspaceUserId = data.workspaceUserId;
userAuth.miscData = data.miscData;
if (data.workspaceProjectId !== undefined) {
userAuth.workspaceProjectId = data.workspaceProjectId;
}
await this.create({
data: userAuth,
props: {
@@ -105,6 +146,9 @@ export class Service extends DatabaseService<Model> {
authToken: data.authToken,
workspaceUserId: data.workspaceUserId,
miscData: data.miscData,
...(data.workspaceProjectId !== undefined && {
workspaceProjectId: data.workspaceProjectId,
}),
},
props: {
isRoot: true,

View File

@@ -892,6 +892,7 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
channelName: string;
projectId: ObjectID;
teamId: string;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<WorkspaceChannel> {
const channel: WorkspaceChannel | null =
await this.getWorkspaceChannelByName({
@@ -1547,6 +1548,7 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
authToken: string;
projectId: ObjectID;
teamId: string;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<Dictionary<WorkspaceChannel>> {
logger.debug("Getting all workspace channels for team ID: " + data.teamId);
@@ -1600,6 +1602,7 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
channelName: string;
projectId: ObjectID;
teamId?: string;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<boolean> {
if (!data.teamId) {
throw new BadDataException(

View File

@@ -435,6 +435,7 @@ export default class SlackUtil extends WorkspaceBase {
authToken: string;
channelNames: Array<string>;
projectId: ObjectID;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<Array<WorkspaceChannel>> {
logger.debug("Creating channels if they do not exist with data:");
logger.debug(data);
@@ -450,12 +451,24 @@ export default class SlackUtil extends WorkspaceBase {
channelName = channelName.replace(/\s+/g, "-");
// Check if channel exists using optimized method
const getChannelData: {
authToken: string;
channelName: string;
projectId: ObjectID;
workspaceProjectAuthTokenId?: ObjectID;
} = {
authToken: data.authToken,
channelName: channelName,
projectId: data.projectId,
};
if (data.workspaceProjectAuthTokenId) {
getChannelData.workspaceProjectAuthTokenId =
data.workspaceProjectAuthTokenId;
}
const existingChannel: WorkspaceChannel | null =
await this.getWorkspaceChannelByName({
authToken: data.authToken,
channelName: channelName,
projectId: data.projectId,
});
await this.getWorkspaceChannelByName(getChannelData);
if (existingChannel) {
logger.debug(`Channel ${channelName} already exists.`);
@@ -486,16 +499,29 @@ export default class SlackUtil extends WorkspaceBase {
authToken: string;
channelName: string;
projectId: ObjectID;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<WorkspaceChannel> {
logger.debug("Getting workspace channel ID from channel name with data:");
logger.debug(data);
const channelLookupData: {
authToken: string;
channelName: string;
projectId: ObjectID;
workspaceProjectAuthTokenId?: ObjectID;
} = {
authToken: data.authToken,
channelName: data.channelName,
projectId: data.projectId,
};
if (data.workspaceProjectAuthTokenId) {
channelLookupData.workspaceProjectAuthTokenId =
data.workspaceProjectAuthTokenId;
}
const channel: WorkspaceChannel | null =
await this.getWorkspaceChannelByName({
authToken: data.authToken,
channelName: data.channelName,
projectId: data.projectId,
});
await this.getWorkspaceChannelByName(channelLookupData);
if (!channel) {
logger.error("Channel not found.");
@@ -576,6 +602,7 @@ export default class SlackUtil extends WorkspaceBase {
public static override async getAllWorkspaceChannels(data: {
authToken: string;
projectId: ObjectID;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<Dictionary<WorkspaceChannel>> {
logger.debug("Getting all workspace channels with data:");
logger.debug(data);
@@ -660,10 +687,21 @@ export default class SlackUtil extends WorkspaceBase {
// Update cache in bulk
try {
await this.updateChannelsInCache({
const updateCacheData: {
projectId: ObjectID;
channelCache: Dictionary<WorkspaceChannel>;
workspaceProjectAuthTokenId?: ObjectID;
} = {
projectId: data.projectId,
channelCache: localChannelCache,
});
};
if (data.workspaceProjectAuthTokenId) {
updateCacheData.workspaceProjectAuthTokenId =
data.workspaceProjectAuthTokenId;
}
await this.updateChannelsInCache(updateCacheData);
} catch (error) {
logger.error("Error bulk updating channel cache:");
logger.error(error);
@@ -677,12 +715,24 @@ export default class SlackUtil extends WorkspaceBase {
private static async updateChannelsInCache(data: {
projectId: ObjectID;
channelCache: Dictionary<WorkspaceChannel>;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<void> {
const projectAuthQuery: {
projectId: ObjectID;
workspaceType: WorkspaceType;
workspaceProjectAuthTokenId?: ObjectID;
} = {
projectId: data.projectId,
workspaceType: WorkspaceType.Slack,
};
if (data.workspaceProjectAuthTokenId) {
projectAuthQuery.workspaceProjectAuthTokenId =
data.workspaceProjectAuthTokenId;
}
const projectAuth: any =
await WorkspaceProjectAuthTokenService.getProjectAuth({
projectId: data.projectId,
workspaceType: WorkspaceType.Slack,
});
await WorkspaceProjectAuthTokenService.getProjectAuth(projectAuthQuery);
if (!projectAuth) {
logger.debug("No project auth found, cannot update cache");
@@ -705,6 +755,9 @@ export default class SlackUtil extends WorkspaceBase {
authToken: projectAuth.authToken,
workspaceProjectId: projectAuth.workspaceProjectId,
miscData: miscData,
...(data.workspaceProjectAuthTokenId && {
workspaceProjectAuthTokenId: data.workspaceProjectAuthTokenId,
}),
});
logger.debug("Channel cache updated successfully");
@@ -714,15 +767,27 @@ export default class SlackUtil extends WorkspaceBase {
public static async getChannelFromCache(data: {
projectId: ObjectID;
channelName: string;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<WorkspaceChannel | null> {
logger.debug("Getting channel from cache with data:");
logger.debug(data);
const cacheAuthQuery: {
projectId: ObjectID;
workspaceType: WorkspaceType;
workspaceProjectAuthTokenId?: ObjectID;
} = {
projectId: data.projectId,
workspaceType: WorkspaceType.Slack,
};
if (data.workspaceProjectAuthTokenId) {
cacheAuthQuery.workspaceProjectAuthTokenId =
data.workspaceProjectAuthTokenId;
}
const projectAuth: any =
await WorkspaceProjectAuthTokenService.getProjectAuth({
projectId: data.projectId,
workspaceType: WorkspaceType.Slack,
});
await WorkspaceProjectAuthTokenService.getProjectAuth(cacheAuthQuery);
if (!projectAuth || !projectAuth.miscData) {
logger.debug("No project auth found or no misc data");
@@ -755,15 +820,29 @@ export default class SlackUtil extends WorkspaceBase {
projectId: ObjectID;
channelName: string;
channel: WorkspaceChannel;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<void> {
logger.debug("Updating channel cache with data:");
logger.debug(data);
const updateCacheAuthQuery: {
projectId: ObjectID;
workspaceType: WorkspaceType;
workspaceProjectAuthTokenId?: ObjectID;
} = {
projectId: data.projectId,
workspaceType: WorkspaceType.Slack,
};
if (data.workspaceProjectAuthTokenId) {
updateCacheAuthQuery.workspaceProjectAuthTokenId =
data.workspaceProjectAuthTokenId;
}
const projectAuth: any =
await WorkspaceProjectAuthTokenService.getProjectAuth({
projectId: data.projectId,
workspaceType: WorkspaceType.Slack,
});
await WorkspaceProjectAuthTokenService.getProjectAuth(
updateCacheAuthQuery,
);
if (!projectAuth) {
logger.debug("No project auth found, cannot update cache");
@@ -790,6 +869,9 @@ export default class SlackUtil extends WorkspaceBase {
authToken: projectAuth.authToken,
workspaceProjectId: projectAuth.workspaceProjectId,
miscData: miscData,
...(data.workspaceProjectAuthTokenId && {
workspaceProjectAuthTokenId: data.workspaceProjectAuthTokenId,
}),
});
logger.debug("Channel cache updated successfully");
@@ -800,6 +882,7 @@ export default class SlackUtil extends WorkspaceBase {
authToken: string;
channelName: string;
projectId: ObjectID;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<WorkspaceChannel | null> {
logger.debug("Getting workspace channel by name with data:");
logger.debug(data);
@@ -813,11 +896,22 @@ export default class SlackUtil extends WorkspaceBase {
// Try to get from cache first
try {
const cachedChannelLookup: {
projectId: ObjectID;
channelName: string;
workspaceProjectAuthTokenId?: ObjectID;
} = {
projectId: data.projectId,
channelName: normalizedChannelName,
};
if (data.workspaceProjectAuthTokenId) {
cachedChannelLookup.workspaceProjectAuthTokenId =
data.workspaceProjectAuthTokenId;
}
const cachedChannel: WorkspaceChannel | null =
await this.getChannelFromCache({
projectId: data.projectId,
channelName: normalizedChannelName,
});
await this.getChannelFromCache(cachedChannelLookup);
if (cachedChannel) {
logger.debug("Channel found in cache:");
logger.debug(cachedChannel);
@@ -909,10 +1003,21 @@ export default class SlackUtil extends WorkspaceBase {
// Update cache before returning
try {
await this.updateChannelsInCache({
const updateCacheData: {
projectId: ObjectID;
channelCache: Dictionary<WorkspaceChannel>;
workspaceProjectAuthTokenId?: ObjectID;
} = {
projectId: data.projectId,
channelCache: localChannelCache,
});
};
if (data.workspaceProjectAuthTokenId) {
updateCacheData.workspaceProjectAuthTokenId =
data.workspaceProjectAuthTokenId;
}
await this.updateChannelsInCache(updateCacheData);
} catch (error) {
logger.error("Error bulk updating channel cache:");
logger.error(error);
@@ -931,10 +1036,21 @@ export default class SlackUtil extends WorkspaceBase {
// Update cache even if channel not found
try {
await this.updateChannelsInCache({
const updateCacheData: {
projectId: ObjectID;
channelCache: Dictionary<WorkspaceChannel>;
workspaceProjectAuthTokenId?: ObjectID;
} = {
projectId: data.projectId,
channelCache: localChannelCache,
});
};
if (data.workspaceProjectAuthTokenId) {
updateCacheData.workspaceProjectAuthTokenId =
data.workspaceProjectAuthTokenId;
}
await this.updateChannelsInCache(updateCacheData);
} catch (error) {
logger.error("Error bulk updating channel cache:");
logger.error(error);
@@ -1013,6 +1129,7 @@ export default class SlackUtil extends WorkspaceBase {
authToken: string;
channelName: string;
projectId: ObjectID;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<boolean> {
// if channel name starts with #, remove it
if (data.channelName && data.channelName.startsWith("#")) {
@@ -1023,12 +1140,24 @@ export default class SlackUtil extends WorkspaceBase {
data.channelName = data.channelName.toLowerCase();
// Check if channel exists using optimized method
const channelLookup: {
authToken: string;
channelName: string;
projectId: ObjectID;
workspaceProjectAuthTokenId?: ObjectID;
} = {
authToken: data.authToken,
channelName: data.channelName,
projectId: data.projectId,
};
if (data.workspaceProjectAuthTokenId) {
channelLookup.workspaceProjectAuthTokenId =
data.workspaceProjectAuthTokenId;
}
const channel: WorkspaceChannel | null =
await this.getWorkspaceChannelByName({
authToken: data.authToken,
channelName: data.channelName,
projectId: data.projectId,
});
await this.getWorkspaceChannelByName(channelLookup);
return channel !== null;
}
@@ -1059,12 +1188,25 @@ export default class SlackUtil extends WorkspaceBase {
channelName = channelName.substring(1);
}
const channelLookup: {
authToken: string;
channelName: string;
projectId: ObjectID;
workspaceProjectAuthTokenId?: ObjectID;
} = {
authToken: data.authToken,
channelName: channelName,
projectId: data.projectId,
};
if (data.workspaceMessagePayload.workspaceProjectAuthTokenId) {
channelLookup.workspaceProjectAuthTokenId = new ObjectID(
data.workspaceMessagePayload.workspaceProjectAuthTokenId,
);
}
const channel: WorkspaceChannel | null =
await this.getWorkspaceChannelByName({
authToken: data.authToken,
channelName: channelName,
projectId: data.projectId,
});
await this.getWorkspaceChannelByName(channelLookup);
if (channel) {
workspaceChannelsToPostTo.push(channel);

View File

@@ -163,11 +163,27 @@ export default class WorkspaceUtil {
const responses: Array<WorkspaceSendMessageResponse> = [];
for (const messagePayloadByWorkspace of data.messagePayloadsByWorkspace) {
const workspaceProjectAuthTokenId: ObjectID | undefined =
messagePayloadByWorkspace.workspaceProjectAuthTokenId
? new ObjectID(messagePayloadByWorkspace.workspaceProjectAuthTokenId)
: undefined;
const projectAuthQuery: {
projectId: ObjectID;
workspaceType: WorkspaceType;
workspaceProjectAuthTokenId?: ObjectID;
} = {
projectId: data.projectId,
workspaceType: messagePayloadByWorkspace.workspaceType,
};
if (workspaceProjectAuthTokenId) {
projectAuthQuery.workspaceProjectAuthTokenId =
workspaceProjectAuthTokenId;
}
const projectAuthToken: WorkspaceProjectAuthToken | null =
await WorkspaceProjectAuthTokenService.getProjectAuth({
projectId: data.projectId,
workspaceType: messagePayloadByWorkspace.workspaceType,
});
await WorkspaceProjectAuthTokenService.getProjectAuth(projectAuthQuery);
if (!projectAuthToken) {
responses.push({

View File

@@ -43,6 +43,7 @@ export interface WorkspaceChannel {
name: string;
workspaceType: WorkspaceType;
teamId?: string; // Required for Microsoft Teams
workspaceProjectAuthTokenId?: string; // Optional: link to project auth token for multi-workspace support
}
export default class WorkspaceBase {
@@ -61,6 +62,7 @@ export default class WorkspaceBase {
channelName: string;
projectId: ObjectID;
teamId?: string;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<boolean> {
throw new NotImplementedException();
}
@@ -219,6 +221,7 @@ export default class WorkspaceBase {
public static async getAllWorkspaceChannels(_data: {
authToken: string;
projectId: ObjectID;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<Dictionary<WorkspaceChannel>> {
throw new NotImplementedException();
}
@@ -228,6 +231,7 @@ export default class WorkspaceBase {
authToken: string;
channelName: string;
projectId: ObjectID;
workspaceProjectAuthTokenId?: ObjectID;
}): Promise<WorkspaceChannel> {
throw new NotImplementedException();
}

View File

@@ -3,4 +3,5 @@ import { WorkspaceChannel } from "../../../Server/Utils/Workspace/WorkspaceBase"
export default interface NotificationRuleWorkspaceChannel
extends WorkspaceChannel {
notificationRuleId: string;
workspaceProjectAuthTokenId?: string;
}

View File

@@ -106,4 +106,5 @@ export default interface WorkspaceMessagePayload {
messageBlocks: Array<WorkspaceMessageBlock>; // Message to add to blocks.
workspaceType: WorkspaceType;
teamId?: string | undefined; // Team ID for Microsoft Teams
workspaceProjectAuthTokenId?: string | undefined; // Workspace project auth token to use (for multi-workspace support)
}

View File

@@ -40,6 +40,9 @@ const SlackChannelCacheModal: FunctionComponent<ComponentProps> = (
await API.get({
url: URL.fromString(
`${HOME_URL.toString()}/api/slack/get-all-channels`,
).addQueryParam(
"workspaceProjectAuthTokenId",
props.projectAuthTokenId.toString(),
),
headers: ModelAPI.getCommonHeaders(),
});

View File

@@ -30,6 +30,7 @@ import ListResult from "Common/Types/BaseDatabase/ListResult";
import WorkspaceUserAuthToken from "Common/Models/DatabaseModels/WorkspaceUserAuthToken";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import WorkspaceType from "Common/Types/Workspace/WorkspaceType";
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import SlackIntegrationDocumentation from "./SlackIntegrationDocumentation";
import Link from "Common/UI/Components/Link/Link";
import SlackChannelCacheModal from "./SlackChannelCacheModal";
@@ -47,18 +48,19 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
const [isLoading, setIsLoading] = React.useState<boolean>(true);
const [manifest, setManifest] = React.useState<JSONObject | null>(null);
const [isUserAccountConnected, setIsUserAccountConnected] =
React.useState<boolean>(false);
const [userAuthTokenId, setWorkspaceUserAuthTokenId] =
React.useState<ObjectID | null>(null);
const [projectAuthTokenId, setWorkspaceProjectAuthTokenId] =
React.useState<ObjectID | null>(null);
const [isProjectAccountConnected, setIsProjectAccountConnected] =
React.useState<boolean>(false);
const [projectAuthTokens, setProjectAuthTokens] = React.useState<
Array<WorkspaceProjectAuthToken>
>([]);
const [userAuthTokens, setUserAuthTokens] = React.useState<
Array<WorkspaceUserAuthToken>
>([]);
const [isButtonLoading, setIsButtonLoading] = React.useState<boolean>(false);
const [slackTeamName, setSlackTeamName] = React.useState<string | null>(null);
const [showChannelsModal, setShowChannelsModal] =
React.useState<boolean>(false);
const [selectedProjectAuthTokenId, setSelectedProjectAuthTokenId] =
React.useState<ObjectID | null>(null);
const isProjectAccountConnected: boolean = projectAuthTokens.length > 0;
useEffect(() => {
if (isProjectAccountConnected) {
@@ -73,75 +75,81 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
setError(null);
setIsLoading(true);
// check if the project is already connected with slack.
const projectId: ObjectID | null = ProjectUtil.getCurrentProjectId();
const userId: ObjectID | null = UserUtil.getUserId();
if (!projectId) {
setError(
<div>
Looks like you have not selected any project. Please select a
project to continue.
</div>,
);
return;
}
if (!userId) {
setError(
<div>
Looks like you are not logged in. Please login to continue.
</div>,
);
return;
}
const projectAuth: ListResult<WorkspaceProjectAuthToken> =
await ModelAPI.getList<WorkspaceProjectAuthToken>({
modelType: WorkspaceProjectAuthToken,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
projectId: projectId,
workspaceType: WorkspaceType.Slack,
},
select: {
_id: true,
miscData: true,
workspaceProjectId: true,
},
limit: 1,
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
createdAt: SortOrder.Descending,
createdAt: SortOrder.Ascending,
},
});
if (projectAuth.data.length > 0) {
setIsProjectAccountConnected(true);
const slackTeamName: string | undefined = (
projectAuth.data[0]!.miscData! as SlackMiscData
).teamName;
setWorkspaceProjectAuthTokenId(projectAuth.data[0]!.id);
setSlackTeamName(slackTeamName);
}
// fetch user auth token.
setProjectAuthTokens(projectAuth.data);
const userAuth: ListResult<WorkspaceUserAuthToken> =
await ModelAPI.getList<WorkspaceUserAuthToken>({
modelType: WorkspaceUserAuthToken,
query: {
userId: UserUtil.getUserId()!,
userId: userId,
projectId: projectId,
workspaceType: WorkspaceType.Slack,
},
select: {
_id: true,
workspaceProjectId: true,
miscData: true,
},
limit: 1,
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
createdAt: SortOrder.Descending,
},
});
if (userAuth.data.length > 0) {
setIsUserAccountConnected(true);
setWorkspaceUserAuthTokenId(userAuth.data[0]!.id);
setUserAuthTokens(userAuth.data);
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.get<JSONObject>({
url: URL.fromString(`${HOME_URL.toString()}/api/slack/app-manifest`),
});
if (response instanceof HTTPErrorResponse) {
throw response;
}
if (!isUserAccountConnected || !isProjectAccountConnected) {
// if any of this is not connected then fetch the app manifest, so we can connect with slack.
// fetch app manifest.
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.get<JSONObject>({
url: URL.fromString(
`${HOME_URL.toString()}/api/slack/app-manifest`,
),
});
if (response instanceof HTTPErrorResponse) {
throw response;
}
setManifest(response.data);
}
setManifest(response.data);
} catch (error) {
setError(<div>{API.getFriendlyErrorMessage(error as Error)}</div>);
} finally {
@@ -175,91 +183,57 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
return <ErrorMessage message={error} />;
}
let cardTitle: string = "";
let cardDescription: string = "";
let cardButtons: Array<CardButtonSchema> = [];
// if user and project both connected with slack, then.
if (isUserAccountConnected && isProjectAccountConnected) {
cardTitle = `You are connected with ${slackTeamName} team on Slack`;
cardDescription = `Your account is already connected with Slack.`;
cardButtons = [
{
title: `View Channels`,
isLoading: isButtonLoading,
buttonStyle: ButtonStyleType.NORMAL,
onClick: async () => {
try {
setError(null);
setShowChannelsModal(true);
} catch (error) {
setError(
<div>{API.getFriendlyErrorMessage(error as Exception)}</div>,
);
}
},
icon: IconProp.Slack,
},
{
title: `Disconnect`,
isLoading: isButtonLoading,
buttonStyle: ButtonStyleType.DANGER,
onClick: async () => {
try {
setIsButtonLoading(true);
setError(null);
if (userAuthTokenId) {
await ModelAPI.deleteItem({
modelType: WorkspaceUserAuthToken,
id: userAuthTokenId!,
});
setIsUserAccountConnected(false);
setWorkspaceUserAuthTokenId(null);
} else {
setError(
<div>
Looks like the user auth token id is not set properly. Please
try again.
</div>,
);
}
} catch (error) {
setError(
<div>{API.getFriendlyErrorMessage(error as Exception)}</div>,
);
}
setIsButtonLoading(false);
},
icon: IconProp.Close,
},
];
interface ConnectWithSlackData {
mode: "workspace" | "user";
expectedWorkspaceProjectId?: string;
}
const connectWithSlack: VoidFunction = (): void => {
if (SlackAppClientId) {
const projectId: ObjectID | null = ProjectUtil.getCurrentProjectId();
const userId: ObjectID | null = UserUtil.getUserId();
type ConnectWithSlack = (data: ConnectWithSlackData) => void;
if (!projectId) {
setError(
<div>
Looks like you have not selected any project. Please select a
project to continue.
</div>,
);
return;
}
const connectWithSlack: ConnectWithSlack = (
data: ConnectWithSlackData,
): void => {
if (!SlackAppClientId) {
setError(
<div>
Looks like the Slack App Client ID is not set in the environment
variables when you installed OneUptime. For more information, please
check this guide to set up Slack App properly:{" "}
<Link
to={new Route("/docs/self-hosted/slack-integration")}
openInNewTab={true}
>
Slack Integration
</Link>
</div>,
);
return;
}
if (!userId) {
setError(
<div>
Looks like you are not logged in. Please login to continue.
</div>,
);
return;
}
const projectId: ObjectID | null = ProjectUtil.getCurrentProjectId();
const userId: ObjectID | null = UserUtil.getUserId();
if (!projectId) {
setError(
<div>
Looks like you have not selected any project. Please select a project
to continue.
</div>,
);
return;
}
if (!userId) {
setError(
<div>Looks like you are not logged in. Please login to continue.</div>,
);
return;
}
const projectInstallRedirectUri: string = `${APP_API_URL}/slack/auth/${projectId.toString()}/${userId.toString()}`;
const userSigninRedirectUri: string = `${APP_API_URL}/slack/auth/${projectId.toString()}/${userId.toString()}/user`;
if (data.mode === "workspace") {
const userScopes: Array<string> = [];
if (
@@ -304,7 +278,6 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
);
}
// if any of the user or bot scopes length = = then error.
if (userScopes.length === 0 || botScopes.length === 0) {
setError(
<div>
@@ -321,75 +294,126 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
return;
}
const project_install_redirect_uri: string = `${APP_API_URL}/slack/auth/${projectId.toString()}/${userId.toString()}`;
const user_signin_redirect_uri: string = `${APP_API_URL}/slack/auth/${projectId.toString()}/${userId.toString()}/user`;
if (!isProjectAccountConnected) {
Navigation.navigate(
URL.fromString(
`https://slack.com/oauth/v2/authorize?scope=${botScopes.join(
",",
)}&user_scope=${userScopes.join(
",",
)}&client_id=${SlackAppClientId}&redirect_uri=${project_install_redirect_uri}`,
),
);
} else {
// if project account is not connected then we just need to sign in with slack and not install the app.
Navigation.navigate(
URL.fromString(
`https://slack.com/openid/connect/authorize?response_type=code&scope=openid%20profile%20email&client_id=${SlackAppClientId}&redirect_uri=${user_signin_redirect_uri}`,
),
);
}
} else {
setError(
<div>
Looks like the Slack App Client ID is not set in the environment
variables when you installed OneUptime. For more information, please
check this guide to set up Slack App properly:{" "}
<Link
to={new Route("/docs/self-hosted/slack-integration")}
openInNewTab={true}
>
Slack Integration
</Link>
</div>,
Navigation.navigate(
URL.fromString(
`https://slack.com/oauth/v2/authorize?scope=${botScopes.join(
",",
)}&user_scope=${userScopes.join(
",",
)}&client_id=${SlackAppClientId}&redirect_uri=${projectInstallRedirectUri}`,
),
);
return;
}
const stateParam: string | undefined = data.expectedWorkspaceProjectId;
const stateQuery: string = stateParam
? `&state=${encodeURIComponent(stateParam)}`
: "";
Navigation.navigate(
URL.fromString(
`https://slack.com/openid/connect/authorize?response_type=code&scope=openid%20profile%20email&client_id=${SlackAppClientId}&redirect_uri=${userSigninRedirectUri}${stateQuery}`,
),
);
};
type GetConnectWithSlackButtonFunction = (title: string) => CardButtonSchema;
type GetConnectWithSlackButtonFunction = (
title: string,
onClick: VoidFunction,
) => CardButtonSchema;
const getConnectWithSlackButton: GetConnectWithSlackButtonFunction = (
title: string,
onClick: VoidFunction,
): CardButtonSchema => {
return {
title: title || `Connect with Slack`,
buttonStyle: ButtonStyleType.PRIMARY,
onClick: () => {
return connectWithSlack();
return onClick();
},
icon: IconProp.Slack,
};
};
// if user is not connected and the project is connected with slack.
if (!isUserAccountConnected && isProjectAccountConnected) {
cardTitle = `You are disconnected from Slack (but OneUptime is already installed in ${slackTeamName} team)`;
cardDescription = `Connect your account with Slack to make the most out of OneUptime.`;
cardButtons = [
// connect with slack button.
getConnectWithSlackButton(`Connect my account with Slack`),
{
const userAuthByWorkspaceProjectId: Map<string, WorkspaceUserAuthToken> =
new Map();
userAuthTokens.forEach((token: WorkspaceUserAuthToken) => {
if (token.workspaceProjectId) {
userAuthByWorkspaceProjectId.set(token.workspaceProjectId, token);
}
});
const workspaceCards: Array<ReactElement> = projectAuthTokens.map(
(workspace: WorkspaceProjectAuthToken) => {
const teamName: string | undefined = (workspace.miscData as SlackMiscData)
?.teamName;
const workspaceProjectId: string | undefined =
workspace.workspaceProjectId;
const userAuth: WorkspaceUserAuthToken | undefined = workspaceProjectId
? userAuthByWorkspaceProjectId.get(workspaceProjectId)
: undefined;
const buttons: Array<CardButtonSchema> = [];
if (userAuth) {
buttons.push({
title: `Disconnect My Account`,
isLoading: isButtonLoading,
buttonStyle: ButtonStyleType.DANGER,
onClick: async () => {
try {
setIsButtonLoading(true);
setError(null);
await ModelAPI.deleteItem({
modelType: WorkspaceUserAuthToken,
id: userAuth.id!,
});
await loadItems();
} catch (error) {
setError(
<div>{API.getFriendlyErrorMessage(error as Exception)}</div>,
);
}
setIsButtonLoading(false);
},
icon: IconProp.Close,
});
} else {
buttons.push(
getConnectWithSlackButton(`Connect My Account`, () => {
const connectData: {
mode: "user" | "workspace";
expectedWorkspaceProjectId?: string;
} = {
mode: "user",
};
if (workspaceProjectId) {
connectData.expectedWorkspaceProjectId = workspaceProjectId;
}
return connectWithSlack(connectData);
}),
);
}
buttons.push({
title: `View Channels`,
isLoading: isButtonLoading,
buttonStyle: ButtonStyleType.NORMAL,
onClick: async () => {
try {
setError(null);
setShowChannelsModal(true);
if (workspace.id) {
setSelectedProjectAuthTokenId(workspace.id);
setShowChannelsModal(true);
}
} catch (error) {
setError(
<div>{API.getFriendlyErrorMessage(error as Exception)}</div>,
@@ -397,8 +421,9 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
}
},
icon: IconProp.Slack,
},
{
});
buttons.push({
title: `Uninstall OneUptime from Slack`,
isLoading: isButtonLoading,
buttonStyle: ButtonStyleType.DANGER,
@@ -406,21 +431,12 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
try {
setIsButtonLoading(true);
setError(null);
if (projectAuthTokenId) {
if (workspace.id) {
await ModelAPI.deleteItem({
modelType: WorkspaceProjectAuthToken,
id: projectAuthTokenId!,
id: workspace.id,
});
setIsProjectAccountConnected(false);
setWorkspaceProjectAuthTokenId(null);
} else {
setError(
<div>
Looks like the user auth token id is not set properly. Please
try again.
</div>,
);
await loadItems();
}
} catch (error) {
setError(
@@ -430,15 +446,47 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
setIsButtonLoading(false);
},
icon: IconProp.Trash,
},
];
}
});
if (!isProjectAccountConnected) {
cardTitle = `Connect with Slack`;
cardDescription = `Connect your account with Slack to make the most out of OneUptime.`;
cardButtons = [getConnectWithSlackButton(`Connect with Slack`)];
}
return (
<Card
key={workspace.id?.toString()}
title={`Slack Workspace: ${teamName || workspaceProjectId || "Slack"}`}
description={
userAuth
? "Your account is connected to this Slack workspace."
: "Connect your account to enable personalized Slack actions."
}
buttons={buttons}
/>
);
},
);
const connectWorkspaceCard: ReactElement = (
<Card
title={
isProjectAccountConnected
? "Connect Another Slack Workspace"
: "Connect with Slack"
}
description={
isProjectAccountConnected
? "Install OneUptime in another Slack workspace."
: "Install OneUptime in your Slack workspace."
}
buttons={[
getConnectWithSlackButton(
isProjectAccountConnected
? "Connect Another Workspace"
: "Connect with Slack",
() => {
return connectWithSlack({ mode: "workspace" });
},
),
]}
/>
);
if (!SlackAppClientId) {
return <SlackIntegrationDocumentation manifest={manifest as JSONObject} />;
@@ -446,18 +494,16 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
return (
<Fragment>
<div>
<Card
title={cardTitle}
description={cardDescription}
buttons={cardButtons}
/>
<div className="space-y-4">
{connectWorkspaceCard}
{workspaceCards}
</div>
{showChannelsModal && projectAuthTokenId ? (
{showChannelsModal && selectedProjectAuthTokenId ? (
<SlackChannelCacheModal
projectAuthTokenId={projectAuthTokenId}
projectAuthTokenId={selectedProjectAuthTokenId}
onClose={() => {
return setShowChannelsModal(false);
setShowChannelsModal(false);
setSelectedProjectAuthTokenId(null);
}}
/>
) : null}

View File

@@ -54,7 +54,11 @@ import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import URL from "Common/Types/API/URL";
import { APP_API_URL } from "Common/UI/Config";
import { JSONObject } from "Common/Types/JSON";
import { MicrosoftTeamsTeam } from "Common/Models/DatabaseModels/WorkspaceProjectAuthToken";
import WorkspaceProjectAuthToken, {
MicrosoftTeamsMiscData,
MicrosoftTeamsTeam,
SlackMiscData,
} from "Common/Models/DatabaseModels/WorkspaceProjectAuthToken";
export interface ComponentProps {
workspaceType: WorkspaceType;
eventType: NotificationRuleEventType;
@@ -87,6 +91,8 @@ const WorkspaceNotificationRuleTable: FunctionComponent<ComponentProps> = (
const [microsoftTeamsTeams, setMicrosoftTeams] = React.useState<
Array<MicrosoftTeamsTeam>
>([]);
const [workspaceProjectAuthTokens, setWorkspaceProjectAuthTokens] =
React.useState<Array<WorkspaceProjectAuthToken>>([]);
const [users, setUsers] = React.useState<Array<User>>([]);
const [showTestModal, setShowTestModal] = React.useState<boolean>(false);
@@ -372,6 +378,27 @@ const WorkspaceNotificationRuleTable: FunctionComponent<ComponentProps> = (
setMicrosoftTeams(teamsData);
}
}
const workspaceAuths: ListResult<WorkspaceProjectAuthToken> =
await ModelAPI.getList<WorkspaceProjectAuthToken>({
modelType: WorkspaceProjectAuthToken,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
workspaceType: props.workspaceType,
},
select: {
_id: true,
miscData: true,
workspaceProjectId: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
createdAt: SortOrder.Ascending,
},
});
setWorkspaceProjectAuthTokens(workspaceAuths.data);
} catch (err) {
setError(API.getFriendlyErrorMessage(err as Exception));
}
@@ -392,6 +419,70 @@ const WorkspaceNotificationRuleTable: FunctionComponent<ComponentProps> = (
return <ErrorMessage message={error} />;
}
type GetWorkspaceDisplayName = (
workspace: WorkspaceProjectAuthToken,
) => string;
const getWorkspaceDisplayName: GetWorkspaceDisplayName = (
workspace: WorkspaceProjectAuthToken,
): string => {
if (props.workspaceType === WorkspaceType.Slack) {
const teamName: string | undefined = (workspace.miscData as SlackMiscData)
?.teamName;
return teamName || workspace.workspaceProjectId || "Slack Workspace";
}
if (props.workspaceType === WorkspaceType.MicrosoftTeams) {
const teamName: string | undefined = (
workspace.miscData as MicrosoftTeamsMiscData
)?.teamName;
return (
teamName ||
workspace.workspaceProjectId ||
`${getWorkspaceTypeDisplayName(props.workspaceType)} Workspace`
);
}
return (
workspace.workspaceProjectId ||
`${getWorkspaceTypeDisplayName(props.workspaceType)} Workspace`
);
};
const workspaceOptions: Array<{ label: string; value: string }> =
workspaceProjectAuthTokens.map((workspace: WorkspaceProjectAuthToken) => {
return {
label: getWorkspaceDisplayName(workspace),
value: workspace.id?.toString() || "",
};
});
const defaultWorkspaceAuthTokenId: string | undefined =
workspaceProjectAuthTokens.length === 1
? workspaceProjectAuthTokens[0]?.id?.toString()
: undefined;
type GetWorkspaceNameById = (id?: string) => string;
const getWorkspaceNameById: GetWorkspaceNameById = (id?: string): string => {
if (!id) {
return "-";
}
const match: WorkspaceProjectAuthToken | undefined =
workspaceProjectAuthTokens.find(
(workspace: WorkspaceProjectAuthToken) => {
return workspace.id?.toString() === id.toString();
},
);
if (!match) {
return "-";
}
return getWorkspaceDisplayName(match);
};
type RemoveFilterWithNoValues = (
notificationRule: IncidentNotificationRule,
) => IncidentNotificationRule;
@@ -473,18 +564,48 @@ const WorkspaceNotificationRuleTable: FunctionComponent<ComponentProps> = (
values.eventType = props.eventType;
values.projectId = ProjectUtil.getCurrentProjectId()!;
values.workspaceType = props.workspaceType;
if (
!values.workspaceProjectAuthTokenId &&
defaultWorkspaceAuthTokenId
) {
values.workspaceProjectAuthTokenId = new ObjectID(
defaultWorkspaceAuthTokenId,
);
}
values.notificationRule = removeFiltersWithNoValues(
values.notificationRule as IncidentNotificationRule,
);
return Promise.resolve(values);
}}
onBeforeEdit={(values: WorkspaceNotificationRule) => {
if (
!values.workspaceProjectAuthTokenId &&
defaultWorkspaceAuthTokenId
) {
values.workspaceProjectAuthTokenId = new ObjectID(
defaultWorkspaceAuthTokenId,
);
}
values.notificationRule = removeFiltersWithNoValues(
values.notificationRule as IncidentNotificationRule,
);
return Promise.resolve(values);
}}
formFields={[
{
field: {
workspaceProjectAuthTokenId: true,
},
title: `${getWorkspaceTypeDisplayName(props.workspaceType)} Workspace`,
description: `Select the ${getWorkspaceTypeDisplayName(props.workspaceType)} workspace where this rule should post notifications.`,
fieldType: FormFieldSchemaType.Dropdown,
required: true,
stepId: "basic",
showIf: () => {
return workspaceProjectAuthTokens.length > 1;
},
dropdownOptions: workspaceOptions,
},
{
field: {
name: true,
@@ -599,6 +720,28 @@ const WorkspaceNotificationRuleTable: FunctionComponent<ComponentProps> = (
title: "Rule Description",
type: FieldType.Text,
},
...(workspaceProjectAuthTokens.length > 1
? [
{
field: {
workspaceProjectAuthTokenId: true,
},
title: `${getWorkspaceTypeDisplayName(props.workspaceType)} Workspace`,
type: FieldType.Element,
getElement: (
value: WorkspaceNotificationRule,
): ReactElement => {
return (
<Fragment>
{getWorkspaceNameById(
value.workspaceProjectAuthTokenId?.toString(),
)}
</Fragment>
);
},
},
]
: []),
{
field: {
notificationRule: true,