mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
119 Commits
master
...
teams-inte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b976805076 | ||
|
|
037f2d5dea | ||
|
|
7bdf84ab12 | ||
|
|
d91c3db24d | ||
|
|
fe7eb1c45a | ||
|
|
1b0bd34279 | ||
|
|
2bd8e8782a | ||
|
|
7c0ba6566e | ||
|
|
a7bf69269f | ||
|
|
4aaa059793 | ||
|
|
be040a964f | ||
|
|
d966e19751 | ||
|
|
be691cd7f8 | ||
|
|
9bd00b4e6a | ||
|
|
0f127f2f10 | ||
|
|
e2c070ec63 | ||
|
|
0bbf82c7a4 | ||
|
|
015f22c01a | ||
|
|
9c05035f85 | ||
|
|
03bc9136a7 | ||
|
|
e7948e0f11 | ||
|
|
6b436cd51a | ||
|
|
4e93e9a4e1 | ||
|
|
bb199506e1 | ||
|
|
87be845a8c | ||
|
|
0c35a704ab | ||
|
|
48af4d346f | ||
|
|
af0ea621f7 | ||
|
|
8a736d6853 | ||
|
|
c8f0c540a3 | ||
|
|
c4ebf42e62 | ||
|
|
5f3d84e44c | ||
|
|
1e34c33006 | ||
|
|
43dbe62a9a | ||
|
|
ba650466f0 | ||
|
|
290a46ab81 | ||
|
|
83cececd3c | ||
|
|
21edefb6da | ||
|
|
099e00e907 | ||
|
|
c916e2b596 | ||
|
|
68ad1babf9 | ||
|
|
f40c2f4628 | ||
|
|
e6fe8c436a | ||
|
|
38ba91e411 | ||
|
|
92505cc3c2 | ||
|
|
33d671ded7 | ||
|
|
e10ad8fbe3 | ||
|
|
b793a65286 | ||
|
|
0c073dd5cb | ||
|
|
536c59e5bd | ||
|
|
fc85a9451e | ||
|
|
54a1a56a83 | ||
|
|
ee2acf6ecf | ||
|
|
9552063e89 | ||
|
|
bdeac99f5b | ||
|
|
a4ae41010e | ||
|
|
4af5a99519 | ||
|
|
ff19e4069a | ||
|
|
0600cd4b69 | ||
|
|
41965c7c56 | ||
|
|
fa19ff280f | ||
|
|
f3bba038ca | ||
|
|
c9783f803f | ||
|
|
ff5583839a | ||
|
|
40f6717033 | ||
|
|
c485d1beb1 | ||
|
|
45f955e73d | ||
|
|
6ee9917e60 | ||
|
|
4b0b3182c2 | ||
|
|
bad1b899ad | ||
|
|
b12dbab76f | ||
|
|
ec90a58602 | ||
|
|
08d591211d | ||
|
|
10e8f0530f | ||
|
|
b870de33fa | ||
|
|
d28e6dfece | ||
|
|
8d27414c08 | ||
|
|
ce84cbd93e | ||
|
|
048661b8ae | ||
|
|
a041e8eb2d | ||
|
|
fc98103519 | ||
|
|
a8f4893403 | ||
|
|
58979ec3e1 | ||
|
|
09d1d52544 | ||
|
|
3d9d2ab6d1 | ||
|
|
f9332fb678 | ||
|
|
60f267ee94 | ||
|
|
108545e590 | ||
|
|
64eb4e47bd | ||
|
|
ff1b192ef6 | ||
|
|
0819d0b565 | ||
|
|
66d154fad7 | ||
|
|
49ecf62ecb | ||
|
|
17457bbf3c | ||
|
|
dd8692b97c | ||
|
|
b9d22bc6c7 | ||
|
|
2c05f36853 | ||
|
|
42404ab02e | ||
|
|
e8355a9f08 | ||
|
|
7c5388e078 | ||
|
|
3d2eedec82 | ||
|
|
c052d79949 | ||
|
|
5326bbe3be | ||
|
|
9df7e58127 | ||
|
|
1f0f92b133 | ||
|
|
9bb6c2d8bf | ||
|
|
32c93575b1 | ||
|
|
fb8dca8d1c | ||
|
|
4707e16318 | ||
|
|
eadb24b7c2 | ||
|
|
dc22bececf | ||
|
|
768281d1f4 | ||
|
|
3a994e98d7 | ||
|
|
2a41dfe5bb | ||
|
|
4fc697d552 | ||
|
|
01e619a283 | ||
|
|
16e049a564 | ||
|
|
258870fe2e | ||
|
|
7341b6170a |
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"watch": ["./","../Common/UI", "../Common/Types", "../Common/Utils", "../Common/Models"],
|
||||
"watch": [
|
||||
"./",
|
||||
"../Common/UI",
|
||||
"../Common/Types",
|
||||
"../Common/Utils",
|
||||
"../Common/Models"
|
||||
],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"ignore": [
|
||||
"./public/**",
|
||||
@@ -10,5 +16,7 @@
|
||||
"./build/dist/**",
|
||||
"../Common/Server/**"
|
||||
],
|
||||
"exec": "npm run dev-build && npm run start"
|
||||
"exec": "sh -c 'npm run dev-build && node esbuild.config.js --watch & npm run start'",
|
||||
"delay": "200ms",
|
||||
"legacyWatch": true
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"Common": "file:../Common",
|
||||
"Common": "link:../Common",
|
||||
|
||||
"ejs": "^3.1.10",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"watch": ["./","../Common/UI", "../Common/Types", "../Common/Utils", "../Common/Models"],
|
||||
"watch": [
|
||||
"./",
|
||||
"../Common/UI",
|
||||
"../Common/Types",
|
||||
"../Common/Utils",
|
||||
"../Common/Models"
|
||||
],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"ignore": [
|
||||
"./public/**",
|
||||
@@ -10,5 +16,7 @@
|
||||
"./build/dist/**",
|
||||
"../Common/Server/**"
|
||||
],
|
||||
"exec": " npm run dev-build && npm run start"
|
||||
"exec": "sh -c 'npm run dev-build && node esbuild.config.js --watch & npm run start'",
|
||||
"delay": "200ms",
|
||||
"legacyWatch": true
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": false,
|
||||
"dependencies": {
|
||||
"Common": "file:../Common",
|
||||
"Common": "link:../Common",
|
||||
|
||||
"ejs": "^3.1.10",
|
||||
"react": "^18.3.1",
|
||||
@@ -11,7 +11,7 @@
|
||||
"react-router-dom": "^6.23.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev-build": "NODE_ENV=development node esbuild.config.js",
|
||||
"dev-build": "npm run generate-sw",
|
||||
"dev": "npx nodemon",
|
||||
"build": "NODE_ENV=production node esbuild.config.js",
|
||||
"analyze": "analyze=true NODE_ENV=production node esbuild.config.js",
|
||||
|
||||
@@ -521,6 +521,7 @@ import ScheduledMaintenanceFeedService, {
|
||||
} from "Common/Server/Services/ScheduledMaintenanceFeedService";
|
||||
|
||||
import SlackAPI from "Common/Server/API/SlackAPI";
|
||||
import MicrosoftTeamsAPI from "Common/Server/API/MicrosoftTeamsAPI";
|
||||
|
||||
import WorkspaceProjectAuthToken from "Common/Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import WorkspaceProjectAuthTokenService, {
|
||||
@@ -1619,6 +1620,12 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
new ResellerPlanAPI().getRouter(),
|
||||
);
|
||||
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new SlackAPI().getRouter());
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new MicrosoftTeamsAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new GlobalConfigAPI().getRouter(),
|
||||
|
||||
@@ -34,6 +34,23 @@ export interface SlackMiscData extends MiscData {
|
||||
};
|
||||
}
|
||||
|
||||
export interface MicrosoftTeamsMiscData extends MiscData {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
tenantId: string;
|
||||
// Below fields are optional and used for refreshing access tokens.
|
||||
refreshToken?: string; // Microsoft OAuth refresh token
|
||||
tokenExpiresAt?: string; // ISO date string of when current access token expires
|
||||
// App-only (client credentials) token caching per project/tenant
|
||||
appAccessToken?: string; // Microsoft Graph application access token
|
||||
appAccessTokenExpiresAt?: string; // ISO expiry for app access token
|
||||
lastAppTokenIssuedAt?: string; // ISO time when the current app token was minted
|
||||
// Admin consent status for application permissions
|
||||
adminConsentGranted?: boolean; // Whether admin has granted consent for application permissions
|
||||
adminConsentGrantedAt?: string; // ISO timestamp when admin consent was granted
|
||||
adminConsentGrantedBy?: string; // User ID who granted admin consent
|
||||
}
|
||||
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
create: [
|
||||
|
||||
@@ -109,7 +109,7 @@ class WorkspaceSetting extends BaseModel {
|
||||
unique: false,
|
||||
nullable: false,
|
||||
})
|
||||
public settings?: SlackSettings = undefined;
|
||||
public settings?: Settings = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
|
||||
651
Common/Server/API/MicrosoftTeamsAPI.ts
Normal file
651
Common/Server/API/MicrosoftTeamsAPI.ts
Normal file
@@ -0,0 +1,651 @@
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import BadRequestException from "../../Types/Exception/BadRequestException";
|
||||
import URL from "../../Types/API/URL";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import API from "../../Utils/API";
|
||||
import HTTPErrorResponse from "../../Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "../../Types/API/HTTPResponse";
|
||||
import {
|
||||
AppApiClientUrl,
|
||||
DashboardClientUrl,
|
||||
MicrosoftTeamsAppClientId,
|
||||
MicrosoftTeamsAppClientSecret,
|
||||
} from "../EnvironmentConfig";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import WorkspaceProjectAuthTokenService from "../Services/WorkspaceProjectAuthTokenService";
|
||||
import WorkspaceUserAuthTokenService from "../Services/WorkspaceUserAuthTokenService";
|
||||
import WorkspaceProjectAuthToken from "../../Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
import logger from "../Utils/Logger";
|
||||
|
||||
export default class MicrosoftTeamsAPI {
|
||||
public getRouter(): ExpressRouter {
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
// Admin consent endpoint for application permissions
|
||||
router.get(
|
||||
"/teams/admin-consent",
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
if (!MicrosoftTeamsAppClientId || !MicrosoftTeamsAppClientSecret) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"Microsoft Teams App Client credentials are not set",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Extract project_id and user_id from state parameter
|
||||
const stateParam: string | undefined = req.query["state"]?.toString();
|
||||
if (!stateParam) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Missing state parameter"),
|
||||
);
|
||||
}
|
||||
|
||||
let projectIdStr: string;
|
||||
let userIdStr: string;
|
||||
|
||||
try {
|
||||
const stateData: JSONObject = JSON.parse(
|
||||
Buffer.from(stateParam, "base64").toString(),
|
||||
);
|
||||
projectIdStr = stateData["projectId"] as string;
|
||||
userIdStr = stateData["userId"] as string;
|
||||
|
||||
if (!stateData?.['projectId']) {
|
||||
throw new BadDataException("Invalid state data");
|
||||
}
|
||||
|
||||
} catch {
|
||||
// Error is intentionally ignored
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException("Please try again."),
|
||||
);
|
||||
}
|
||||
|
||||
const error: string | undefined = req.query["error"]?.toString();
|
||||
const adminConsent: string | undefined =
|
||||
req.query["admin_consent"]?.toString();
|
||||
const tenantId: string | undefined = req.query["tenant"]?.toString();
|
||||
|
||||
const settingsUrl: URL = URL.fromString(
|
||||
`${DashboardClientUrl.toString()}/${projectIdStr.toString()}/settings/microsoft-teams-integration`,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
settingsUrl.addQueryParam("error", error),
|
||||
);
|
||||
}
|
||||
|
||||
if (adminConsent === "True" && tenantId) {
|
||||
// Admin consent was granted successfully
|
||||
logger.debug(
|
||||
`Admin consent granted for tenant ${tenantId} by user ${userIdStr}`,
|
||||
);
|
||||
|
||||
// Update or create the project auth token with admin consent status
|
||||
try {
|
||||
const existingAuth: WorkspaceProjectAuthToken | null =
|
||||
await WorkspaceProjectAuthTokenService.findOneBy({
|
||||
query: {
|
||||
projectId: new ObjectID(projectIdStr),
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
miscData: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const currentMiscData: JSONObject =
|
||||
(existingAuth?.miscData as JSONObject) || {};
|
||||
const updatedMiscData: JSONObject = {
|
||||
...currentMiscData,
|
||||
tenantId: tenantId,
|
||||
adminConsentGranted: true,
|
||||
adminConsentGrantedAt: new Date().toISOString(),
|
||||
adminConsentGrantedBy: userIdStr,
|
||||
teamName: currentMiscData["teamName"] || "Microsoft Teams",
|
||||
teamId: currentMiscData["teamId"] || tenantId,
|
||||
};
|
||||
|
||||
if (existingAuth) {
|
||||
// Update existing auth token
|
||||
await WorkspaceProjectAuthTokenService.updateOneById({
|
||||
id: existingAuth.id!,
|
||||
data: {
|
||||
miscData: updatedMiscData as any,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Create new project auth token with admin consent
|
||||
const newAuthToken: WorkspaceProjectAuthToken =
|
||||
new WorkspaceProjectAuthToken();
|
||||
newAuthToken.projectId = new ObjectID(projectIdStr);
|
||||
newAuthToken.workspaceType = WorkspaceType.MicrosoftTeams;
|
||||
newAuthToken.authToken = `admin-consent-${tenantId}-${Date.now()}`; // Placeholder token
|
||||
newAuthToken.workspaceProjectId = tenantId;
|
||||
newAuthToken.miscData = updatedMiscData as any;
|
||||
|
||||
await WorkspaceProjectAuthTokenService.create({
|
||||
data: newAuthToken,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
settingsUrl.addQueryParam("admin_consent", "granted"),
|
||||
);
|
||||
} catch (updateError) {
|
||||
logger.error("Error updating admin consent status:");
|
||||
logger.error(updateError);
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
settingsUrl.addQueryParam("error", "consent_update_failed"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Admin consent was denied or failed
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
settingsUrl.addQueryParam("error", "admin_consent_denied"),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// OAuth redirect for project install (admin installs app in a team)
|
||||
router.get(
|
||||
"/teams/auth",
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
if (!MicrosoftTeamsAppClientId || !MicrosoftTeamsAppClientSecret) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"Microsoft Teams App Client credentials are not set",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Extract project_id and user_id from state parameter
|
||||
const stateParam: string | undefined = req.query["state"]?.toString();
|
||||
if (!stateParam) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Missing state parameter"),
|
||||
);
|
||||
}
|
||||
|
||||
let projectIdStr: string;
|
||||
let userIdStr: string;
|
||||
let authType: string;
|
||||
|
||||
try {
|
||||
const stateData: JSONObject = JSON.parse(
|
||||
Buffer.from(stateParam, "base64").toString(),
|
||||
);
|
||||
projectIdStr = stateData["projectId"] as string;
|
||||
userIdStr = stateData["userId"] as string;
|
||||
authType = stateData["authType"] as string;
|
||||
|
||||
if (!projectIdStr || !userIdStr || !authType) {
|
||||
throw new Error("Invalid state data");
|
||||
}
|
||||
} catch {
|
||||
// Error is intentionally ignored
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid state parameter"),
|
||||
);
|
||||
}
|
||||
|
||||
const error: string | undefined = req.query["error"]?.toString();
|
||||
const code: string | undefined = req.query["code"]?.toString();
|
||||
|
||||
const settingsUrl: URL = URL.fromString(
|
||||
`${DashboardClientUrl.toString()}/${projectIdStr.toString()}/settings/microsoft-teams-integration`,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
settingsUrl.addQueryParam("error", error),
|
||||
);
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Missing code"),
|
||||
);
|
||||
}
|
||||
|
||||
const redirectUri: URL = URL.fromString(
|
||||
`${AppApiClientUrl.toString()}/teams/auth`,
|
||||
);
|
||||
|
||||
// Exchange code for tokens - use 'common' endpoint for multi-tenant support
|
||||
const tokenResp: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post(
|
||||
URL.fromString(
|
||||
`https://login.microsoftonline.com/common/oauth2/v2.0/token`,
|
||||
),
|
||||
{
|
||||
client_id: MicrosoftTeamsAppClientId,
|
||||
client_secret: MicrosoftTeamsAppClientSecret,
|
||||
grant_type: "authorization_code",
|
||||
code: code,
|
||||
redirect_uri: redirectUri.toString(),
|
||||
},
|
||||
{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
);
|
||||
|
||||
if (tokenResp instanceof HTTPErrorResponse) {
|
||||
logger.error(tokenResp.jsonData);
|
||||
|
||||
const errorMessage: string =
|
||||
"Error from Microsoft: " + tokenResp.message;
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(errorMessage),
|
||||
);
|
||||
}
|
||||
|
||||
// Example response fields: access_token, id_token, refresh_token
|
||||
const accessToken: string | undefined = (
|
||||
tokenResp.jsonData as JSONObject
|
||||
)["access_token"] as string;
|
||||
|
||||
const refreshToken: string | undefined = (
|
||||
tokenResp.jsonData as JSONObject
|
||||
)["refresh_token"] as string;
|
||||
|
||||
const expiresIn: number | undefined = (
|
||||
tokenResp.jsonData as JSONObject
|
||||
)["expires_in"] as number; // seconds
|
||||
|
||||
const idToken: string | undefined = (tokenResp.jsonData as JSONObject)[
|
||||
"id_token"
|
||||
] as string;
|
||||
|
||||
if (!accessToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("No access token from Microsoft"),
|
||||
);
|
||||
}
|
||||
|
||||
// Extract tenant information from the ID token
|
||||
let tenantId: string = "common";
|
||||
let tenantName: string = "Microsoft Teams";
|
||||
let teamId: string = tenantId;
|
||||
|
||||
if (idToken) {
|
||||
try {
|
||||
// Decode JWT payload (second part after splitting by '.')
|
||||
const tokenParts: string[] = idToken.split(".");
|
||||
if (tokenParts.length >= 2 && tokenParts[1]) {
|
||||
const payload: JSONObject = JSON.parse(
|
||||
Buffer.from(tokenParts[1], "base64").toString(),
|
||||
);
|
||||
|
||||
// Extract tenant information from token claims
|
||||
if (payload["tid"]) {
|
||||
tenantId = payload["tid"] as string;
|
||||
teamId = payload["tid"] as string;
|
||||
}
|
||||
|
||||
// Try to get tenant name from various claims
|
||||
if (payload["tenant_name"]) {
|
||||
tenantName = payload["tenant_name"] as string;
|
||||
} else if (payload["tenant_display_name"]) {
|
||||
tenantName = payload["tenant_display_name"] as string;
|
||||
} else if (
|
||||
payload["iss"] &&
|
||||
typeof payload["iss"] === "string" &&
|
||||
payload["iss"].includes("/")
|
||||
) {
|
||||
// Extract tenant ID from issuer if available
|
||||
const issuerParts: string[] = (payload["iss"] as string).split(
|
||||
"/",
|
||||
);
|
||||
const issuerTenantId: string = issuerParts[
|
||||
issuerParts.length - 2
|
||||
] as string;
|
||||
if (issuerTenantId && issuerTenantId !== "common") {
|
||||
tenantId = issuerTenantId;
|
||||
teamId = issuerTenantId;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error decoding ID token: " + (error as Error).message,
|
||||
);
|
||||
// Continue with default values
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to fetch user's joined teams to auto-select the first team (improves UX so we don't
|
||||
// display 'Microsoft Teams on Microsoft Teams'). We only do this if we have not already
|
||||
// identified a specific team (currently teamId defaults to tenantId) and we have a valid access token
|
||||
// with the Team.ReadBasic.All scope.
|
||||
try {
|
||||
const teamsResponse: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.get(
|
||||
URL.fromString("https://graph.microsoft.com/v1.0/me/joinedTeams"),
|
||||
undefined,
|
||||
{
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
|
||||
if (!(teamsResponse instanceof HTTPErrorResponse)) {
|
||||
const teamsJson: JSONObject = teamsResponse.data as JSONObject;
|
||||
const teamsArr: Array<JSONObject> =
|
||||
(teamsJson["value"] as Array<JSONObject>) || [];
|
||||
if (teamsArr.length > 0) {
|
||||
const firstTeam: JSONObject = teamsArr[0] as JSONObject;
|
||||
const firstTeamId: string | undefined = firstTeam["id"] as
|
||||
| string
|
||||
| undefined;
|
||||
const firstTeamName: string | undefined = firstTeam[
|
||||
"displayName"
|
||||
] as string | undefined;
|
||||
// Only override if we have meaningful data.
|
||||
if (firstTeamId) {
|
||||
teamId = firstTeamId;
|
||||
}
|
||||
if (firstTeamName) {
|
||||
tenantName = firstTeamName;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug(
|
||||
"Could not auto-fetch Teams list to select default team. Proceeding with tenant defaults.",
|
||||
);
|
||||
}
|
||||
} catch (autoSelectErr) {
|
||||
logger.error(
|
||||
"Error auto-selecting first Microsoft Team: " +
|
||||
(autoSelectErr as Error).message,
|
||||
);
|
||||
}
|
||||
|
||||
// Get the actual Microsoft Teams user ID from Microsoft Graph API
|
||||
let microsoftTeamsUserId: string = userIdStr; // fallback to OneUptime user ID
|
||||
try {
|
||||
const userInfoResponse: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.get(
|
||||
URL.fromString("https://graph.microsoft.com/v1.0/me"),
|
||||
undefined,
|
||||
{
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
|
||||
if (!(userInfoResponse instanceof HTTPErrorResponse)) {
|
||||
const userInfo: JSONObject = userInfoResponse.data as JSONObject;
|
||||
const actualUserId: string = userInfo["id"] as string;
|
||||
if (actualUserId) {
|
||||
microsoftTeamsUserId = actualUserId;
|
||||
logger.debug(
|
||||
`Retrieved Microsoft Teams user ID: ${microsoftTeamsUserId} for OneUptime user: ${userIdStr}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
`Could not retrieve Microsoft Teams user info for user ${userIdStr}. Using OneUptime user ID as fallback.`,
|
||||
);
|
||||
logger.warn(userInfoResponse.message);
|
||||
}
|
||||
} catch (userInfoError) {
|
||||
logger.error("Error fetching Microsoft Teams user info:");
|
||||
logger.error(userInfoError);
|
||||
logger.warn(
|
||||
`Using OneUptime user ID ${userIdStr} as fallback for Microsoft Teams user ID`,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle different auth types based on state parameter
|
||||
const tokenExpiryDate: string | undefined = expiresIn
|
||||
? new Date(Date.now() + (expiresIn - 60) * 1000).toISOString() // subtract 60s buffer
|
||||
: undefined;
|
||||
|
||||
if (authType === "project") {
|
||||
// Project-level installation - save both project and user auth tokens
|
||||
await WorkspaceProjectAuthTokenService.refreshAuthToken({
|
||||
projectId: new ObjectID(projectIdStr),
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
authToken: accessToken,
|
||||
workspaceProjectId: tenantId,
|
||||
miscData: {
|
||||
teamId: teamId,
|
||||
teamName: tenantName,
|
||||
tenantId: tenantId,
|
||||
refreshToken: refreshToken || "",
|
||||
tokenExpiresAt: tokenExpiryDate || "",
|
||||
},
|
||||
});
|
||||
|
||||
// Also save user auth for the installing user
|
||||
await WorkspaceUserAuthTokenService.refreshAuthToken({
|
||||
projectId: new ObjectID(projectIdStr),
|
||||
userId: new ObjectID(userIdStr),
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
authToken: accessToken,
|
||||
workspaceUserId: microsoftTeamsUserId,
|
||||
miscData: {
|
||||
userId: microsoftTeamsUserId,
|
||||
oneUptimeUserId: userIdStr,
|
||||
tenantId: tenantId,
|
||||
refreshToken: refreshToken || "",
|
||||
tokenExpiresAt: tokenExpiryDate || "",
|
||||
},
|
||||
});
|
||||
} else if (authType === "user") {
|
||||
// User-level authentication only
|
||||
await WorkspaceUserAuthTokenService.refreshAuthToken({
|
||||
projectId: new ObjectID(projectIdStr),
|
||||
userId: new ObjectID(userIdStr),
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
authToken: accessToken,
|
||||
workspaceUserId: microsoftTeamsUserId,
|
||||
miscData: {
|
||||
userId: microsoftTeamsUserId,
|
||||
oneUptimeUserId: userIdStr,
|
||||
tenantId: tenantId,
|
||||
refreshToken: refreshToken || "",
|
||||
tokenExpiresAt: tokenExpiryDate || "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Response.redirect(req, res, settingsUrl);
|
||||
},
|
||||
);
|
||||
|
||||
// Endpoint to get available teams for a user
|
||||
router.post(
|
||||
"/teams/get-teams",
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
try {
|
||||
const userAuthTokenId: string = req.body["userAuthTokenId"] as string;
|
||||
|
||||
if (!userAuthTokenId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("User auth token ID is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// Get the user auth token
|
||||
const userAuthToken: any =
|
||||
await WorkspaceUserAuthTokenService.findOneById({
|
||||
id: new ObjectID(userAuthTokenId),
|
||||
select: {
|
||||
authToken: true,
|
||||
userId: true,
|
||||
projectId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("User auth token not found"),
|
||||
);
|
||||
}
|
||||
|
||||
// Make API call to Microsoft Graph to get user's joined teams
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.get(
|
||||
URL.fromString("https://graph.microsoft.com/v1.0/me/joinedTeams"),
|
||||
undefined,
|
||||
{
|
||||
Authorization: `Bearer ${userAuthToken.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
logger.error("Error getting teams from Microsoft Graph:");
|
||||
logger.error(response);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"Failed to fetch teams from Microsoft Graph",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const teamsData: JSONObject = response.data as JSONObject;
|
||||
const teams: Array<JSONObject> =
|
||||
(teamsData["value"] as Array<JSONObject>) || [];
|
||||
|
||||
// Transform the teams data to match our interface
|
||||
const transformedTeams: Array<{
|
||||
id: string;
|
||||
displayName: string;
|
||||
description?: string | undefined;
|
||||
}> = teams.map((team: JSONObject) => {
|
||||
const description: string | undefined = team["description"] as
|
||||
| string
|
||||
| undefined;
|
||||
return {
|
||||
id: team["id"] as string,
|
||||
displayName: team["displayName"] as string,
|
||||
...(description && { description }),
|
||||
};
|
||||
});
|
||||
// Auto-select the first team if the project auth token has no team set yet.
|
||||
try {
|
||||
if (transformedTeams.length > 0) {
|
||||
// Find corresponding project-level auth token to update miscData
|
||||
const projectAuth: WorkspaceProjectAuthToken | null =
|
||||
await WorkspaceProjectAuthTokenService.findOneBy({
|
||||
query: {
|
||||
projectId: userAuthToken.projectId!,
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
miscData: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (projectAuth) {
|
||||
const miscData: JSONObject = (projectAuth.miscData ||
|
||||
{}) as JSONObject;
|
||||
const existingTeamId: string | undefined = miscData[
|
||||
"teamId"
|
||||
] as string | undefined;
|
||||
if (!existingTeamId) {
|
||||
const first: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
description?: string | undefined;
|
||||
} = transformedTeams[0]!;
|
||||
await WorkspaceProjectAuthTokenService.updateOneById({
|
||||
id: projectAuth.id!,
|
||||
data: {
|
||||
miscData: {
|
||||
...miscData,
|
||||
teamId: first.id,
|
||||
teamName: first.displayName,
|
||||
},
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (autoSelectErr) {
|
||||
logger.error("Failed to auto-select first Microsoft Teams team:");
|
||||
logger.error(autoSelectErr);
|
||||
}
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
teams: transformedTeams,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error in /teams/get-teams endpoint:");
|
||||
logger.error(error);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Failed to fetch teams"),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
@@ -337,6 +337,12 @@ export const SlackAppClientSecret: string | null =
|
||||
export const SlackAppSigningSecret: string | null =
|
||||
process.env["SLACK_APP_SIGNING_SECRET"] || null;
|
||||
|
||||
// Microsoft Teams / Azure AD App Config
|
||||
export const MicrosoftTeamsAppClientId: string | null =
|
||||
process.env["MICROSOFT_TEAMS_APP_CLIENT_ID"] || null;
|
||||
export const MicrosoftTeamsAppClientSecret: string | null =
|
||||
process.env["MICROSOFT_TEAMS_APP_CLIENT_SECRET"] || null;
|
||||
|
||||
// VAPID Configuration for Web Push Notifications
|
||||
export const VapidPublicKey: string | undefined =
|
||||
process.env["VAPID_PUBLIC_KEY"] || undefined;
|
||||
|
||||
@@ -8,6 +8,7 @@ import Incident from "../../Models/DatabaseModels/Incident";
|
||||
import IncidentService from "./IncidentService";
|
||||
import { NotificationRuleConditionCheckOn } from "../../Types/Workspace/NotificationRules/NotificationRuleCondition";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import BadRequestException from "../../Types/Exception/BadRequestException";
|
||||
import Label from "../../Models/DatabaseModels/Label";
|
||||
import MonitorService from "./MonitorService";
|
||||
import Alert from "../../Models/DatabaseModels/Alert";
|
||||
@@ -116,7 +117,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
throw new BadDataException(
|
||||
"This account is not connected to " +
|
||||
rule.workspaceType +
|
||||
". Please go to User Settings and connect the account.",
|
||||
". Please go to User Settings and connect the account."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -139,7 +140,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
throw new BadDataException(
|
||||
"This project is not connected to " +
|
||||
rule.workspaceType +
|
||||
". Please go to Project Settings and connect the account.",
|
||||
". Please go to Project Settings and connect the account."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,7 +183,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
});
|
||||
} catch (err) {
|
||||
throw new BadDataException(
|
||||
"Cannot create a new channel. " + (err as Error)?.message,
|
||||
"Cannot create a new channel. " + (err as Error)?.message
|
||||
);
|
||||
}
|
||||
|
||||
@@ -195,7 +196,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
});
|
||||
} catch (err) {
|
||||
throw new BadDataException(
|
||||
"Cannot invite users to the channel. " + (err as Error)?.message,
|
||||
"Cannot invite users to the channel. " + (err as Error)?.message
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -210,7 +211,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
// check if these channels exist.
|
||||
const channelExists: boolean =
|
||||
await WorkspaceUtil.getWorkspaceTypeUtil(
|
||||
rule.workspaceType!,
|
||||
rule.workspaceType!
|
||||
).doesChannelExist({
|
||||
authToken: projectAuthToken,
|
||||
channelName: channelName,
|
||||
@@ -219,10 +220,16 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
|
||||
if (!channelExists) {
|
||||
throw new BadDataException(
|
||||
`Channel ${channelName} does not exist. If this channel is private, you need to invite OneUptime bot to the channel and try again.`,
|
||||
`Channel ${channelName} does not exist. If this channel is private, you need to invite OneUptime bot to the channel and try again.`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
// If it's already a BadRequestException with specific Microsoft Teams provisioning error, re-throw it
|
||||
if (err instanceof BadRequestException) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// For other errors, wrap them in BadDataException
|
||||
throw new BadDataException((err as Error)?.message);
|
||||
}
|
||||
}
|
||||
@@ -236,7 +243,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
projectId: data.projectId,
|
||||
messagePayloadsByWorkspace: messageBlocksByWorkspaceTypes.map(
|
||||
(
|
||||
messageBlocksByWorkspaceType: MessageBlocksByWorkspaceType,
|
||||
messageBlocksByWorkspaceType: MessageBlocksByWorkspaceType
|
||||
) => {
|
||||
return {
|
||||
_type: "WorkspaceMessagePayload",
|
||||
@@ -245,19 +252,19 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
channelNames: [],
|
||||
channelIds: [createdChannel.id],
|
||||
};
|
||||
},
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
// Log results for test sends (created channels)
|
||||
const getMessageSummary: (wt: WorkspaceType) => string = (
|
||||
wt: WorkspaceType,
|
||||
wt: WorkspaceType
|
||||
): string => {
|
||||
const blocks: Array<WorkspaceMessageBlock> | undefined =
|
||||
messageBlocksByWorkspaceTypes.find(
|
||||
(b: MessageBlocksByWorkspaceType) => {
|
||||
return b.workspaceType === wt;
|
||||
},
|
||||
}
|
||||
)?.messageBlocks;
|
||||
if (!blocks) {
|
||||
return "";
|
||||
@@ -299,8 +306,14 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// If it's already a BadRequestException with specific error message, re-throw it
|
||||
if (err instanceof BadRequestException) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// For other errors, wrap them in BadDataException
|
||||
throw new BadDataException(
|
||||
"Cannot post message to channel. " + (err as Error)?.message,
|
||||
"Cannot post message to channel. " + (err as Error)?.message
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -312,7 +325,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
projectId: data.projectId,
|
||||
messagePayloadsByWorkspace: messageBlocksByWorkspaceTypes.map(
|
||||
(
|
||||
messageBlocksByWorkspaceType: MessageBlocksByWorkspaceType,
|
||||
messageBlocksByWorkspaceType: MessageBlocksByWorkspaceType
|
||||
) => {
|
||||
return {
|
||||
_type: "WorkspaceMessagePayload",
|
||||
@@ -321,19 +334,19 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
channelNames: [existingChannelName],
|
||||
channelIds: [],
|
||||
};
|
||||
},
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
// Log results for test sends (existing channels)
|
||||
const getMessageSummary: (wt: WorkspaceType) => string = (
|
||||
wt: WorkspaceType,
|
||||
wt: WorkspaceType
|
||||
): string => {
|
||||
const blocks: Array<WorkspaceMessageBlock> | undefined =
|
||||
messageBlocksByWorkspaceTypes.find(
|
||||
(b: MessageBlocksByWorkspaceType) => {
|
||||
return b.workspaceType === wt;
|
||||
},
|
||||
}
|
||||
)?.messageBlocks;
|
||||
if (!blocks) {
|
||||
return "";
|
||||
@@ -375,8 +388,14 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// If it's already a BadRequestException with specific error message, re-throw it
|
||||
if (err instanceof BadRequestException) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// For other errors, wrap them in BadDataException
|
||||
throw new BadDataException(
|
||||
"Cannot post message to channel. " + (err as Error)?.message,
|
||||
"Cannot post message to channel. " + (err as Error)?.message
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -398,7 +417,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
notificationFor: data.notificationFor,
|
||||
workspaceType: workspaceType,
|
||||
notificationRuleEventType: this.getNotificationRuleEventType(
|
||||
data.notificationFor,
|
||||
data.notificationFor
|
||||
),
|
||||
});
|
||||
|
||||
@@ -428,7 +447,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
const channelIds: Array<string> = channels.map(
|
||||
(channel: WorkspaceChannel) => {
|
||||
return channel.id;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// get project auth token.
|
||||
@@ -498,7 +517,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
return (
|
||||
messageBlocksByWorkspaceType.workspaceType === workspaceType
|
||||
);
|
||||
},
|
||||
}
|
||||
)
|
||||
?.messageBlocks.push(...messageBlocksByWorkspaceType.messageBlocks);
|
||||
}
|
||||
@@ -511,7 +530,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
await this.getExistingChannelNamesBasedOnEventType({
|
||||
projectId: data.projectId,
|
||||
notificationRuleEventType: this.getNotificationRuleEventType(
|
||||
data.notificationFor,
|
||||
data.notificationFor
|
||||
),
|
||||
workspaceType: messageBlocksByWorkspaceType.workspaceType,
|
||||
notificationFor: data.notificationFor,
|
||||
@@ -546,13 +565,13 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
|
||||
// Create logs for each response/thread
|
||||
const getMessageSummary: (wt: WorkspaceType) => string = (
|
||||
wt: WorkspaceType,
|
||||
wt: WorkspaceType
|
||||
): string => {
|
||||
const blocks: Array<WorkspaceMessageBlock> | undefined =
|
||||
messageBlocksByWorkspaceTypes.find(
|
||||
(b: MessageBlocksByWorkspaceType) => {
|
||||
return b.workspaceType === wt;
|
||||
},
|
||||
}
|
||||
)?.messageBlocks;
|
||||
if (!blocks) {
|
||||
return "";
|
||||
@@ -656,7 +675,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
{
|
||||
scheduledMaintenanceId: data.notificationFor.scheduledMaintenanceId,
|
||||
workspaceType: data.workspaceType,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -667,7 +686,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
}
|
||||
|
||||
private getNotificationRuleEventType(
|
||||
notificationFor: NotificationFor,
|
||||
notificationFor: NotificationFor
|
||||
): NotificationRuleEventType {
|
||||
if (notificationFor.alertId) {
|
||||
return NotificationRuleEventType.Alert;
|
||||
@@ -712,7 +731,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
|
||||
if (!userId) {
|
||||
throw new BadDataException(
|
||||
"Bot user ID not found in project auth token",
|
||||
"Bot user ID not found in project auth token"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -748,7 +767,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
notificationRules: notificationRules.map(
|
||||
(rule: WorkspaceNotificationRule) => {
|
||||
return rule.notificationRule as BaseNotificationRule;
|
||||
},
|
||||
}
|
||||
),
|
||||
}) || [];
|
||||
|
||||
@@ -767,132 +786,123 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
}): Promise<{
|
||||
channelsCreated: Array<NotificationRuleWorkspaceChannel>;
|
||||
} | null> {
|
||||
try {
|
||||
logger.debug(
|
||||
"WorkspaceNotificationRuleService.createInviteAndPostToChannelsBasedOnRules",
|
||||
);
|
||||
logger.debug(data);
|
||||
logger.debug(
|
||||
"WorkspaceNotificationRuleService.createChannelsAndInviteUsersToChannelsBasedOnRules called with data:"
|
||||
);
|
||||
logger.debug(data);
|
||||
logger.debug(
|
||||
`DEBUG: Processing channel creation for event type: ${data.notificationRuleEventType}`
|
||||
);
|
||||
|
||||
const channelsCreated: Array<NotificationRuleWorkspaceChannel> = [];
|
||||
const channelsCreated: Array<NotificationRuleWorkspaceChannel> = [];
|
||||
|
||||
const projectAuths: Array<WorkspaceProjectAuthToken> =
|
||||
await WorkspaceProjectAuthTokenService.getProjectAuths({
|
||||
projectId: data.projectId,
|
||||
});
|
||||
const projectAuths: Array<WorkspaceProjectAuthToken> =
|
||||
await WorkspaceProjectAuthTokenService.getProjectAuths({
|
||||
projectId: data.projectId,
|
||||
});
|
||||
|
||||
logger.debug("projectAuths");
|
||||
logger.debug(projectAuths);
|
||||
logger.debug("projectAuths");
|
||||
logger.debug(projectAuths);
|
||||
|
||||
if (!projectAuths || projectAuths.length === 0) {
|
||||
// do nothing.
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const projectAuth of projectAuths) {
|
||||
try {
|
||||
if (!projectAuth.authToken) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!projectAuth.workspaceType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const authToken: string = projectAuth.authToken;
|
||||
const workspaceType: WorkspaceType = projectAuth.workspaceType;
|
||||
|
||||
const notificationRules: Array<WorkspaceNotificationRule> =
|
||||
await this.getMatchingNotificationRules({
|
||||
projectId: data.projectId,
|
||||
workspaceType: workspaceType,
|
||||
notificationRuleEventType: data.notificationRuleEventType,
|
||||
notificationFor: data.notificationFor,
|
||||
});
|
||||
|
||||
logger.debug("notificationRules");
|
||||
logger.debug(notificationRules);
|
||||
|
||||
if (!notificationRules || notificationRules.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug("Creating channels based on rules");
|
||||
const createdWorkspaceChannels: Array<NotificationRuleWorkspaceChannel> =
|
||||
await this.createChannelsBasedOnRules({
|
||||
projectId: data.projectId,
|
||||
projectOrUserAuthTokenForWorkspace: authToken,
|
||||
workspaceType: workspaceType,
|
||||
notificationRules: notificationRules,
|
||||
channelNameSiffix: data.channelNameSiffix,
|
||||
notificationEventType: data.notificationRuleEventType,
|
||||
notificationFor: data.notificationFor,
|
||||
});
|
||||
|
||||
logger.debug("createdWorkspaceChannels");
|
||||
logger.debug(createdWorkspaceChannels);
|
||||
|
||||
logger.debug("Inviting users and teams to channels based on rules");
|
||||
await this.inviteUsersAndTeamsToChannelsBasedOnRules({
|
||||
projectId: data.projectId,
|
||||
projectAuth: projectAuth,
|
||||
workspaceType: workspaceType,
|
||||
notificationRules: notificationRules,
|
||||
notificationChannels: createdWorkspaceChannels,
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
"Getting existing channel names from notification rules",
|
||||
);
|
||||
const existingChannelNames: Array<string> =
|
||||
this.getExistingChannelNamesFromNotificationRules({
|
||||
notificationRules: notificationRules.map(
|
||||
(rule: WorkspaceNotificationRule) => {
|
||||
return rule.notificationRule as BaseNotificationRule;
|
||||
},
|
||||
),
|
||||
}) || [];
|
||||
|
||||
logger.debug("Existing channel names:");
|
||||
logger.debug(existingChannelNames);
|
||||
|
||||
logger.debug(
|
||||
"Adding created channel names to existing channel names",
|
||||
);
|
||||
for (const channel of createdWorkspaceChannels) {
|
||||
if (!existingChannelNames.includes(channel.name)) {
|
||||
existingChannelNames.push(channel.name);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Final list of channel names to post messages to:");
|
||||
logger.debug(existingChannelNames);
|
||||
|
||||
logger.debug("Posting messages to workspace channels");
|
||||
|
||||
logger.debug("Channels created:");
|
||||
logger.debug(createdWorkspaceChannels);
|
||||
|
||||
channelsCreated.push(...createdWorkspaceChannels);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"Error in creating channels and inviting users to channels for workspace type " +
|
||||
projectAuth.workspaceType,
|
||||
);
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Returning created channels");
|
||||
return {
|
||||
channelsCreated: channelsCreated,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"Error in createChannelsAndInviteUsersToChannelsBasedOnRules:",
|
||||
);
|
||||
logger.error(err);
|
||||
if (!projectAuths || projectAuths.length === 0) {
|
||||
// do nothing.
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const projectAuth of projectAuths) {
|
||||
try {
|
||||
if (!projectAuth.authToken) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!projectAuth.workspaceType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const authToken: string = projectAuth.authToken;
|
||||
const workspaceType: WorkspaceType = projectAuth.workspaceType;
|
||||
|
||||
const notificationRules: Array<WorkspaceNotificationRule> =
|
||||
await this.getMatchingNotificationRules({
|
||||
projectId: data.projectId,
|
||||
workspaceType: workspaceType,
|
||||
notificationRuleEventType: data.notificationRuleEventType,
|
||||
notificationFor: data.notificationFor,
|
||||
});
|
||||
|
||||
logger.debug("notificationRules");
|
||||
logger.debug(notificationRules);
|
||||
|
||||
if (!notificationRules || notificationRules.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug("Creating channels based on rules");
|
||||
const createdWorkspaceChannels: Array<NotificationRuleWorkspaceChannel> =
|
||||
await this.createChannelsBasedOnRules({
|
||||
projectId: data.projectId,
|
||||
projectOrUserAuthTokenForWorkspace: authToken,
|
||||
workspaceType: workspaceType,
|
||||
notificationRules: notificationRules,
|
||||
channelNameSiffix: data.channelNameSiffix,
|
||||
notificationEventType: data.notificationRuleEventType,
|
||||
notificationFor: data.notificationFor,
|
||||
});
|
||||
|
||||
logger.debug("createdWorkspaceChannels");
|
||||
logger.debug(createdWorkspaceChannels);
|
||||
|
||||
logger.debug("Inviting users and teams to channels based on rules");
|
||||
await this.inviteUsersAndTeamsToChannelsBasedOnRules({
|
||||
projectId: data.projectId,
|
||||
projectAuth: projectAuth,
|
||||
workspaceType: workspaceType,
|
||||
notificationRules: notificationRules,
|
||||
notificationChannels: createdWorkspaceChannels,
|
||||
});
|
||||
|
||||
logger.debug("Getting existing channel names from notification rules");
|
||||
const existingChannelNames: Array<string> =
|
||||
this.getExistingChannelNamesFromNotificationRules({
|
||||
notificationRules: notificationRules.map(
|
||||
(rule: WorkspaceNotificationRule) => {
|
||||
return rule.notificationRule as BaseNotificationRule;
|
||||
}
|
||||
),
|
||||
}) || [];
|
||||
|
||||
logger.debug("Existing channel names:");
|
||||
logger.debug(existingChannelNames);
|
||||
|
||||
logger.debug("Adding created channel names to existing channel names");
|
||||
for (const channel of createdWorkspaceChannels) {
|
||||
if (!existingChannelNames.includes(channel.name)) {
|
||||
existingChannelNames.push(channel.name);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Final list of channel names to post messages to:");
|
||||
logger.debug(existingChannelNames);
|
||||
|
||||
logger.debug("Posting messages to workspace channels");
|
||||
|
||||
logger.debug("Channels created:");
|
||||
logger.debug(createdWorkspaceChannels);
|
||||
|
||||
channelsCreated.push(...createdWorkspaceChannels);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"Error in creating channels and inviting users to channels for workspace type " +
|
||||
projectAuth.workspaceType
|
||||
);
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Returning created channels");
|
||||
return {
|
||||
channelsCreated: channelsCreated,
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
@@ -1014,7 +1024,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
logger.debug(channelIds);
|
||||
|
||||
await WorkspaceUtil.getWorkspaceTypeUtil(
|
||||
data.workspaceType,
|
||||
data.workspaceType
|
||||
).sendMessage({
|
||||
userId: data.projectAuth.workspaceProjectId!,
|
||||
authToken: data.projectAuth.authToken!,
|
||||
@@ -1046,7 +1056,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
logger.debug(workspaceUserIds);
|
||||
|
||||
await WorkspaceUtil.getWorkspaceTypeUtil(
|
||||
data.workspaceType,
|
||||
data.workspaceType
|
||||
).inviteUsersToChannels({
|
||||
authToken: data.projectAuth.authToken!,
|
||||
workspaceChannelInvitationPayload: {
|
||||
@@ -1084,7 +1094,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
"inviteUsersBasedOnRulesAndWorkspaceChannels called with data:",
|
||||
"inviteUsersBasedOnRulesAndWorkspaceChannels called with data:"
|
||||
);
|
||||
logger.debug(data);
|
||||
const userIds: Array<ObjectID> = data.userIds;
|
||||
@@ -1112,7 +1122,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
return notificationRules.find((rule: WorkspaceNotificationRule) => {
|
||||
return rule.id?.toString() === channel.notificationRuleId;
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
logger.debug("Channels to invite to based on rule:");
|
||||
@@ -1165,7 +1175,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
const channelIds: Array<string> = channelsToInviteToBasedOnRule.map(
|
||||
(channel: NotificationRuleWorkspaceChannel) => {
|
||||
return channel.id as string;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
logger.debug("Channel IDs to send message to:");
|
||||
@@ -1192,7 +1202,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
],
|
||||
},
|
||||
projectId: data.projectId,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error("Error in sending message to channel");
|
||||
@@ -1207,14 +1217,14 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
const channelNames: Array<string> = channelsToInviteToBasedOnRule.map(
|
||||
(channel: NotificationRuleWorkspaceChannel) => {
|
||||
return channel.name;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
logger.debug("Channel names to invite to:");
|
||||
logger.debug(channelNames);
|
||||
|
||||
await WorkspaceUtil.getWorkspaceTypeUtil(
|
||||
workspaceType,
|
||||
workspaceType
|
||||
).inviteUsersToChannels({
|
||||
authToken: projectAuth.authToken!,
|
||||
workspaceChannelInvitationPayload: {
|
||||
@@ -1273,7 +1283,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
}
|
||||
|
||||
const usersInTeam: Array<User> = await TeamMemberService.getUsersInTeams(
|
||||
data.teamIds,
|
||||
data.teamIds
|
||||
);
|
||||
|
||||
logger.debug("Users in teams:");
|
||||
@@ -1336,6 +1346,9 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
}): Promise<Array<NotificationRuleWorkspaceChannel>> {
|
||||
logger.debug("createChannelsBasedOnRules called with data:");
|
||||
logger.debug(data);
|
||||
logger.debug(
|
||||
`DEBUG: Creating channels for workspace type: ${data.workspaceType}`
|
||||
);
|
||||
|
||||
const createdWorkspaceChannels: Array<NotificationRuleWorkspaceChannel> =
|
||||
[];
|
||||
@@ -1352,6 +1365,9 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
|
||||
logger.debug("New channel names to be created:");
|
||||
logger.debug(notificationChannels);
|
||||
logger.debug(
|
||||
`DEBUG: Number of channels to create: ${notificationChannels.length}`
|
||||
);
|
||||
|
||||
if (!notificationChannels || notificationChannels.length === 0) {
|
||||
logger.debug("No new channel names found. Returning empty array.");
|
||||
@@ -1365,75 +1381,89 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
}).length > 0
|
||||
) {
|
||||
logger.debug(
|
||||
`Channel name ${notificationChannel.channelName} already created. Skipping.`,
|
||||
`Channel name ${notificationChannel.channelName} already created. Skipping.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Creating new channel with name: ${notificationChannel.channelName}`,
|
||||
`Creating new channel with name: ${notificationChannel.channelName}`
|
||||
);
|
||||
const channel: WorkspaceChannel =
|
||||
await WorkspaceUtil.getWorkspaceTypeUtil(
|
||||
data.workspaceType,
|
||||
).createChannel({
|
||||
authToken: data.projectOrUserAuthTokenForWorkspace,
|
||||
channelName: notificationChannel.channelName,
|
||||
projectId: data.projectId,
|
||||
});
|
||||
|
||||
const notificationWorkspaceChannel: NotificationRuleWorkspaceChannel = {
|
||||
...channel,
|
||||
notificationRuleId: notificationChannel.notificationRuleId,
|
||||
};
|
||||
|
||||
logger.debug("Channel created:");
|
||||
logger.debug(channel);
|
||||
|
||||
// Log the channel creation
|
||||
try {
|
||||
const logData: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
incidentId?: ObjectID;
|
||||
alertId?: ObjectID;
|
||||
scheduledMaintenanceId?: ObjectID;
|
||||
onCallDutyPolicyId?: ObjectID;
|
||||
} = {
|
||||
projectId: data.projectId,
|
||||
workspaceType: data.workspaceType,
|
||||
channelId: channel.id,
|
||||
channelName: channel.name,
|
||||
const channel: WorkspaceChannel =
|
||||
await WorkspaceUtil.getWorkspaceTypeUtil(
|
||||
data.workspaceType
|
||||
).createChannel({
|
||||
authToken: data.projectOrUserAuthTokenForWorkspace,
|
||||
channelName: notificationChannel.channelName,
|
||||
projectId: data.projectId,
|
||||
});
|
||||
|
||||
const notificationWorkspaceChannel: NotificationRuleWorkspaceChannel = {
|
||||
...channel,
|
||||
notificationRuleId: notificationChannel.notificationRuleId,
|
||||
};
|
||||
|
||||
// Add resource associations only if they exist
|
||||
if (data.notificationFor?.incidentId) {
|
||||
logData.incidentId = data.notificationFor.incidentId;
|
||||
}
|
||||
if (data.notificationFor?.alertId) {
|
||||
logData.alertId = data.notificationFor.alertId;
|
||||
}
|
||||
if (data.notificationFor?.scheduledMaintenanceId) {
|
||||
logData.scheduledMaintenanceId =
|
||||
data.notificationFor.scheduledMaintenanceId;
|
||||
}
|
||||
if (data.notificationFor?.onCallDutyPolicyId) {
|
||||
logData.onCallDutyPolicyId = data.notificationFor.onCallDutyPolicyId;
|
||||
logger.debug("Channel created:");
|
||||
logger.debug(channel);
|
||||
logger.debug(
|
||||
`DEBUG: Channel created successfully for ${data.workspaceType}: ${channel.name} (ID: ${channel.id})`
|
||||
);
|
||||
|
||||
// Log the channel creation
|
||||
try {
|
||||
const logData: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
incidentId?: ObjectID;
|
||||
alertId?: ObjectID;
|
||||
scheduledMaintenanceId?: ObjectID;
|
||||
onCallDutyPolicyId?: ObjectID;
|
||||
} = {
|
||||
projectId: data.projectId,
|
||||
workspaceType: data.workspaceType,
|
||||
channelId: channel.id,
|
||||
channelName: channel.name,
|
||||
};
|
||||
|
||||
// Add resource associations only if they exist
|
||||
if (data.notificationFor?.incidentId) {
|
||||
logData.incidentId = data.notificationFor.incidentId;
|
||||
}
|
||||
if (data.notificationFor?.alertId) {
|
||||
logData.alertId = data.notificationFor.alertId;
|
||||
}
|
||||
if (data.notificationFor?.scheduledMaintenanceId) {
|
||||
logData.scheduledMaintenanceId =
|
||||
data.notificationFor.scheduledMaintenanceId;
|
||||
}
|
||||
if (data.notificationFor?.onCallDutyPolicyId) {
|
||||
logData.onCallDutyPolicyId =
|
||||
data.notificationFor.onCallDutyPolicyId;
|
||||
}
|
||||
|
||||
await WorkspaceNotificationLogService.logCreateChannel(logData, {
|
||||
isRoot: true,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Error logging channel creation:");
|
||||
logger.error(err);
|
||||
// Don't throw the error, just log it so the main flow continues
|
||||
}
|
||||
|
||||
await WorkspaceNotificationLogService.logCreateChannel(logData, {
|
||||
isRoot: true,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Error logging channel creation:");
|
||||
logger.error(err);
|
||||
// Don't throw the error, just log it so the main flow continues
|
||||
createdChannelNames.push(channel.name);
|
||||
createdWorkspaceChannels.push(notificationWorkspaceChannel);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`ERROR: Failed to create channel ${notificationChannel.channelName} for ${data.workspaceType}:`
|
||||
);
|
||||
logger.error(error);
|
||||
// Continue with other channels even if one fails
|
||||
continue;
|
||||
}
|
||||
|
||||
createdChannelNames.push(channel.name);
|
||||
createdWorkspaceChannels.push(notificationWorkspaceChannel);
|
||||
}
|
||||
|
||||
logger.debug("Returning created workspace channels:");
|
||||
@@ -1539,7 +1569,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
notificationRules: Array<BaseNotificationRule>;
|
||||
}): Array<string> {
|
||||
logger.debug(
|
||||
"getExistingChannelNamesFromNotificationRules called with data:",
|
||||
"getExistingChannelNamesFromNotificationRules called with data:"
|
||||
);
|
||||
logger.debug(data);
|
||||
|
||||
@@ -1583,7 +1613,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
notificationRuleId: string;
|
||||
}> {
|
||||
logger.debug(
|
||||
"getnotificationChannelssFromNotificationRules called with data:",
|
||||
"getnotificationChannelssFromNotificationRules called with data:"
|
||||
);
|
||||
logger.debug(data);
|
||||
|
||||
@@ -1618,7 +1648,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
channels.filter(
|
||||
(name: { channelName: string; notificationRuleId: string }) => {
|
||||
return name.channelName === channelName;
|
||||
},
|
||||
}
|
||||
).length === 0
|
||||
) {
|
||||
// if channel name is not already added then add it.
|
||||
@@ -1629,7 +1659,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
logger.debug(`Channel name ${channelName} added to the list.`);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Channel name ${channelName} already exists in the list. Skipping.`,
|
||||
`Channel name ${channelName} already exists in the list. Skipping.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1877,7 +1907,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
scheduledMaintenance.monitors?.map(
|
||||
(monitor: ScheduledMaintenance) => {
|
||||
return monitor.id!;
|
||||
},
|
||||
}
|
||||
) || [],
|
||||
});
|
||||
|
||||
@@ -1917,7 +1947,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
scheduledMaintenance.monitors?.map(
|
||||
(monitor: ScheduledMaintenance) => {
|
||||
return monitor._id?.toString() || "";
|
||||
},
|
||||
}
|
||||
) || [],
|
||||
[NotificationRuleConditionCheckOn.OnCallDutyPolicyName]: undefined,
|
||||
[NotificationRuleConditionCheckOn.OnCallDutyPolicyDescription]:
|
||||
|
||||
@@ -2,7 +2,7 @@ import ObjectID from "../../Types/ObjectID";
|
||||
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model, {
|
||||
SlackMiscData,
|
||||
MiscData,
|
||||
} from "../../Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
@@ -64,13 +64,36 @@ export class Service extends DatabaseService<Model> {
|
||||
return Boolean(await this.getProjectAuth(data));
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async getByAuthToken(data: {
|
||||
authToken: string;
|
||||
workspaceType: WorkspaceType;
|
||||
}): Promise<Model | null> {
|
||||
return await this.findOneBy({
|
||||
query: {
|
||||
authToken: data.authToken,
|
||||
workspaceType: data.workspaceType,
|
||||
},
|
||||
select: {
|
||||
authToken: true,
|
||||
workspaceProjectId: true,
|
||||
miscData: true,
|
||||
projectId: true,
|
||||
workspaceType: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async refreshAuthToken(data: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
authToken: string;
|
||||
workspaceProjectId: string;
|
||||
miscData: SlackMiscData;
|
||||
miscData: MiscData;
|
||||
}): Promise<void> {
|
||||
let projectAuth: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model, {
|
||||
SlackSettings,
|
||||
} from "../../Models/DatabaseModels/WorkspaceSetting";
|
||||
import Model, { Settings } from "../../Models/DatabaseModels/WorkspaceSetting";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
@@ -37,7 +35,7 @@ export class Service extends DatabaseService<Model> {
|
||||
public async refreshSetting(data: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
settings: SlackSettings;
|
||||
settings: Settings;
|
||||
}): Promise<void> {
|
||||
let workspaceSetting: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
@@ -77,5 +75,26 @@ export class Service extends DatabaseService<Model> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async getSettings(data: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
}): Promise<Settings | null> {
|
||||
const ws: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
workspaceType: data.workspaceType,
|
||||
},
|
||||
select: {
|
||||
settings: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return ws?.settings || null;
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
|
||||
@@ -2,7 +2,7 @@ import ObjectID from "../../Types/ObjectID";
|
||||
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model, {
|
||||
SlackMiscData,
|
||||
MiscData,
|
||||
} from "../../Models/DatabaseModels/WorkspaceUserAuthToken";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
|
||||
@@ -59,6 +59,30 @@ export class Service extends DatabaseService<Model> {
|
||||
);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async getByAuthToken(data: {
|
||||
authToken: string;
|
||||
workspaceType: WorkspaceType;
|
||||
}): Promise<Model | null> {
|
||||
return await this.findOneBy({
|
||||
query: {
|
||||
authToken: data.authToken,
|
||||
workspaceType: data.workspaceType,
|
||||
},
|
||||
select: {
|
||||
authToken: true,
|
||||
projectId: true,
|
||||
userId: true,
|
||||
workspaceUserId: true,
|
||||
miscData: true,
|
||||
workspaceType: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async refreshAuthToken(data: {
|
||||
projectId: ObjectID;
|
||||
@@ -66,7 +90,7 @@ export class Service extends DatabaseService<Model> {
|
||||
workspaceType: WorkspaceType;
|
||||
authToken: string;
|
||||
workspaceUserId: string;
|
||||
miscData: SlackMiscData;
|
||||
miscData: MiscData;
|
||||
}): Promise<void> {
|
||||
let userAuth: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
enum SlackActionType {
|
||||
enum MicrosoftTeamsActionType {
|
||||
// Incident actions
|
||||
AcknowledgeIncident = "AcknowledgeIncident",
|
||||
ResolveIncident = "ResolveIncident",
|
||||
@@ -9,10 +9,10 @@ enum SlackActionType {
|
||||
ViewExecuteIncidentOnCallPolicy = "ViewExecuteIncidentOnCallPolicy",
|
||||
SubmitExecuteIncidentOnCallPolicy = "SubmitExecuteIncidentOnCallPolicy",
|
||||
ViewIncident = "ViewIncident",
|
||||
NewIncident = "/incident", // new incident slash command
|
||||
NewIncident = "/incident", // new incident command
|
||||
SubmitNewIncident = "SubmitNewIncident",
|
||||
|
||||
// Alert Actions just like Incident Actions
|
||||
// Alert Actions
|
||||
AcknowledgeAlert = "AcknowledgeAlert",
|
||||
ResolveAlert = "ResolveAlert",
|
||||
ViewAddAlertNote = "ViewAddAlertNote",
|
||||
@@ -23,7 +23,7 @@ enum SlackActionType {
|
||||
SubmitExecuteAlertOnCallPolicy = "SubmitExecuteAlertOnCallPolicy",
|
||||
ViewAlert = "ViewAlert",
|
||||
|
||||
// Scheduled Maintenance Actions just like Incident Actions.
|
||||
// Scheduled Maintenance Actions
|
||||
MarkScheduledMaintenanceAsComplete = "MarkScheduledMaintenanceAsComplete",
|
||||
MarkScheduledMaintenanceAsOngoing = "MarkScheduledMaintenanceAsOngoing",
|
||||
ViewAddScheduledMaintenanceNote = "ViewAddScheduledMaintenanceNote",
|
||||
@@ -31,8 +31,14 @@ enum SlackActionType {
|
||||
ViewChangeScheduledMaintenanceState = "ViewChangeScheduledMaintenanceState",
|
||||
SubmitChangeScheduledMaintenanceState = "SubmitChangeScheduledMaintenanceState",
|
||||
ViewScheduledMaintenance = "ViewScheduledMaintenance",
|
||||
NewScheduledMaintenance = "/maintenance", // new scheduled maintenance slash command
|
||||
NewScheduledMaintenance = "/maintenance", // new scheduled maintenance command
|
||||
SubmitNewScheduledMaintenance = "SubmitNewScheduledMaintenance",
|
||||
|
||||
// Monitor Actions
|
||||
ViewMonitor = "ViewMonitor",
|
||||
|
||||
// On-call policy Actions
|
||||
ViewOnCallPolicy = "ViewOnCallPolicy",
|
||||
}
|
||||
|
||||
export default SlackActionType;
|
||||
export default MicrosoftTeamsActionType;
|
||||
|
||||
459
Common/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts
Normal file
459
Common/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts
Normal file
@@ -0,0 +1,459 @@
|
||||
import BadDataException from "../../../../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../../../../Types/ObjectID";
|
||||
import AlertService from "../../../../Services/AlertService";
|
||||
import { ExpressRequest, ExpressResponse } from "../../../Express";
|
||||
import MicrosoftTeamsUtil from "../MicrosoftTeams";
|
||||
import MicrosoftTeamsActionType from "./ActionTypes";
|
||||
import { MicrosoftTeamsAction, MicrosoftTeamsRequest } from "./Auth";
|
||||
import Response from "../../../Response";
|
||||
import {
|
||||
WorkspaceDropdownBlock,
|
||||
WorkspaceModalBlock,
|
||||
WorkspacePayloadMarkdown,
|
||||
WorkspaceTextAreaBlock,
|
||||
} from "../../../../../Types/Workspace/WorkspaceMessagePayload";
|
||||
import AlertInternalNoteService from "../../../../Services/AlertInternalNoteService";
|
||||
import { LIMIT_PER_PROJECT } from "../../../../../Types/Database/LimitMax";
|
||||
import { DropdownOption } from "../../../../../UI/Components/Dropdown/Dropdown";
|
||||
import AlertState from "../../../../../Models/DatabaseModels/AlertState";
|
||||
import AlertStateService from "../../../../Services/AlertStateService";
|
||||
import AlertInternalNote from "../../../../../Models/DatabaseModels/AlertInternalNote";
|
||||
import logger from "../../../Logger";
|
||||
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
||||
|
||||
export default class MicrosoftTeamsAlertActions {
|
||||
@CaptureSpan()
|
||||
public static isAlertAction(data: {
|
||||
actionType: MicrosoftTeamsActionType;
|
||||
}): boolean {
|
||||
const { actionType } = data;
|
||||
|
||||
switch (actionType) {
|
||||
case MicrosoftTeamsActionType.AcknowledgeAlert:
|
||||
case MicrosoftTeamsActionType.ResolveAlert:
|
||||
case MicrosoftTeamsActionType.ViewAddAlertNote:
|
||||
case MicrosoftTeamsActionType.SubmitAlertNote:
|
||||
case MicrosoftTeamsActionType.ViewChangeAlertState:
|
||||
case MicrosoftTeamsActionType.SubmitChangeAlertState:
|
||||
case MicrosoftTeamsActionType.ViewExecuteAlertOnCallPolicy:
|
||||
case MicrosoftTeamsActionType.SubmitExecuteAlertOnCallPolicy:
|
||||
case MicrosoftTeamsActionType.ViewAlert:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async acknowledgeAlert(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { req, res } = data;
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Alert ID"),
|
||||
);
|
||||
}
|
||||
|
||||
// Send early response to Teams
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
const alertId: ObjectID = new ObjectID(actionValue);
|
||||
|
||||
// Check if already acknowledged
|
||||
const isAlreadyAcknowledged: boolean =
|
||||
await AlertService.isAlertAcknowledged({
|
||||
alertId: alertId,
|
||||
});
|
||||
|
||||
if (isAlreadyAcknowledged) {
|
||||
const markdownPayload: WorkspacePayloadMarkdown = {
|
||||
_type: "WorkspacePayloadMarkdown",
|
||||
text: `Alert has already been acknowledged.`,
|
||||
};
|
||||
|
||||
await MicrosoftTeamsUtil.sendDirectMessageToUser({
|
||||
messageBlocks: [markdownPayload],
|
||||
authToken: data.teamsRequest.projectAuthToken!,
|
||||
workspaceUserId: data.teamsRequest.teamsUserId!,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Acknowledge the alert
|
||||
await AlertService.acknowledgeAlert(alertId, data.teamsRequest.userId!);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async resolveAlert(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { req, res } = data;
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Alert ID"),
|
||||
);
|
||||
}
|
||||
|
||||
// Send early response to Teams
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
const alertId: ObjectID = new ObjectID(actionValue);
|
||||
|
||||
// Check if already resolved
|
||||
const isAlreadyResolved: boolean = await AlertService.isAlertResolved({
|
||||
alertId: alertId,
|
||||
});
|
||||
|
||||
if (isAlreadyResolved) {
|
||||
const markdownPayload: WorkspacePayloadMarkdown = {
|
||||
_type: "WorkspacePayloadMarkdown",
|
||||
text: `Alert has already been resolved.`,
|
||||
};
|
||||
|
||||
await MicrosoftTeamsUtil.sendDirectMessageToUser({
|
||||
messageBlocks: [markdownPayload],
|
||||
authToken: data.teamsRequest.projectAuthToken!,
|
||||
workspaceUserId: data.teamsRequest.teamsUserId!,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the alert
|
||||
await AlertService.resolveAlert(alertId, data.teamsRequest.userId!);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async viewExecuteOnCallPolicy(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Implementation for viewing execute on-call policy modal
|
||||
logger.debug("Microsoft Teams view execute on-call policy implementation");
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async viewChangeAlertState(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { req, res } = data;
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Alert ID"),
|
||||
);
|
||||
}
|
||||
|
||||
// Send early response to Teams
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
const alertStates: Array<AlertState> = await AlertStateService.findBy({
|
||||
query: {
|
||||
projectId: data.teamsRequest.projectId!,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
const dropdownOptions: Array<DropdownOption> = alertStates
|
||||
.map((state: AlertState) => {
|
||||
return {
|
||||
label: state.name || "",
|
||||
value: state._id?.toString() || "",
|
||||
};
|
||||
})
|
||||
.filter((option: DropdownOption) => {
|
||||
return option.label !== "" && option.value !== "";
|
||||
});
|
||||
|
||||
const statePickerDropdown: WorkspaceDropdownBlock = {
|
||||
_type: "WorkspaceDropdownBlock",
|
||||
label: "Alert State",
|
||||
blockId: "alertState",
|
||||
placeholder: "Select Alert State",
|
||||
options: dropdownOptions,
|
||||
};
|
||||
|
||||
const modalBlock: WorkspaceModalBlock = {
|
||||
_type: "WorkspaceModalBlock",
|
||||
title: "Change Alert State",
|
||||
submitButtonTitle: "Submit",
|
||||
cancelButtonTitle: "Cancel",
|
||||
actionId: MicrosoftTeamsActionType.SubmitChangeAlertState,
|
||||
actionValue: actionValue,
|
||||
blocks: [statePickerDropdown],
|
||||
};
|
||||
|
||||
await MicrosoftTeamsUtil.showModalToUser({
|
||||
authToken: data.teamsRequest.projectAuthToken!,
|
||||
modalBlock: modalBlock,
|
||||
triggerId: data.teamsRequest.triggerId!,
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async submitChangeAlertState(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { req, res } = data;
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Alert ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.teamsRequest.viewValues) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid View Values"),
|
||||
);
|
||||
}
|
||||
|
||||
const alertStateId: string | undefined =
|
||||
data.teamsRequest.viewValues["alertState"]?.toString();
|
||||
|
||||
if (!alertStateId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Alert State is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// Send early response to Teams
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
const alertId: ObjectID = new ObjectID(actionValue);
|
||||
const stateId: ObjectID = new ObjectID(alertStateId);
|
||||
|
||||
// Update alert state
|
||||
await AlertService.updateOneById({
|
||||
id: alertId,
|
||||
data: {
|
||||
currentAlertStateId: stateId,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async executeOnCallPolicy(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Implementation for executing on-call policy
|
||||
logger.debug("Microsoft Teams execute on-call policy implementation");
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async submitAlertNote(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { req, res } = data;
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Alert ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.teamsRequest.viewValues) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid View Values"),
|
||||
);
|
||||
}
|
||||
|
||||
const noteContent: string | undefined =
|
||||
data.teamsRequest.viewValues["alertNote"]?.toString();
|
||||
|
||||
if (!noteContent) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Note content is required"),
|
||||
);
|
||||
}
|
||||
|
||||
// Send early response to Teams
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
const alertId: ObjectID = new ObjectID(actionValue);
|
||||
|
||||
// Create internal note for alert
|
||||
const internalNote: AlertInternalNote = new AlertInternalNote();
|
||||
internalNote.alertId = alertId;
|
||||
internalNote.note = noteContent;
|
||||
internalNote.projectId = data.teamsRequest.projectId!;
|
||||
internalNote.createdByUserId = data.teamsRequest.userId!;
|
||||
|
||||
await AlertInternalNoteService.create({
|
||||
data: internalNote,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async viewAddAlertNote(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { req, res } = data;
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Alert ID"),
|
||||
);
|
||||
}
|
||||
|
||||
// Send early response to Teams
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
const noteTextArea: WorkspaceTextAreaBlock = {
|
||||
_type: "WorkspaceTextAreaBlock",
|
||||
label: "Note Content",
|
||||
blockId: "alertNote",
|
||||
placeholder: "Enter note content here...",
|
||||
};
|
||||
|
||||
const modalBlock: WorkspaceModalBlock = {
|
||||
_type: "WorkspaceModalBlock",
|
||||
title: "Add Alert Note",
|
||||
submitButtonTitle: "Submit",
|
||||
cancelButtonTitle: "Cancel",
|
||||
actionId: MicrosoftTeamsActionType.SubmitAlertNote,
|
||||
actionValue: actionValue,
|
||||
blocks: [noteTextArea],
|
||||
};
|
||||
|
||||
await MicrosoftTeamsUtil.showModalToUser({
|
||||
authToken: data.teamsRequest.projectAuthToken!,
|
||||
modalBlock: modalBlock,
|
||||
triggerId: data.teamsRequest.triggerId!,
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async handleAlertAction(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { action } = data;
|
||||
|
||||
switch (action.actionType) {
|
||||
case MicrosoftTeamsActionType.AcknowledgeAlert:
|
||||
await this.acknowledgeAlert(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.ResolveAlert:
|
||||
await this.resolveAlert(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.ViewExecuteAlertOnCallPolicy:
|
||||
await this.viewExecuteOnCallPolicy(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.SubmitExecuteAlertOnCallPolicy:
|
||||
await this.executeOnCallPolicy(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.ViewChangeAlertState:
|
||||
await this.viewChangeAlertState(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.SubmitChangeAlertState:
|
||||
await this.submitChangeAlertState(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.ViewAddAlertNote:
|
||||
await this.viewAddAlertNote(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.SubmitAlertNote:
|
||||
await this.submitAlertNote(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.ViewAlert:
|
||||
// View action doesn't need implementation as it's handled by notification display
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.debug(
|
||||
`Unhandled Microsoft Teams alert action: ${action.actionType}`,
|
||||
);
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
}
|
||||
}
|
||||
230
Common/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts
Normal file
230
Common/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import WorkspaceProjectAuthToken from "../../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import WorkspaceUserAuthToken from "../../../../../Models/DatabaseModels/WorkspaceUserAuthToken";
|
||||
import ObjectID from "../../../../../Types/ObjectID";
|
||||
import WorkspaceUserAuthTokenService from "../../../../Services/WorkspaceUserAuthTokenService";
|
||||
import { ExpressRequest } from "../../../Express";
|
||||
import MicrosoftTeamsActionType from "./ActionTypes";
|
||||
import WorkspaceProjectAuthTokenService from "../../../../Services/WorkspaceProjectAuthTokenService";
|
||||
import logger from "../../../Logger";
|
||||
import { JSONObject } from "../../../../../Types/JSON";
|
||||
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
||||
import Dictionary from "../../../../../Types/Dictionary";
|
||||
|
||||
export interface MicrosoftTeamsAction {
|
||||
actionValue?: string | undefined;
|
||||
actionType?: MicrosoftTeamsActionType | undefined;
|
||||
}
|
||||
|
||||
export interface MicrosoftTeamsRequest {
|
||||
isAuthorized: boolean;
|
||||
userId?: ObjectID | undefined;
|
||||
projectId?: ObjectID | undefined;
|
||||
projectAuthToken?: string | undefined;
|
||||
userAuthToken?: string | undefined;
|
||||
teamsChannelId?: string | undefined;
|
||||
teamsMessageId?: string | undefined;
|
||||
teamsUserFullName?: string | undefined;
|
||||
teamsUserId?: string | undefined;
|
||||
teamsUsername?: string | undefined;
|
||||
actions?: MicrosoftTeamsAction[] | undefined;
|
||||
triggerId?: string | undefined;
|
||||
payloadType?: string | undefined;
|
||||
view?: JSONObject | undefined;
|
||||
viewValues?:
|
||||
| Dictionary<string | number | Array<string | number> | Date>
|
||||
| undefined;
|
||||
}
|
||||
|
||||
const teamsActionTypesThatDoNotRequireUserTeamsAccountToBeConnectedToOneUptime: Array<MicrosoftTeamsActionType> =
|
||||
[
|
||||
// anyone in the company can create incident.
|
||||
// regardless of whether they are connected to OneUptime or not.
|
||||
MicrosoftTeamsActionType.NewIncident,
|
||||
MicrosoftTeamsActionType.SubmitNewIncident,
|
||||
MicrosoftTeamsActionType.ViewIncident,
|
||||
];
|
||||
|
||||
export default class MicrosoftTeamsAuthAction {
|
||||
@CaptureSpan()
|
||||
public static async getTeamsRequestFromExpressRequest(
|
||||
req: ExpressRequest,
|
||||
options: {
|
||||
actionType?: MicrosoftTeamsActionType | undefined;
|
||||
actionValue?: string | undefined;
|
||||
},
|
||||
): Promise<MicrosoftTeamsRequest> {
|
||||
const teamsRequest: MicrosoftTeamsRequest = {
|
||||
isAuthorized: false,
|
||||
};
|
||||
|
||||
// Extract Teams-specific headers and payload
|
||||
// This would need to be adapted based on the actual Teams webhook format
|
||||
const teamsPayload: JSONObject = req.body as JSONObject;
|
||||
|
||||
logger.debug("Microsoft Teams request payload:");
|
||||
logger.debug(teamsPayload);
|
||||
|
||||
try {
|
||||
// Parse Teams user information
|
||||
if (teamsPayload["from"]) {
|
||||
const fromUser: JSONObject = teamsPayload["from"] as JSONObject;
|
||||
teamsRequest.teamsUserId = fromUser["id"] as string;
|
||||
teamsRequest.teamsUserFullName = fromUser["name"] as string;
|
||||
teamsRequest.teamsUsername = fromUser["name"] as string;
|
||||
}
|
||||
|
||||
// Parse channel information
|
||||
if (teamsPayload["channelData"]) {
|
||||
const channelData: JSONObject = teamsPayload[
|
||||
"channelData"
|
||||
] as JSONObject;
|
||||
teamsRequest.teamsChannelId = channelData["teamsChannelId"] as string;
|
||||
}
|
||||
|
||||
if (options.actionType) {
|
||||
teamsRequest.actions = [
|
||||
{
|
||||
actionType: options.actionType,
|
||||
actionValue: options.actionValue,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Authorize the request
|
||||
await this.authorizeTeamsRequest(teamsRequest);
|
||||
|
||||
return teamsRequest;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error parsing Microsoft Teams request: ${(error as Error).message}`,
|
||||
);
|
||||
return teamsRequest;
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async authorizeTeamsRequest(
|
||||
teamsRequest: MicrosoftTeamsRequest,
|
||||
): Promise<void> {
|
||||
if (!teamsRequest.teamsUserId) {
|
||||
logger.error("No Teams user ID found in request");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Authorizing Teams request for user ${teamsRequest.teamsUserId}`,
|
||||
);
|
||||
|
||||
try {
|
||||
// Find user auth token by Teams user ID
|
||||
const userAuthTokens: Array<WorkspaceUserAuthToken> =
|
||||
await WorkspaceUserAuthTokenService.findBy({
|
||||
query: {
|
||||
workspaceUserId: teamsRequest.teamsUserId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
userId: true,
|
||||
projectId: true,
|
||||
authToken: true,
|
||||
},
|
||||
limit: 1,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (userAuthTokens.length === 0) {
|
||||
// Check if this action type doesn't require user account connection
|
||||
if (
|
||||
teamsRequest.actions &&
|
||||
teamsRequest.actions.length > 0 &&
|
||||
teamsActionTypesThatDoNotRequireUserTeamsAccountToBeConnectedToOneUptime.includes(
|
||||
teamsRequest.actions[0]!.actionType!,
|
||||
)
|
||||
) {
|
||||
// Try to get project auth token instead
|
||||
const projectAuthTokens: Array<WorkspaceProjectAuthToken> =
|
||||
await WorkspaceProjectAuthTokenService.findBy({
|
||||
query: {},
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
authToken: true,
|
||||
miscData: true,
|
||||
},
|
||||
limit: 1,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (projectAuthTokens.length > 0) {
|
||||
const projectAuthToken: WorkspaceProjectAuthToken =
|
||||
projectAuthTokens[0]!;
|
||||
teamsRequest.projectId = projectAuthToken.projectId;
|
||||
teamsRequest.projectAuthToken = projectAuthToken.authToken;
|
||||
teamsRequest.isAuthorized = true;
|
||||
|
||||
logger.debug(
|
||||
`Authorized Teams request using project auth token for project ${teamsRequest.projectId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!teamsRequest.isAuthorized) {
|
||||
logger.debug(
|
||||
`Teams user ${teamsRequest.teamsUserId} is not connected to OneUptime`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const userAuthToken: WorkspaceUserAuthToken = userAuthTokens[0]!;
|
||||
|
||||
// Get project auth token
|
||||
const projectAuthTokens: Array<WorkspaceProjectAuthToken> =
|
||||
await WorkspaceProjectAuthTokenService.findBy({
|
||||
query: {
|
||||
projectId: userAuthToken.projectId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
authToken: true,
|
||||
miscData: true,
|
||||
},
|
||||
limit: 1,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (projectAuthTokens.length === 0) {
|
||||
logger.debug(
|
||||
`No project auth token found for project ${userAuthToken.projectId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const projectAuthToken: WorkspaceProjectAuthToken = projectAuthTokens[0]!;
|
||||
|
||||
teamsRequest.userId = userAuthToken.userId;
|
||||
teamsRequest.projectId = userAuthToken.projectId;
|
||||
teamsRequest.userAuthToken = userAuthToken.authToken;
|
||||
teamsRequest.projectAuthToken = projectAuthToken.authToken;
|
||||
teamsRequest.isAuthorized = true;
|
||||
|
||||
logger.debug(
|
||||
`Authorized Teams request for user ${teamsRequest.userId} in project ${teamsRequest.projectId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error authorizing Teams request: ${(error as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
396
Common/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts
Normal file
396
Common/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
import BadDataException from "../../../../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../../../../Types/ObjectID";
|
||||
import IncidentService from "../../../../Services/IncidentService";
|
||||
import { ExpressRequest, ExpressResponse } from "../../../Express";
|
||||
import MicrosoftTeamsActionType from "./ActionTypes";
|
||||
import { MicrosoftTeamsAction, MicrosoftTeamsRequest } from "./Auth";
|
||||
import Response from "../../../Response";
|
||||
import logger from "../../../Logger";
|
||||
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
||||
|
||||
export default class MicrosoftTeamsIncidentActions {
|
||||
@CaptureSpan()
|
||||
public static isIncidentAction(data: {
|
||||
actionType: MicrosoftTeamsActionType;
|
||||
}): boolean {
|
||||
const { actionType } = data;
|
||||
|
||||
switch (actionType) {
|
||||
case MicrosoftTeamsActionType.AcknowledgeIncident:
|
||||
case MicrosoftTeamsActionType.ResolveIncident:
|
||||
case MicrosoftTeamsActionType.ViewAddIncidentNote:
|
||||
case MicrosoftTeamsActionType.SubmitIncidentNote:
|
||||
case MicrosoftTeamsActionType.ViewChangeIncidentState:
|
||||
case MicrosoftTeamsActionType.SubmitChangeIncidentState:
|
||||
case MicrosoftTeamsActionType.ViewExecuteIncidentOnCallPolicy:
|
||||
case MicrosoftTeamsActionType.SubmitExecuteIncidentOnCallPolicy:
|
||||
case MicrosoftTeamsActionType.ViewIncident:
|
||||
case MicrosoftTeamsActionType.NewIncident:
|
||||
case MicrosoftTeamsActionType.SubmitNewIncident:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async acknowledgeIncident(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { req, res } = data;
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Incident ID"),
|
||||
);
|
||||
}
|
||||
|
||||
// Send early response to Teams
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
const incidentId: ObjectID = new ObjectID(actionValue);
|
||||
|
||||
try {
|
||||
// Check if already acknowledged
|
||||
const isAlreadyAcknowledged: boolean =
|
||||
await IncidentService.isIncidentAcknowledged({
|
||||
incidentId: incidentId,
|
||||
});
|
||||
|
||||
if (isAlreadyAcknowledged) {
|
||||
logger.debug("Incident is already acknowledged");
|
||||
return;
|
||||
}
|
||||
|
||||
// Acknowledge the incident
|
||||
await IncidentService.acknowledgeIncident(
|
||||
incidentId,
|
||||
data.teamsRequest.userId!,
|
||||
);
|
||||
|
||||
logger.debug("Incident acknowledged successfully via Microsoft Teams");
|
||||
} catch (error) {
|
||||
logger.error("Error acknowledging incident via Microsoft Teams");
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async resolveIncident(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { req, res } = data;
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Incident ID"),
|
||||
);
|
||||
}
|
||||
|
||||
// Send early response to Teams
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
const incidentId: ObjectID = new ObjectID(actionValue);
|
||||
|
||||
try {
|
||||
// Check if already resolved
|
||||
const isAlreadyResolved: boolean =
|
||||
await IncidentService.isIncidentResolved({
|
||||
incidentId: incidentId,
|
||||
});
|
||||
|
||||
if (isAlreadyResolved) {
|
||||
logger.debug("Incident is already resolved");
|
||||
return;
|
||||
}
|
||||
|
||||
await IncidentService.resolveIncident(
|
||||
incidentId,
|
||||
data.teamsRequest.userId!,
|
||||
);
|
||||
|
||||
logger.debug("Incident resolved successfully via Microsoft Teams");
|
||||
} catch (error) {
|
||||
logger.error("Error resolving incident via Microsoft Teams");
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async viewAddIncidentNote(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Placeholder implementation - would show note addition modal
|
||||
logger.debug(
|
||||
"Microsoft Teams view add incident note not yet fully implemented",
|
||||
);
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async submitIncidentNote(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Placeholder implementation - would submit note to incident
|
||||
logger.debug(
|
||||
"Microsoft Teams submit incident note not yet fully implemented",
|
||||
);
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async viewChangeIncidentState(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Placeholder implementation - would show state change modal
|
||||
logger.debug(
|
||||
"Microsoft Teams view change incident state not yet fully implemented",
|
||||
);
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async submitChangeIncidentState(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Placeholder implementation - would submit state change
|
||||
logger.debug(
|
||||
"Microsoft Teams submit change incident state not yet fully implemented",
|
||||
);
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async viewExecuteOnCallPolicy(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Placeholder implementation - would show on-call policy execution modal
|
||||
logger.debug(
|
||||
"Microsoft Teams view execute on-call policy not yet fully implemented",
|
||||
);
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async executeOnCallPolicy(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Placeholder implementation - would execute on-call policy
|
||||
logger.debug(
|
||||
"Microsoft Teams execute on-call policy not yet fully implemented",
|
||||
);
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async viewIncident(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Placeholder implementation - would show incident details
|
||||
logger.debug("Microsoft Teams view incident not yet fully implemented");
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async viewNewIncidentModal(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Placeholder implementation - would show new incident creation modal
|
||||
logger.debug(
|
||||
"Microsoft Teams view new incident modal not yet fully implemented",
|
||||
);
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async submitNewIncident(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Placeholder implementation - would create new incident
|
||||
logger.debug(
|
||||
"Microsoft Teams submit new incident not yet fully implemented",
|
||||
);
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async executeAction(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { teamsRequest, req, res } = data;
|
||||
const { userId, projectAuthToken } = teamsRequest;
|
||||
|
||||
if (!projectAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Project Auth Token"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid User ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
data.action.actionType === MicrosoftTeamsActionType.AcknowledgeIncident
|
||||
) {
|
||||
await this.acknowledgeIncident({
|
||||
teamsRequest,
|
||||
action: data.action,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
} else if (
|
||||
data.action.actionType === MicrosoftTeamsActionType.ResolveIncident
|
||||
) {
|
||||
await this.resolveIncident({
|
||||
teamsRequest,
|
||||
action: data.action,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
} else if (
|
||||
data.action.actionType === MicrosoftTeamsActionType.ViewAddIncidentNote
|
||||
) {
|
||||
await this.viewAddIncidentNote({
|
||||
teamsRequest,
|
||||
action: data.action,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
} else if (
|
||||
data.action.actionType === MicrosoftTeamsActionType.SubmitIncidentNote
|
||||
) {
|
||||
await this.submitIncidentNote({
|
||||
teamsRequest,
|
||||
action: data.action,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
} else if (
|
||||
data.action.actionType ===
|
||||
MicrosoftTeamsActionType.ViewChangeIncidentState
|
||||
) {
|
||||
await this.viewChangeIncidentState({
|
||||
teamsRequest,
|
||||
action: data.action,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
} else if (
|
||||
data.action.actionType ===
|
||||
MicrosoftTeamsActionType.SubmitChangeIncidentState
|
||||
) {
|
||||
await this.submitChangeIncidentState({
|
||||
teamsRequest,
|
||||
action: data.action,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
} else if (
|
||||
data.action.actionType ===
|
||||
MicrosoftTeamsActionType.ViewExecuteIncidentOnCallPolicy
|
||||
) {
|
||||
await this.viewExecuteOnCallPolicy({
|
||||
teamsRequest,
|
||||
action: data.action,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
} else if (
|
||||
data.action.actionType ===
|
||||
MicrosoftTeamsActionType.SubmitExecuteIncidentOnCallPolicy
|
||||
) {
|
||||
await this.executeOnCallPolicy({
|
||||
teamsRequest,
|
||||
action: data.action,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
} else if (
|
||||
data.action.actionType === MicrosoftTeamsActionType.ViewIncident
|
||||
) {
|
||||
await this.viewIncident({
|
||||
teamsRequest,
|
||||
action: data.action,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
} else if (
|
||||
data.action.actionType === MicrosoftTeamsActionType.NewIncident
|
||||
) {
|
||||
await this.viewNewIncidentModal({
|
||||
teamsRequest,
|
||||
action: data.action,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
} else if (
|
||||
data.action.actionType === MicrosoftTeamsActionType.SubmitNewIncident
|
||||
) {
|
||||
await this.submitNewIncident({
|
||||
teamsRequest,
|
||||
action: data.action,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
} else {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Action Type"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import BadDataException from "../../../../../Types/Exception/BadDataException";
|
||||
import { ExpressRequest, ExpressResponse } from "../../../Express";
|
||||
import MicrosoftTeamsActionType from "./ActionTypes";
|
||||
import { MicrosoftTeamsAction, MicrosoftTeamsRequest } from "./Auth";
|
||||
import Response from "../../../Response";
|
||||
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
||||
|
||||
export default class MicrosoftTeamsMonitorActions {
|
||||
@CaptureSpan()
|
||||
public static isMonitorAction(data: {
|
||||
actionType: MicrosoftTeamsActionType;
|
||||
}): boolean {
|
||||
const { actionType } = data;
|
||||
|
||||
switch (actionType) {
|
||||
case MicrosoftTeamsActionType.ViewMonitor:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async handleMonitorAction(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Project is authorized and user is authorized. Perform actions based on action type.
|
||||
const actionType: MicrosoftTeamsActionType | undefined =
|
||||
data.action.actionType;
|
||||
|
||||
if (actionType === MicrosoftTeamsActionType.ViewMonitor) {
|
||||
// Do nothing. This is just a view Monitor action.
|
||||
// Clear response.
|
||||
return Response.sendJsonObjectResponse(data.req, data.res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
}
|
||||
|
||||
// Invalid action type.
|
||||
return Response.sendErrorResponse(
|
||||
data.req,
|
||||
data.res,
|
||||
new BadDataException("Invalid Action Type"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import BadDataException from "../../../../../Types/Exception/BadDataException";
|
||||
import { ExpressRequest, ExpressResponse } from "../../../Express";
|
||||
import MicrosoftTeamsActionType from "./ActionTypes";
|
||||
import { MicrosoftTeamsAction, MicrosoftTeamsRequest } from "./Auth";
|
||||
import Response from "../../../Response";
|
||||
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
||||
|
||||
export default class MicrosoftTeamsOnCallDutyActions {
|
||||
@CaptureSpan()
|
||||
public static isOnCallDutyAction(data: {
|
||||
actionType: MicrosoftTeamsActionType;
|
||||
}): boolean {
|
||||
const { actionType } = data;
|
||||
|
||||
switch (actionType) {
|
||||
case MicrosoftTeamsActionType.ViewOnCallPolicy:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async handleOnCallDutyAction(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
// Project is authorized and user is authorized. Perform actions based on action type.
|
||||
const actionType: MicrosoftTeamsActionType | undefined =
|
||||
data.action.actionType;
|
||||
|
||||
if (actionType === MicrosoftTeamsActionType.ViewOnCallPolicy) {
|
||||
// Do nothing. This is just a view on-call policy action.
|
||||
// Clear response.
|
||||
return Response.sendJsonObjectResponse(data.req, data.res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
}
|
||||
|
||||
// Invalid action type.
|
||||
return Response.sendErrorResponse(
|
||||
data.req,
|
||||
data.res,
|
||||
new BadDataException("Invalid Action Type"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,704 @@
|
||||
import BadDataException from "../../../../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../../../../Types/ObjectID";
|
||||
import ScheduledMaintenanceService from "../../../../Services/ScheduledMaintenanceService";
|
||||
import { ExpressRequest, ExpressResponse } from "../../../Express";
|
||||
import MicrosoftTeamsUtil from "../MicrosoftTeams";
|
||||
import MicrosoftTeamsActionType from "./ActionTypes";
|
||||
import { MicrosoftTeamsAction, MicrosoftTeamsRequest } from "./Auth";
|
||||
import Response from "../../../Response";
|
||||
import {
|
||||
WorkspaceModalBlock,
|
||||
WorkspaceDropdownBlock,
|
||||
WorkspaceTextAreaBlock,
|
||||
WorkspaceTextBoxBlock,
|
||||
WorkspaceMessageBlock,
|
||||
WorkspacePayloadMarkdown,
|
||||
} from "../../../../../Types/Workspace/WorkspaceMessagePayload";
|
||||
import ScheduledMaintenancePublicNoteService from "../../../../Services/ScheduledMaintenancePublicNoteService";
|
||||
import ScheduledMaintenanceInternalNoteService from "../../../../Services/ScheduledMaintenanceInternalNoteService";
|
||||
import { DropdownOption } from "../../../../../UI/Components/Dropdown/Dropdown";
|
||||
import ScheduledMaintenanceState from "../../../../../Models/DatabaseModels/ScheduledMaintenanceState";
|
||||
import ScheduledMaintenanceStateService from "../../../../Services/ScheduledMaintenanceStateService";
|
||||
import AccessTokenService from "../../../../Services/AccessTokenService";
|
||||
import logger from "../../../Logger";
|
||||
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
||||
import WorkspaceType from "../../../../../Types/Workspace/WorkspaceType";
|
||||
import WorkspaceNotificationLogService from "../../../../Services/WorkspaceNotificationLogService";
|
||||
|
||||
export default class MicrosoftTeamsScheduledMaintenanceActions {
|
||||
@CaptureSpan()
|
||||
public static isScheduledMaintenanceAction(data: {
|
||||
actionType: MicrosoftTeamsActionType;
|
||||
}): boolean {
|
||||
const { actionType } = data;
|
||||
|
||||
switch (actionType) {
|
||||
case MicrosoftTeamsActionType.MarkScheduledMaintenanceAsOngoing:
|
||||
case MicrosoftTeamsActionType.MarkScheduledMaintenanceAsComplete:
|
||||
case MicrosoftTeamsActionType.ViewAddScheduledMaintenanceNote:
|
||||
case MicrosoftTeamsActionType.SubmitScheduledMaintenanceNote:
|
||||
case MicrosoftTeamsActionType.ViewChangeScheduledMaintenanceState:
|
||||
case MicrosoftTeamsActionType.SubmitChangeScheduledMaintenanceState:
|
||||
case MicrosoftTeamsActionType.ViewScheduledMaintenance:
|
||||
case MicrosoftTeamsActionType.NewScheduledMaintenance:
|
||||
case MicrosoftTeamsActionType.SubmitNewScheduledMaintenance:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async submitNewScheduledMaintenance(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { teamsRequest, req, res } = data;
|
||||
const { userId, projectAuthToken } = teamsRequest;
|
||||
|
||||
if (!userId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid User ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!projectAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Project Auth Token"),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
data.action.actionType ===
|
||||
MicrosoftTeamsActionType.SubmitNewScheduledMaintenance
|
||||
) {
|
||||
// We send this early let Teams know we're ok. We'll do the rest in the background.
|
||||
Response.sendEmptySuccessResponse(req, res);
|
||||
|
||||
try {
|
||||
// Implementation for creating new scheduled maintenance
|
||||
logger.debug(
|
||||
"Microsoft Teams scheduled maintenance submission implementation",
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async viewNewScheduledMaintenanceModal(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const blocks: Array<WorkspaceMessageBlock> = [];
|
||||
|
||||
// Send response to clear the action
|
||||
Response.sendTextResponse(data.req, data.res, "");
|
||||
|
||||
const scheduledMaintenanceTitle: WorkspaceTextBoxBlock = {
|
||||
_type: "WorkspaceTextBoxBlock",
|
||||
label: "Event Title",
|
||||
blockId: "scheduledMaintenanceTitle",
|
||||
placeholder: "Scheduled Maintenance Title",
|
||||
initialValue: data.action.actionValue || "",
|
||||
};
|
||||
|
||||
blocks.push(scheduledMaintenanceTitle);
|
||||
|
||||
const scheduledMaintenanceDescription: WorkspaceTextAreaBlock = {
|
||||
_type: "WorkspaceTextAreaBlock",
|
||||
label: "Event Description",
|
||||
blockId: "scheduledMaintenanceDescription",
|
||||
placeholder: "Scheduled Maintenance Description",
|
||||
};
|
||||
|
||||
blocks.push(scheduledMaintenanceDescription);
|
||||
|
||||
const modalBlock: WorkspaceModalBlock = {
|
||||
_type: "WorkspaceModalBlock",
|
||||
title: "Create Scheduled Maintenance",
|
||||
submitButtonTitle: "Create",
|
||||
cancelButtonTitle: "Cancel",
|
||||
actionId: MicrosoftTeamsActionType.SubmitNewScheduledMaintenance,
|
||||
actionValue: "",
|
||||
blocks: blocks,
|
||||
};
|
||||
|
||||
await MicrosoftTeamsUtil.showModalToUser({
|
||||
authToken: data.teamsRequest.projectAuthToken!,
|
||||
modalBlock: modalBlock,
|
||||
triggerId: data.teamsRequest.triggerId!,
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async markScheduledMaintenanceAsOngoing(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { teamsRequest, req, res } = data;
|
||||
const { userId, projectAuthToken, teamsUsername } = teamsRequest;
|
||||
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Scheduled Maintenance ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid User ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!projectAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Project Auth Token"),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
data.action.actionType ===
|
||||
MicrosoftTeamsActionType.MarkScheduledMaintenanceAsOngoing
|
||||
) {
|
||||
const scheduledMaintenanceId: ObjectID = new ObjectID(actionValue);
|
||||
|
||||
// Send early response to Teams to acknowledge the action
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
const isAlreadyOngoing: boolean =
|
||||
await ScheduledMaintenanceService.isScheduledMaintenanceOngoing({
|
||||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
});
|
||||
|
||||
if (isAlreadyOngoing) {
|
||||
const scheduledMaintenanceNumber: number | null =
|
||||
await ScheduledMaintenanceService.getScheduledMaintenanceNumber({
|
||||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
});
|
||||
|
||||
// Send a direct message to the user that the maintenance is already ongoing
|
||||
const markdownPayload: WorkspacePayloadMarkdown = {
|
||||
_type: "WorkspacePayloadMarkdown",
|
||||
text: `@${teamsUsername}, unfortunately you cannot change the state to ongoing because the **[Scheduled Maintenance ${scheduledMaintenanceNumber?.toString()}](${await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(teamsRequest.projectId!, scheduledMaintenanceId)})** is already in ongoing state.`,
|
||||
};
|
||||
|
||||
await MicrosoftTeamsUtil.sendDirectMessageToUser({
|
||||
messageBlocks: [markdownPayload],
|
||||
authToken: projectAuthToken,
|
||||
workspaceUserId: teamsRequest.teamsUserId!,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ScheduledMaintenanceService.markScheduledMaintenanceAsOngoing(
|
||||
scheduledMaintenanceId,
|
||||
userId,
|
||||
);
|
||||
|
||||
// Log the button interaction
|
||||
if (teamsRequest.projectId) {
|
||||
try {
|
||||
const logData: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
channelId?: string;
|
||||
userId: ObjectID;
|
||||
buttonAction: string;
|
||||
scheduledMaintenanceId?: ObjectID;
|
||||
} = {
|
||||
projectId: teamsRequest.projectId,
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
userId: userId,
|
||||
buttonAction: "mark_scheduled_maintenance_as_ongoing",
|
||||
};
|
||||
|
||||
if (teamsRequest.teamsChannelId) {
|
||||
logData.channelId = teamsRequest.teamsChannelId;
|
||||
}
|
||||
logData.scheduledMaintenanceId = scheduledMaintenanceId;
|
||||
|
||||
await WorkspaceNotificationLogService.logButtonPressed(logData, {
|
||||
isRoot: true,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Error logging button interaction:");
|
||||
logger.error(err);
|
||||
// Don't throw the error, just log it so the main flow continues
|
||||
}
|
||||
}
|
||||
|
||||
// Scheduled Maintenance Feed will send a message to the channel that the maintenance has been marked as ongoing.
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalid action type.
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Action Type"),
|
||||
);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async resolveScheduledMaintenance(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { teamsRequest, req, res } = data;
|
||||
const { userId, projectAuthToken, teamsUsername } = teamsRequest;
|
||||
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Scheduled Maintenance ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid User ID"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!projectAuthToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Project Auth Token"),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
data.action.actionType ===
|
||||
MicrosoftTeamsActionType.MarkScheduledMaintenanceAsComplete
|
||||
) {
|
||||
const scheduledMaintenanceId: ObjectID = new ObjectID(actionValue);
|
||||
|
||||
// Send early response to Teams to acknowledge the action
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
const isAlreadyResolved: boolean =
|
||||
await ScheduledMaintenanceService.isScheduledMaintenanceCompleted({
|
||||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
});
|
||||
|
||||
if (isAlreadyResolved) {
|
||||
const scheduledMaintenanceNumber: number | null =
|
||||
await ScheduledMaintenanceService.getScheduledMaintenanceNumber({
|
||||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
});
|
||||
|
||||
// Send a direct message to the user that the maintenance is already resolved
|
||||
const markdownPayload: WorkspacePayloadMarkdown = {
|
||||
_type: "WorkspacePayloadMarkdown",
|
||||
text: `@${teamsUsername}, unfortunately you cannot resolve the **[Scheduled Maintenance ${scheduledMaintenanceNumber?.toString()}](${await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(teamsRequest.projectId!, scheduledMaintenanceId)})**. It has already been resolved.`,
|
||||
};
|
||||
|
||||
await MicrosoftTeamsUtil.sendDirectMessageToUser({
|
||||
messageBlocks: [markdownPayload],
|
||||
authToken: projectAuthToken,
|
||||
workspaceUserId: teamsRequest.teamsUserId!,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ScheduledMaintenanceService.markScheduledMaintenanceAsComplete(
|
||||
scheduledMaintenanceId,
|
||||
userId,
|
||||
);
|
||||
|
||||
// Log the button interaction
|
||||
if (teamsRequest.projectId) {
|
||||
try {
|
||||
const logData: {
|
||||
projectId: ObjectID;
|
||||
workspaceType: WorkspaceType;
|
||||
channelId?: string;
|
||||
userId: ObjectID;
|
||||
buttonAction: string;
|
||||
scheduledMaintenanceId?: ObjectID;
|
||||
} = {
|
||||
projectId: teamsRequest.projectId,
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
userId: userId,
|
||||
buttonAction: "mark_scheduled_maintenance_as_complete",
|
||||
};
|
||||
|
||||
if (teamsRequest.teamsChannelId) {
|
||||
logData.channelId = teamsRequest.teamsChannelId;
|
||||
}
|
||||
logData.scheduledMaintenanceId = scheduledMaintenanceId;
|
||||
|
||||
await WorkspaceNotificationLogService.logButtonPressed(logData, {
|
||||
isRoot: true,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Error logging button interaction:");
|
||||
logger.error(err);
|
||||
// Don't throw the error, just log it so the main flow continues
|
||||
}
|
||||
}
|
||||
|
||||
// Scheduled Maintenance Feed will send a message to the channel that the maintenance has been completed.
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalid action type.
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Action Type"),
|
||||
);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async viewChangeScheduledMaintenanceState(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { req, res } = data;
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Scheduled Maintenance ID"),
|
||||
);
|
||||
}
|
||||
|
||||
// Send early response to Teams
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
const scheduledMaintenanceStates: Array<ScheduledMaintenanceState> =
|
||||
await ScheduledMaintenanceStateService.getAllScheduledMaintenanceStates({
|
||||
projectId: data.teamsRequest.projectId!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const dropdownOptions: Array<DropdownOption> = scheduledMaintenanceStates
|
||||
.map((state: ScheduledMaintenanceState) => {
|
||||
return {
|
||||
label: state.name || "",
|
||||
value: state._id?.toString() || "",
|
||||
};
|
||||
})
|
||||
.filter((option: DropdownOption) => {
|
||||
return option.label !== "" || option.value !== "";
|
||||
});
|
||||
|
||||
const statePickerDropdown: WorkspaceDropdownBlock = {
|
||||
_type: "WorkspaceDropdownBlock",
|
||||
label: "Scheduled Maintenance State",
|
||||
blockId: "scheduledMaintenanceState",
|
||||
placeholder: "Select Scheduled Maintenance State",
|
||||
options: dropdownOptions,
|
||||
};
|
||||
|
||||
const modalBlock: WorkspaceModalBlock = {
|
||||
_type: "WorkspaceModalBlock",
|
||||
title: "Change Event State",
|
||||
submitButtonTitle: "Submit",
|
||||
cancelButtonTitle: "Cancel",
|
||||
actionId: MicrosoftTeamsActionType.SubmitChangeScheduledMaintenanceState,
|
||||
actionValue: actionValue,
|
||||
blocks: [statePickerDropdown],
|
||||
};
|
||||
|
||||
await MicrosoftTeamsUtil.showModalToUser({
|
||||
authToken: data.teamsRequest.projectAuthToken!,
|
||||
modalBlock: modalBlock,
|
||||
triggerId: data.teamsRequest.triggerId!,
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async submitChangeScheduledMaintenanceState(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { req, res } = data;
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Scheduled Maintenance ID"),
|
||||
);
|
||||
}
|
||||
|
||||
// Send early response to Teams
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
if (
|
||||
!data.teamsRequest.viewValues ||
|
||||
!data.teamsRequest.viewValues["scheduledMaintenanceState"]
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid View Values"),
|
||||
);
|
||||
}
|
||||
|
||||
const scheduledMaintenanceId: ObjectID = new ObjectID(actionValue);
|
||||
const stateString: string =
|
||||
data.teamsRequest.viewValues["scheduledMaintenanceState"].toString();
|
||||
|
||||
const stateId: ObjectID = new ObjectID(stateString);
|
||||
|
||||
await ScheduledMaintenanceService.updateOneById({
|
||||
id: scheduledMaintenanceId,
|
||||
data: {
|
||||
currentScheduledMaintenanceStateId: stateId,
|
||||
},
|
||||
props:
|
||||
await AccessTokenService.getDatabaseCommonInteractionPropsByUserAndProject(
|
||||
{
|
||||
userId: data.teamsRequest.userId!,
|
||||
projectId: data.teamsRequest.projectId!,
|
||||
},
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async submitScheduledMaintenanceNote(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { req, res } = data;
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Scheduled Maintenance ID"),
|
||||
);
|
||||
}
|
||||
|
||||
// If view values is empty, then return error
|
||||
if (!data.teamsRequest.viewValues) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid View Values"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.teamsRequest.viewValues["noteType"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Note Type"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.teamsRequest.viewValues["note"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Note"),
|
||||
);
|
||||
}
|
||||
|
||||
const scheduledMaintenanceId: ObjectID = new ObjectID(actionValue);
|
||||
const note: string = data.teamsRequest.viewValues["note"].toString();
|
||||
const noteType: string =
|
||||
data.teamsRequest.viewValues["noteType"].toString();
|
||||
|
||||
if (noteType !== "public" && noteType !== "private") {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Note Type"),
|
||||
);
|
||||
}
|
||||
|
||||
// Send empty response
|
||||
Response.sendJsonObjectResponse(req, res, {
|
||||
response_action: "clear",
|
||||
});
|
||||
|
||||
// If public note then, add a note
|
||||
if (noteType === "public") {
|
||||
await ScheduledMaintenancePublicNoteService.addNote({
|
||||
scheduledMaintenanceId: scheduledMaintenanceId!,
|
||||
note: note || "",
|
||||
projectId: data.teamsRequest.projectId!,
|
||||
userId: data.teamsRequest.userId!,
|
||||
});
|
||||
}
|
||||
|
||||
// If private note then, add a note
|
||||
if (noteType === "private") {
|
||||
await ScheduledMaintenanceInternalNoteService.addNote({
|
||||
scheduledMaintenanceId: scheduledMaintenanceId!,
|
||||
note: note || "",
|
||||
projectId: data.teamsRequest.projectId!,
|
||||
userId: data.teamsRequest.userId!,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async viewAddScheduledMaintenanceNote(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { req, res } = data;
|
||||
const { actionValue } = data.action;
|
||||
|
||||
if (!actionValue) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException("Invalid Scheduled Maintenance ID"),
|
||||
);
|
||||
}
|
||||
|
||||
const scheduledMaintenanceId: ObjectID = new ObjectID(actionValue);
|
||||
|
||||
const noteTypeOptions: Array<DropdownOption> = [
|
||||
{
|
||||
label: "Public Note",
|
||||
value: "public",
|
||||
},
|
||||
{
|
||||
label: "Private Note",
|
||||
value: "private",
|
||||
},
|
||||
];
|
||||
|
||||
const noteTypeDropdown: WorkspaceDropdownBlock = {
|
||||
_type: "WorkspaceDropdownBlock",
|
||||
blockId: "noteType",
|
||||
label: "Note Type",
|
||||
placeholder: "Please select note type...",
|
||||
options: noteTypeOptions,
|
||||
};
|
||||
|
||||
const noteTextArea: WorkspaceTextAreaBlock = {
|
||||
_type: "WorkspaceTextAreaBlock",
|
||||
blockId: "note",
|
||||
label: "Note",
|
||||
placeholder: "Please add a note...",
|
||||
};
|
||||
|
||||
const modal: WorkspaceModalBlock = {
|
||||
_type: "WorkspaceModalBlock",
|
||||
title: "Add Note",
|
||||
actionId: MicrosoftTeamsActionType.SubmitScheduledMaintenanceNote,
|
||||
actionValue: scheduledMaintenanceId.toString(),
|
||||
submitButtonTitle: "Add Note",
|
||||
cancelButtonTitle: "Cancel",
|
||||
blocks: [noteTypeDropdown, noteTextArea],
|
||||
};
|
||||
|
||||
Response.sendJsonObjectResponse(req, res, modal as any);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async handleScheduledMaintenanceAction(data: {
|
||||
teamsRequest: MicrosoftTeamsRequest;
|
||||
action: MicrosoftTeamsAction;
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
}): Promise<void> {
|
||||
const { action } = data;
|
||||
|
||||
switch (action.actionType) {
|
||||
case MicrosoftTeamsActionType.MarkScheduledMaintenanceAsOngoing:
|
||||
await this.markScheduledMaintenanceAsOngoing(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.MarkScheduledMaintenanceAsComplete:
|
||||
await this.resolveScheduledMaintenance(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.ViewAddScheduledMaintenanceNote:
|
||||
await this.viewAddScheduledMaintenanceNote(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.SubmitScheduledMaintenanceNote:
|
||||
await this.submitScheduledMaintenanceNote(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.ViewChangeScheduledMaintenanceState:
|
||||
await this.viewChangeScheduledMaintenanceState(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.SubmitChangeScheduledMaintenanceState:
|
||||
await this.submitChangeScheduledMaintenanceState(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.ViewScheduledMaintenance:
|
||||
// View action doesn't need implementation as it's handled by notification display
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.NewScheduledMaintenance:
|
||||
await this.viewNewScheduledMaintenanceModal(data);
|
||||
break;
|
||||
|
||||
case MicrosoftTeamsActionType.SubmitNewScheduledMaintenance:
|
||||
await this.submitNewScheduledMaintenance(data);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.debug(
|
||||
`Unhandled Microsoft Teams scheduled maintenance action: ${action.actionType}`,
|
||||
);
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import BadDataException from "../../../../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../../../../Types/ObjectID";
|
||||
import {
|
||||
WorkspaceMessageBlock,
|
||||
WorkspaceMessagePayloadButton,
|
||||
WorkspacePayloadButtons,
|
||||
WorkspacePayloadDivider,
|
||||
} from "../../../../../Types/Workspace/WorkspaceMessagePayload";
|
||||
import AlertService from "../../../../Services/AlertService";
|
||||
import MicrosoftTeamsActionType from "../../../../Utils/Workspace/MicrosoftTeams/Actions/ActionTypes";
|
||||
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
||||
|
||||
export default class MicrosoftTeamsAlertMessages {
|
||||
@CaptureSpan()
|
||||
public static async getAlertCreateMessageBlocks(data: {
|
||||
alertId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
}): Promise<Array<WorkspaceMessageBlock>> {
|
||||
if (!data.alertId) {
|
||||
throw new BadDataException("Alert ID is required");
|
||||
}
|
||||
|
||||
// MicrosoftTeams.
|
||||
|
||||
const blockMicrosoftTeams: Array<WorkspaceMessageBlock> = [];
|
||||
|
||||
// add divider.
|
||||
|
||||
const dividerBlock: WorkspacePayloadDivider = {
|
||||
_type: "WorkspacePayloadDivider",
|
||||
};
|
||||
|
||||
blockMicrosoftTeams.push(dividerBlock);
|
||||
|
||||
// now add buttons.
|
||||
// View data.
|
||||
// Execute On Call
|
||||
// Acknowledge alert
|
||||
// Resolve data.
|
||||
// Change Alert State.
|
||||
// Add Note.
|
||||
|
||||
const buttons: Array<WorkspaceMessagePayloadButton> = [];
|
||||
|
||||
// view data.
|
||||
const viewAlertButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "🔗 View Alert",
|
||||
url: await AlertService.getAlertLinkInDashboard(
|
||||
data.projectId!,
|
||||
data.alertId!,
|
||||
),
|
||||
value: data.alertId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ViewAlert,
|
||||
};
|
||||
|
||||
buttons.push(viewAlertButton);
|
||||
|
||||
// execute on call.
|
||||
const executeOnCallButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "📞 Execute On Call",
|
||||
value: data.alertId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ViewExecuteAlertOnCallPolicy,
|
||||
};
|
||||
|
||||
buttons.push(executeOnCallButton);
|
||||
|
||||
// acknowledge data.
|
||||
const acknowledgeAlertButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "👀 Acknowledge Alert",
|
||||
value: data.alertId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.AcknowledgeAlert,
|
||||
};
|
||||
|
||||
buttons.push(acknowledgeAlertButton);
|
||||
|
||||
// resolve data.
|
||||
const resolveAlertButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "✅ Resolve Alert",
|
||||
value: data.alertId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ResolveAlert,
|
||||
};
|
||||
|
||||
buttons.push(resolveAlertButton);
|
||||
|
||||
// change alert state.
|
||||
const changeAlertStateButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "➡️ Change Alert State",
|
||||
value: data.alertId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ViewChangeAlertState,
|
||||
};
|
||||
|
||||
buttons.push(changeAlertStateButton);
|
||||
|
||||
// add note.
|
||||
const addNoteButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "📄 Add Note",
|
||||
value: data.alertId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ViewAddAlertNote,
|
||||
};
|
||||
|
||||
buttons.push(addNoteButton);
|
||||
|
||||
const workspacePayloadButtons: WorkspacePayloadButtons = {
|
||||
buttons: buttons,
|
||||
_type: "WorkspacePayloadButtons",
|
||||
};
|
||||
|
||||
blockMicrosoftTeams.push(workspacePayloadButtons);
|
||||
|
||||
return blockMicrosoftTeams;
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import BadDataException from "../../../../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../../../../Types/ObjectID";
|
||||
import {
|
||||
WorkspaceMessageBlock,
|
||||
WorkspaceMessagePayloadButton,
|
||||
WorkspacePayloadButtons,
|
||||
WorkspacePayloadDivider,
|
||||
} from "../../../../../Types/Workspace/WorkspaceMessagePayload";
|
||||
import IncidentService from "../../../../Services/IncidentService";
|
||||
import MicrosoftTeamsActionType from "../../../../Utils/Workspace/MicrosoftTeams/Actions/ActionTypes";
|
||||
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
||||
|
||||
export default class MicrosoftTeamsIncidentMessages {
|
||||
@CaptureSpan()
|
||||
public static async getIncidentCreateMessageBlocks(data: {
|
||||
incidentId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
}): Promise<Array<WorkspaceMessageBlock>> {
|
||||
if (!data.incidentId) {
|
||||
throw new BadDataException("Incident ID is required");
|
||||
}
|
||||
|
||||
// MicrosoftTeams.
|
||||
|
||||
const blockMicrosoftTeams: Array<WorkspaceMessageBlock> = [];
|
||||
|
||||
// add divider.
|
||||
|
||||
const dividerBlock: WorkspacePayloadDivider = {
|
||||
_type: "WorkspacePayloadDivider",
|
||||
};
|
||||
|
||||
blockMicrosoftTeams.push(dividerBlock);
|
||||
|
||||
// now add buttons.
|
||||
// View data.
|
||||
// Execute On Call
|
||||
// Acknowledge incident
|
||||
// Resolve data.
|
||||
// Change Incident State.
|
||||
// Add Note.
|
||||
|
||||
const buttons: Array<WorkspaceMessagePayloadButton> = [];
|
||||
|
||||
// view data.
|
||||
const viewIncidentButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "🔗 View Incident",
|
||||
url: await IncidentService.getIncidentLinkInDashboard(
|
||||
data.projectId!,
|
||||
data.incidentId!,
|
||||
),
|
||||
value: data.incidentId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ViewIncident,
|
||||
};
|
||||
|
||||
buttons.push(viewIncidentButton);
|
||||
|
||||
// execute on call.
|
||||
const executeOnCallButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "📞 Execute On Call",
|
||||
value: data.incidentId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ViewExecuteIncidentOnCallPolicy,
|
||||
};
|
||||
|
||||
buttons.push(executeOnCallButton);
|
||||
|
||||
// acknowledge data.
|
||||
const acknowledgeIncidentButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "👀 Acknowledge Incident",
|
||||
value: data.incidentId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.AcknowledgeIncident,
|
||||
};
|
||||
|
||||
buttons.push(acknowledgeIncidentButton);
|
||||
|
||||
// resolve data.
|
||||
const resolveIncidentButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "✅ Resolve Incident",
|
||||
value: data.incidentId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ResolveIncident,
|
||||
};
|
||||
|
||||
buttons.push(resolveIncidentButton);
|
||||
|
||||
// change incident state.
|
||||
const changeIncidentStateButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "➡️ Change Incident State",
|
||||
value: data.incidentId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ViewChangeIncidentState,
|
||||
};
|
||||
|
||||
buttons.push(changeIncidentStateButton);
|
||||
|
||||
// add note.
|
||||
const addNoteButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "📄 Add Note",
|
||||
value: data.incidentId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ViewAddIncidentNote,
|
||||
};
|
||||
|
||||
buttons.push(addNoteButton);
|
||||
|
||||
const workspacePayloadButtons: WorkspacePayloadButtons = {
|
||||
buttons: buttons,
|
||||
_type: "WorkspacePayloadButtons",
|
||||
};
|
||||
|
||||
blockMicrosoftTeams.push(workspacePayloadButtons);
|
||||
|
||||
return blockMicrosoftTeams;
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import BadDataException from "../../../../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../../../../Types/ObjectID";
|
||||
import {
|
||||
WorkspaceMessageBlock,
|
||||
WorkspaceMessagePayloadButton,
|
||||
WorkspacePayloadButtons,
|
||||
WorkspacePayloadDivider,
|
||||
} from "../../../../../Types/Workspace/WorkspaceMessagePayload";
|
||||
import ScheduledMaintenanceService from "../../../../Services/ScheduledMaintenanceService";
|
||||
import MicrosoftTeamsActionType from "../../../../Utils/Workspace/MicrosoftTeams/Actions/ActionTypes";
|
||||
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
||||
|
||||
export default class MicrosoftTeamsScheduledMaintenanceMessages {
|
||||
@CaptureSpan()
|
||||
public static async getScheduledMaintenanceCreateMessageBlocks(data: {
|
||||
scheduledMaintenanceId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
}): Promise<Array<WorkspaceMessageBlock>> {
|
||||
if (!data.scheduledMaintenanceId) {
|
||||
throw new BadDataException("ScheduledMaintenance ID is required");
|
||||
}
|
||||
|
||||
// MicrosoftTeams.
|
||||
|
||||
const blockMicrosoftTeams: Array<WorkspaceMessageBlock> = [];
|
||||
|
||||
// add divider.
|
||||
|
||||
const dividerBlock: WorkspacePayloadDivider = {
|
||||
_type: "WorkspacePayloadDivider",
|
||||
};
|
||||
|
||||
blockMicrosoftTeams.push(dividerBlock);
|
||||
|
||||
// now add buttons.
|
||||
// View data.
|
||||
// Execute On Call
|
||||
// Acknowledge scheduledMaintenance
|
||||
// Resolve data.
|
||||
// Change ScheduledMaintenance State.
|
||||
// Add Note.
|
||||
|
||||
const buttons: Array<WorkspaceMessagePayloadButton> = [];
|
||||
|
||||
// view data.
|
||||
const viewScheduledMaintenanceButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "🔗 View ScheduledMaintenance",
|
||||
url: await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(
|
||||
data.projectId!,
|
||||
data.scheduledMaintenanceId!,
|
||||
),
|
||||
value: data.scheduledMaintenanceId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ViewScheduledMaintenance,
|
||||
};
|
||||
|
||||
buttons.push(viewScheduledMaintenanceButton);
|
||||
|
||||
// acknowledge data.
|
||||
const acknowledgeScheduledMaintenanceButton: WorkspaceMessagePayloadButton =
|
||||
{
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "⌛ Mark as Ongoing",
|
||||
value: data.scheduledMaintenanceId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.MarkScheduledMaintenanceAsOngoing,
|
||||
};
|
||||
|
||||
buttons.push(acknowledgeScheduledMaintenanceButton);
|
||||
|
||||
// resolve data.
|
||||
const resolveScheduledMaintenanceButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "✅ Mark as Completed",
|
||||
value: data.scheduledMaintenanceId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.MarkScheduledMaintenanceAsComplete,
|
||||
};
|
||||
|
||||
buttons.push(resolveScheduledMaintenanceButton);
|
||||
|
||||
// change scheduledMaintenance state.
|
||||
const changeScheduledMaintenanceStateButton: WorkspaceMessagePayloadButton =
|
||||
{
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "➡️ Change Scheduled Maintenance State",
|
||||
value: data.scheduledMaintenanceId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ViewChangeScheduledMaintenanceState,
|
||||
};
|
||||
|
||||
buttons.push(changeScheduledMaintenanceStateButton);
|
||||
|
||||
// add note.
|
||||
const addNoteButton: WorkspaceMessagePayloadButton = {
|
||||
_type: "WorkspaceMessagePayloadButton",
|
||||
title: "📄 Add Note",
|
||||
value: data.scheduledMaintenanceId?.toString() || "",
|
||||
actionId: MicrosoftTeamsActionType.ViewAddScheduledMaintenanceNote,
|
||||
};
|
||||
|
||||
buttons.push(addNoteButton);
|
||||
|
||||
const workspacePayloadButtons: WorkspacePayloadButtons = {
|
||||
buttons: buttons,
|
||||
_type: "WorkspacePayloadButtons",
|
||||
};
|
||||
|
||||
blockMicrosoftTeams.push(workspacePayloadButtons);
|
||||
|
||||
return blockMicrosoftTeams;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,234 @@
|
||||
import WorkspaceProjectAuthToken, {
|
||||
MiscData,
|
||||
} from "../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import WorkspaceProjectAuthTokenService from "../../../Services/WorkspaceProjectAuthTokenService";
|
||||
import WorkspaceType from "../../../../Types/Workspace/WorkspaceType";
|
||||
import API from "../../../../Utils/API";
|
||||
import URL from "../../../../Types/API/URL";
|
||||
import HTTPResponse from "../../../../Types/API/HTTPResponse";
|
||||
import HTTPErrorResponse from "../../../../Types/API/HTTPErrorResponse";
|
||||
import { JSONObject } from "../../../../Types/JSON";
|
||||
import logger from "../../Logger";
|
||||
import {
|
||||
MicrosoftTeamsAppClientId,
|
||||
MicrosoftTeamsAppClientSecret,
|
||||
} from "../../../EnvironmentConfig";
|
||||
import ObjectID from "../../../../Types/ObjectID";
|
||||
|
||||
// Re-declare with optional fields aligned to runtime data; all values stored as strings in MiscData
|
||||
export interface MicrosoftTeamsMiscData extends MiscData {
|
||||
refreshToken?: string; // inherited index signature requires string values
|
||||
tokenExpiresAt?: string; // ISO string
|
||||
tenantId?: string;
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
userId?: string; // installing user id
|
||||
}
|
||||
|
||||
export default class MicrosoftTeamsTokenRefresher {
|
||||
public static async refreshProjectAuthTokenIfExpired(data: {
|
||||
projectAuthToken: WorkspaceProjectAuthToken;
|
||||
}): Promise<WorkspaceProjectAuthToken> {
|
||||
const projectAuthToken: WorkspaceProjectAuthToken = data.projectAuthToken;
|
||||
|
||||
try {
|
||||
logger.debug(
|
||||
`Starting token refresh check for project auth token: ${projectAuthToken.id}`,
|
||||
);
|
||||
|
||||
if (projectAuthToken.workspaceType !== WorkspaceType.MicrosoftTeams) {
|
||||
logger.debug(
|
||||
"Project auth token is not for Microsoft Teams, skipping refresh",
|
||||
);
|
||||
return projectAuthToken;
|
||||
}
|
||||
|
||||
const miscData: MicrosoftTeamsMiscData | undefined =
|
||||
projectAuthToken.miscData as MicrosoftTeamsMiscData | undefined;
|
||||
if (!miscData) {
|
||||
logger.debug(
|
||||
"No misc data found in project auth token, cannot refresh",
|
||||
);
|
||||
return projectAuthToken;
|
||||
}
|
||||
|
||||
logger.debug("Misc data analysis:");
|
||||
logger.debug({
|
||||
hasTokenExpiresAt: Boolean(miscData.tokenExpiresAt),
|
||||
hasRefreshToken: Boolean(miscData.refreshToken),
|
||||
tokenExpiresAt: miscData.tokenExpiresAt,
|
||||
refreshTokenLength: miscData.refreshToken?.length || 0,
|
||||
tenantId: miscData.tenantId,
|
||||
teamId: miscData.teamId,
|
||||
});
|
||||
|
||||
const expiresAt: string | undefined = miscData.tokenExpiresAt;
|
||||
const refreshToken: string | undefined = miscData.refreshToken;
|
||||
|
||||
if (!expiresAt || !refreshToken) {
|
||||
logger.debug(
|
||||
"Missing tokenExpiresAt or refreshToken, cannot refresh token",
|
||||
);
|
||||
logger.debug({
|
||||
hasExpiresAt: Boolean(expiresAt),
|
||||
hasRefreshToken: Boolean(refreshToken),
|
||||
});
|
||||
return projectAuthToken;
|
||||
}
|
||||
|
||||
const bufferMs: number = 2 * 60 * 1000; // 2 minutes buffer
|
||||
const expiresDate: Date = new Date(expiresAt);
|
||||
const now: Date = new Date();
|
||||
|
||||
logger.debug("Token expiry check:");
|
||||
logger.debug({
|
||||
expiresAt: expiresAt,
|
||||
expiresDateMs: expiresDate.getTime(),
|
||||
nowMs: now.getTime(),
|
||||
bufferMs: bufferMs,
|
||||
timeUntilExpiry: expiresDate.getTime() - now.getTime(),
|
||||
needsRefresh: expiresDate.getTime() - bufferMs <= now.getTime(),
|
||||
});
|
||||
|
||||
if (expiresDate.getTime() - bufferMs > now.getTime()) {
|
||||
logger.debug("Token is still valid, no refresh needed");
|
||||
return projectAuthToken; // Still valid
|
||||
}
|
||||
|
||||
if (!MicrosoftTeamsAppClientId || !MicrosoftTeamsAppClientSecret) {
|
||||
logger.error(
|
||||
"Microsoft Teams client credentials not set. Cannot refresh token.",
|
||||
);
|
||||
return projectAuthToken;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
"Refreshing Microsoft Teams access token for project auth token " +
|
||||
projectAuthToken.id,
|
||||
);
|
||||
logger.debug("Refresh request details:");
|
||||
logger.debug({
|
||||
refreshTokenLength: refreshToken.length,
|
||||
clientIdProvided: Boolean(MicrosoftTeamsAppClientId),
|
||||
clientSecretProvided: Boolean(MicrosoftTeamsAppClientSecret),
|
||||
});
|
||||
|
||||
const resp: HTTPErrorResponse | HTTPResponse<JSONObject> = await API.post(
|
||||
URL.fromString(
|
||||
"https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||
),
|
||||
{
|
||||
client_id: MicrosoftTeamsAppClientId,
|
||||
client_secret: MicrosoftTeamsAppClientSecret,
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
},
|
||||
{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
);
|
||||
|
||||
if (resp instanceof HTTPErrorResponse) {
|
||||
logger.error("Microsoft Teams token refresh failed: " + resp.message);
|
||||
logger.error("Refresh error details:");
|
||||
logger.error({
|
||||
statusCode: resp.statusCode,
|
||||
message: resp.message,
|
||||
data: resp.data,
|
||||
jsonData: resp.jsonData,
|
||||
});
|
||||
|
||||
// Handle specific client secret error during refresh
|
||||
if (
|
||||
resp.jsonData &&
|
||||
typeof resp.jsonData === "object" &&
|
||||
"error" in resp.jsonData
|
||||
) {
|
||||
const errorData: JSONObject = resp.jsonData as JSONObject;
|
||||
const errorType: string = errorData["error"] as string;
|
||||
const errorDescription: string = errorData[
|
||||
"error_description"
|
||||
] as string;
|
||||
|
||||
if (
|
||||
errorType === "invalid_client" &&
|
||||
errorDescription?.includes("Invalid client secret provided")
|
||||
) {
|
||||
logger.error(
|
||||
"ERROR: Invalid Microsoft Teams client secret detected during token refresh!",
|
||||
);
|
||||
logger.error(
|
||||
"Please ensure you are using the SECRET VALUE (not Secret ID) from your Azure App Registration.",
|
||||
);
|
||||
logger.error(
|
||||
"Go to Azure Portal > App Registrations > Your App > Certificates & secrets > Client secrets",
|
||||
);
|
||||
logger.error(
|
||||
"Copy the full SECRET VALUE and update MICROSOFT_TEAMS_APP_CLIENT_SECRET",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return projectAuthToken;
|
||||
}
|
||||
|
||||
const json: JSONObject = resp.jsonData as JSONObject;
|
||||
const newAccessToken: string | undefined = json["access_token"] as string;
|
||||
const newRefreshToken: string | undefined =
|
||||
(json["refresh_token"] as string) || refreshToken;
|
||||
const expiresIn: number | undefined = json["expires_in"] as number;
|
||||
|
||||
logger.debug("Token refresh response:");
|
||||
logger.debug({
|
||||
hasNewAccessToken: Boolean(newAccessToken),
|
||||
hasNewRefreshToken: Boolean(newRefreshToken),
|
||||
newAccessTokenLength: newAccessToken?.length || 0,
|
||||
newRefreshTokenLength: newRefreshToken?.length || 0,
|
||||
expiresIn: expiresIn,
|
||||
});
|
||||
|
||||
if (!newAccessToken) {
|
||||
logger.error(
|
||||
"Microsoft Teams token refresh response missing access_token",
|
||||
);
|
||||
return projectAuthToken;
|
||||
}
|
||||
|
||||
const newExpiryIso: string | undefined = expiresIn
|
||||
? new Date(Date.now() + (expiresIn - 60) * 1000).toISOString()
|
||||
: miscData.tokenExpiresAt;
|
||||
|
||||
const updatedMisc: MicrosoftTeamsMiscData = {
|
||||
...miscData,
|
||||
refreshToken: newRefreshToken || miscData.refreshToken || "",
|
||||
tokenExpiresAt: newExpiryIso || miscData.tokenExpiresAt || "",
|
||||
};
|
||||
|
||||
logger.debug("Updating project auth token with new credentials");
|
||||
|
||||
await WorkspaceProjectAuthTokenService.refreshAuthToken({
|
||||
projectId: projectAuthToken.projectId as ObjectID,
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
authToken: newAccessToken,
|
||||
workspaceProjectId: projectAuthToken.workspaceProjectId || "",
|
||||
miscData: updatedMisc,
|
||||
});
|
||||
|
||||
projectAuthToken.authToken = newAccessToken;
|
||||
projectAuthToken.miscData = updatedMisc;
|
||||
|
||||
logger.debug(
|
||||
"Microsoft Teams access token refreshed successfully for project auth token " +
|
||||
projectAuthToken.id,
|
||||
);
|
||||
|
||||
return projectAuthToken;
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"Error refreshing Microsoft Teams token: " + (err as Error).message,
|
||||
);
|
||||
logger.error(err);
|
||||
return data.projectAuthToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import WorkspaceProjectAuthTokenService from "../../Services/WorkspaceProjectAut
|
||||
import WorkspaceProjectAuthToken, {
|
||||
SlackMiscData,
|
||||
} from "../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
|
||||
import MicrosoftTeamsTokenRefresher from "./MicrosoftTeams/MicrosoftTeamsTokenRefresher";
|
||||
import { MessageBlocksByWorkspaceType } from "../../Services/WorkspaceNotificationRuleService";
|
||||
import WorkspaceUserAuthToken from "../../../Models/DatabaseModels/WorkspaceUserAuthToken";
|
||||
import WorkspaceUserAuthTokenService from "../../Services/WorkspaceUserAuthTokenService";
|
||||
@@ -150,7 +151,7 @@ export default class WorkspaceUtil {
|
||||
const responses: Array<WorkspaceSendMessageResponse> = [];
|
||||
|
||||
for (const messagePayloadByWorkspace of data.messagePayloadsByWorkspace) {
|
||||
const projectAuthToken: WorkspaceProjectAuthToken | null =
|
||||
let projectAuthToken: WorkspaceProjectAuthToken | null =
|
||||
await WorkspaceProjectAuthTokenService.getProjectAuth({
|
||||
projectId: data.projectId,
|
||||
workspaceType: messagePayloadByWorkspace.workspaceType,
|
||||
@@ -168,18 +169,25 @@ export default class WorkspaceUtil {
|
||||
const workspaceType: WorkspaceType =
|
||||
messagePayloadByWorkspace.workspaceType;
|
||||
|
||||
if (projectAuthToken && workspaceType === WorkspaceType.MicrosoftTeams) {
|
||||
// Refresh token if expired (Teams specific)
|
||||
projectAuthToken =
|
||||
await MicrosoftTeamsTokenRefresher.refreshProjectAuthTokenIfExpired({
|
||||
projectAuthToken,
|
||||
});
|
||||
}
|
||||
|
||||
let botUserId: string | undefined = undefined;
|
||||
|
||||
if (workspaceType === WorkspaceType.Slack) {
|
||||
botUserId = (projectAuthToken.miscData as SlackMiscData).botUserId;
|
||||
}
|
||||
|
||||
if (!botUserId) {
|
||||
responses.push({
|
||||
workspaceType: workspaceType,
|
||||
threads: [],
|
||||
});
|
||||
continue;
|
||||
if (!botUserId) {
|
||||
responses.push({
|
||||
workspaceType: workspaceType,
|
||||
threads: [],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectAuthToken.authToken) {
|
||||
@@ -192,7 +200,7 @@ export default class WorkspaceUtil {
|
||||
|
||||
const result: WorkspaceSendMessageResponse =
|
||||
await WorkspaceUtil.getWorkspaceTypeUtil(workspaceType).sendMessage({
|
||||
userId: botUserId,
|
||||
userId: botUserId || projectAuthToken.workspaceProjectId || "",
|
||||
authToken: projectAuthToken.authToken,
|
||||
projectId: data.projectId,
|
||||
workspaceMessagePayload: messagePayloadByWorkspace,
|
||||
|
||||
@@ -11,12 +11,33 @@ export default class Exception extends Error {
|
||||
this._code = value;
|
||||
}
|
||||
|
||||
public constructor(code: ExceptionCode, message: string) {
|
||||
super(message);
|
||||
public constructor(code: ExceptionCode, message: unknown) {
|
||||
super(Exception.formatMessage(message));
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public getMessage(): string {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
// Normalizes unknown message types to a string to avoid `[object Object]` in API responses.
|
||||
private static formatMessage(message: unknown): string {
|
||||
if (message === undefined || message === null) {
|
||||
return "An error occurred"; // generic fallback
|
||||
}
|
||||
if (typeof message === "string") {
|
||||
return message;
|
||||
}
|
||||
if (message instanceof Error) {
|
||||
const base: string = message.message || message.toString();
|
||||
return message.name && !base.startsWith(message.name)
|
||||
? `${message.name}: ${base}`
|
||||
: base;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(message);
|
||||
} catch {
|
||||
return Object.prototype.toString.call(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,3 +246,7 @@ export const DisableTelemetry: boolean = env("DISABLE_TELEMETRY") === "true";
|
||||
|
||||
export const SlackAppClientId: string | null =
|
||||
env("SLACK_APP_CLIENT_ID") || null;
|
||||
|
||||
// Microsoft Teams (Azure AD App) UI config
|
||||
export const MicrosoftTeamsAppClientId: string | null =
|
||||
env("MICROSOFT_TEAMS_APP_CLIENT_ID") || null;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"watch": ["./","../Common/UI", "../Common/Types", "../Common/Utils", "../Common/Models"],
|
||||
"watch": [
|
||||
"./",
|
||||
"../Common/UI",
|
||||
"../Common/Types",
|
||||
"../Common/Utils",
|
||||
"../Common/Models"
|
||||
],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"ignore": [
|
||||
"./public/**",
|
||||
@@ -10,5 +16,10 @@
|
||||
"./build/dist/**",
|
||||
"../Common/Server/**"
|
||||
],
|
||||
"exec": " npm run dev-build && npm run start"
|
||||
"exec": "sh -c 'npm run dev-build && node esbuild.config.js --watch & npm run start'",
|
||||
"delay": "200ms",
|
||||
"legacyWatch": true,
|
||||
"env": {
|
||||
"FORCE_COLOR": "1"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"generate-sw": "node scripts/generate-sw.js",
|
||||
"dev-build": "npm run generate-sw && NODE_ENV=development node esbuild.config.js",
|
||||
"dev-build": "npm run generate-sw",
|
||||
"dev": "npx nodemon",
|
||||
"build": "npm run generate-sw && NODE_ENV=production node esbuild.config.js",
|
||||
"analyze": "npm run generate-sw && analyze=true NODE_ENV=production node esbuild.config.js",
|
||||
@@ -32,7 +32,7 @@
|
||||
"dependencies": {
|
||||
"@stripe/react-stripe-js": "^1.15.0",
|
||||
"@stripe/stripe-js": "^1.44.1",
|
||||
"Common": "file:../Common",
|
||||
"Common": "link:../Common",
|
||||
|
||||
"ejs": "^3.1.10",
|
||||
"react": "^18.3.1",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/MarkdownViewer";
|
||||
|
||||
export interface ComponentProps {
|
||||
manifest: any;
|
||||
}
|
||||
|
||||
const MicrosoftTeamsIntegrationDocumentation: FunctionComponent<
|
||||
ComponentProps
|
||||
> = (_props: ComponentProps): ReactElement => {
|
||||
const markdownText: string = `
|
||||
#### Setting up Microsoft Teams Integration with OneUptime
|
||||
|
||||
Microsoft Teams is not connected to OneUptime. Here are the steps you need to follow to integrate Microsoft Teams with your OneUptime Project:
|
||||
|
||||
##### Step 1: Create an Azure AD Application
|
||||
|
||||
1. Go to the [Azure Portal](https://portal.azure.com)
|
||||
2. Navigate to **App registrations**
|
||||
3. Click **New registration**
|
||||
4. Fill in the application details:
|
||||
- **Name**: OneUptime Integration
|
||||
- **Supported account types**: Accounts in any organizational directory (Any Azure AD directory - Multitenant)
|
||||
- **Redirect URI**: Please add these URI's
|
||||
- \`${window.location.origin}/api/teams/auth\`
|
||||
- \`${window.location.origin}/api/teams/admin-consent\`
|
||||
5. Click **Register**
|
||||
|
||||
##### Step 2: Configure App Permissions (Delegated + Application)
|
||||
|
||||
We use two permission models:
|
||||
- **Delegated permissions (required)**: Used when a user signs in (interactive OAuth). Enables discovering teams/channels with the installing user's context and (optionally) managing membership.
|
||||
- **Application permissions (for bot-style posting)**: Used to post as the app (client credentials). Without these, messages will fall back to posting as the installing user.
|
||||
|
||||
1. In your app registration, go to **API permissions**
|
||||
2. Click **Add a permission** → **Microsoft Graph**
|
||||
3. Add the following **Delegated permissions** (required):
|
||||
- \`openid\` (returns an id_token so we can read tenant id)
|
||||
- \`profile\` (basic user profile claims)
|
||||
- \`offline_access\` (required for refresh tokens)
|
||||
- \`email\` (view users' email address)
|
||||
- \`User.Read\` (basic profile / required by most sign-ins)
|
||||
- \`Team.ReadBasic.All\` (read the names and descriptions of teams)
|
||||
- \`Channel.ReadBasic.All\` (read the names and descriptions of channels)
|
||||
- \`ChannelMessage.Send\` (send channel messages)
|
||||
- \`TeamMember.ReadWrite.All\` (add and remove members from teams)
|
||||
- \`Teamwork.Read.All\` (read organizational teamwork settings)
|
||||
4. Add the following **Application permissions** (required for bot functionality):
|
||||
- \`Channel.Create\` (create channels)
|
||||
- \`Channel.Delete.All\` (delete channels)
|
||||
- \`Channel.ReadBasic.All\` (read the names and descriptions of all channels)
|
||||
- \`ChannelMember.Read.All\` (read the members of all channels)
|
||||
- \`ChannelMember.ReadWrite.All\` (add and remove members from all channels)
|
||||
- \`ChannelMessage.Read.All\` (read all channel messages)
|
||||
- \`ChatMessage.Read.All\` (read all chat messages)
|
||||
- \`Team.ReadBasic.All\` (get a list of all teams)
|
||||
- \`TeamMember.Read.All\` (read the members of all teams)
|
||||
- \`TeamMember.ReadWrite.All\` (add and remove members from all teams)
|
||||
- \`Teamwork.Migrate.All\` (create chat and channel messages with anyone's identity and with any timestamp)
|
||||
- \`Teamwork.Read.All\` (read organizational teamwork settings)
|
||||
6. Click **Add permissions**
|
||||
7. Click **Grant admin consent** for your organization (tenant admin required)
|
||||
8. Verify all granted Application permissions show a green check mark
|
||||
|
||||
##### Step 3: Get Application Credentials
|
||||
|
||||
1. Go to **Certificates & secrets**
|
||||
2. Click **New client secret**
|
||||
3. Add a description and select expiration
|
||||
4. Copy the **Value** (this is your Client Secret)
|
||||
5. Go to **Overview** and copy the **Application (client) ID**
|
||||
|
||||
##### Step 4: Configure OneUptime Environment Variables
|
||||
|
||||
Add these environment variables to your OneUptime configuration:
|
||||
|
||||
\`\`\`text
|
||||
MICROSOFT_TEAMS_APP_CLIENT_ID=YOUR_APPLICATION_CLIENT_ID
|
||||
MICROSOFT_TEAMS_APP_CLIENT_SECRET=YOUR_CLIENT_SECRET
|
||||
\`\`\`
|
||||
|
||||
If you are using Kubernetes with Helm, add these to your \`values.yaml\` file:
|
||||
|
||||
\`\`\`yaml
|
||||
microsoftTeamsApp:
|
||||
clientId: YOUR_APPLICATION_CLIENT_ID
|
||||
clientSecret: YOUR_CLIENT_SECRET
|
||||
\`\`\`
|
||||
|
||||
|
||||
|
||||
##### Step 5: Restart your OneUptime server
|
||||
|
||||
You need to restart your OneUptime server to apply these changes. Once you have restarted the server, you should see the "Connect to Microsoft Teams" button on this page.
|
||||
|
||||
##### Additional Notes
|
||||
|
||||
- Make sure your OneUptime instance is accessible from the internet for the OAuth flow to work
|
||||
- The redirect URI in your Azure app must exactly match your OneUptime API URL
|
||||
|
||||
|
||||
We would like to improve this integration, so feedback is more than welcome. Please send us any feedback at hello@oneuptime.com
|
||||
`;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={`Integrating Microsoft Teams with your OneUptime Project`}
|
||||
description={`Microsoft Teams is not connected to OneUptime. Here are some of the steps you need to do to integrate Microsoft Teams with your OneUptime Project`}
|
||||
>
|
||||
<MarkdownViewer text={markdownText} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MicrosoftTeamsIntegrationDocumentation;
|
||||
@@ -4,8 +4,10 @@ import React, {
|
||||
ReactElement,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import Card, { CardButtonSchema } from "Common/UI/Components/Card/Card";
|
||||
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import Button, {
|
||||
ButtonStyleType as SharedButtonStyleType,
|
||||
} from "Common/UI/Components/Button/Button";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import URL from "Common/Types/API/URL";
|
||||
@@ -32,6 +34,10 @@ import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import WorkspaceType from "Common/Types/Workspace/WorkspaceType";
|
||||
import SlackIntegrationDocumentation from "./SlackIntegrationDocumentation";
|
||||
import Link from "Common/UI/Components/Link/Link";
|
||||
import Steps from "Common/UI/Components/Forms/Steps/Steps";
|
||||
import { FormStep } from "Common/UI/Components/Forms/Types/FormStep";
|
||||
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
|
||||
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
|
||||
|
||||
export interface ComponentProps {
|
||||
onConnected: VoidFunction;
|
||||
@@ -54,8 +60,12 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
|
||||
React.useState<ObjectID | null>(null);
|
||||
const [isProjectAccountConnected, setIsProjectAccountConnected] =
|
||||
React.useState<boolean>(false);
|
||||
const [isButtonLoading, setIsButtonLoading] = React.useState<boolean>(false);
|
||||
const [slackTeamName, setSlackTeamName] = React.useState<string | null>(null);
|
||||
const [currentStep, setCurrentStep] = React.useState<string>("install-app");
|
||||
const [isFinished, setIsFinished] = React.useState<boolean>(false);
|
||||
const [showUninstallConfirm, setShowUninstallConfirm] =
|
||||
React.useState<boolean>(false);
|
||||
const [isActionLoading, setIsActionLoading] = React.useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isProjectAccountConnected) {
|
||||
@@ -63,7 +73,7 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
|
||||
} else {
|
||||
props.onDisconnected();
|
||||
}
|
||||
}, [isProjectAccountConnected]);
|
||||
}, [isProjectAccountConnected, props]);
|
||||
|
||||
const loadItems: PromiseVoidFunction = async (): Promise<void> => {
|
||||
try {
|
||||
@@ -95,7 +105,7 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
|
||||
projectAuth.data[0]!.miscData! as SlackMiscData
|
||||
).teamName;
|
||||
setWorkspaceProjectAuthTokenId(projectAuth.data[0]!.id);
|
||||
setSlackTeamName(slackTeamName);
|
||||
setSlackTeamName(slackTeamName || null);
|
||||
}
|
||||
|
||||
// fetch user auth token.
|
||||
@@ -162,59 +172,6 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
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: `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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const connectWithSlack: VoidFunction = (): void => {
|
||||
if (SlackAppClientId) {
|
||||
const projectId: ObjectID | null = ProjectUtil.getCurrentProjectId();
|
||||
@@ -338,84 +295,398 @@ const SlackIntegration: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
};
|
||||
|
||||
type GetConnectWithSlackButtonFunction = (title: string) => CardButtonSchema;
|
||||
// Steps definition (no team selection step for Slack)
|
||||
const integrationSteps: Array<FormStep<FormValues<unknown>>> = [
|
||||
{ id: "install-app", title: "Step 1: Install & Authorize Workspace" },
|
||||
{ id: "user-account", title: "Step 2: Connect User Account" },
|
||||
{ id: "finish", title: "Step 3: Finish" },
|
||||
];
|
||||
|
||||
const getConnectWithSlackButton: GetConnectWithSlackButtonFunction = (
|
||||
title: string,
|
||||
): CardButtonSchema => {
|
||||
return {
|
||||
title: title || `Connect with Slack`,
|
||||
buttonStyle: ButtonStyleType.PRIMARY,
|
||||
onClick: () => {
|
||||
return connectWithSlack();
|
||||
},
|
||||
|
||||
icon: IconProp.Slack,
|
||||
};
|
||||
const getCurrentStep: () => string = (): string => {
|
||||
if (!isProjectAccountConnected) {
|
||||
return "install-app";
|
||||
}
|
||||
if (!isUserAccountConnected) {
|
||||
return "user-account";
|
||||
}
|
||||
if (isFinished) {
|
||||
return "finish";
|
||||
}
|
||||
return "user-account";
|
||||
};
|
||||
|
||||
// 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`),
|
||||
{
|
||||
title: `Uninstall OneUptime from Slack`,
|
||||
isLoading: isButtonLoading,
|
||||
buttonStyle: ButtonStyleType.DANGER,
|
||||
onClick: async () => {
|
||||
try {
|
||||
setIsButtonLoading(true);
|
||||
setError(null);
|
||||
if (projectAuthTokenId) {
|
||||
await ModelAPI.deleteItem({
|
||||
modelType: WorkspaceProjectAuthToken,
|
||||
id: projectAuthTokenId!,
|
||||
});
|
||||
useEffect(() => {
|
||||
setCurrentStep(getCurrentStep());
|
||||
}, [isProjectAccountConnected, isUserAccountConnected, isFinished]);
|
||||
|
||||
setIsProjectAccountConnected(false);
|
||||
setWorkspaceProjectAuthTokenId(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.Trash,
|
||||
},
|
||||
];
|
||||
}
|
||||
// Auto-finish if both tokens present on load (refresh persistence)
|
||||
useEffect(() => {
|
||||
if (!isFinished && isProjectAccountConnected && isUserAccountConnected) {
|
||||
setIsFinished(true);
|
||||
setCurrentStep("finish");
|
||||
}
|
||||
}, [isFinished, isProjectAccountConnected, isUserAccountConnected]);
|
||||
|
||||
if (!isProjectAccountConnected) {
|
||||
cardTitle = `Connect with Slack`;
|
||||
cardDescription = `Connect your account with Slack to make the most out of OneUptime.`;
|
||||
cardButtons = [getConnectWithSlackButton(`Connect with Slack`)];
|
||||
}
|
||||
const logoutUser: () => Promise<void> = async (): Promise<void> => {
|
||||
if (!userAuthTokenId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsActionLoading(true);
|
||||
await ModelAPI.deleteItem({
|
||||
modelType: WorkspaceUserAuthToken,
|
||||
id: userAuthTokenId,
|
||||
});
|
||||
setIsUserAccountConnected(false);
|
||||
setWorkspaceUserAuthTokenId(null);
|
||||
setIsFinished(false);
|
||||
setCurrentStep("user-account");
|
||||
} catch (err) {
|
||||
setError(<div>{API.getFriendlyErrorMessage(err as Exception)}</div>);
|
||||
} finally {
|
||||
setIsActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const uninstallIntegration: () => Promise<void> = async (): Promise<void> => {
|
||||
try {
|
||||
setIsActionLoading(true);
|
||||
// Delete user token first (ignore errors)
|
||||
if (userAuthTokenId) {
|
||||
try {
|
||||
await ModelAPI.deleteItem({
|
||||
modelType: WorkspaceUserAuthToken,
|
||||
id: userAuthTokenId,
|
||||
});
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
setWorkspaceUserAuthTokenId(null);
|
||||
setIsUserAccountConnected(false);
|
||||
}
|
||||
if (projectAuthTokenId) {
|
||||
try {
|
||||
await ModelAPI.deleteItem({
|
||||
modelType: WorkspaceProjectAuthToken,
|
||||
id: projectAuthTokenId,
|
||||
});
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
setWorkspaceProjectAuthTokenId(null);
|
||||
setIsProjectAccountConnected(false);
|
||||
}
|
||||
setIsFinished(false);
|
||||
setCurrentStep("install-app");
|
||||
} catch (err) {
|
||||
setError(<div>{API.getFriendlyErrorMessage(err as Exception)}</div>);
|
||||
} finally {
|
||||
setIsActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!SlackAppClientId) {
|
||||
return <SlackIntegrationDocumentation manifest={manifest as JSONObject} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
// Finished management card
|
||||
if (isFinished && isProjectAccountConnected && isUserAccountConnected) {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="w-full">
|
||||
<Card
|
||||
title={`Slack Integration Active (${slackTeamName || "Workspace"})`}
|
||||
description="Manage or uninstall your Slack integration."
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="border rounded-md p-4 bg-gray-50">
|
||||
<h4 className="text-sm font-medium text-gray-800 mb-2">
|
||||
User Session
|
||||
</h4>
|
||||
<p className="text-xs text-gray-600 mb-3">
|
||||
Log out your personal Slack user. Workspace installation
|
||||
remains until you uninstall.
|
||||
</p>
|
||||
<Button
|
||||
title="Log Out of Slack"
|
||||
className="-ml-3"
|
||||
buttonStyle={SharedButtonStyleType.NORMAL}
|
||||
icon={IconProp.Logout}
|
||||
onClick={() => {
|
||||
void logoutUser();
|
||||
}}
|
||||
isLoading={isActionLoading}
|
||||
disabled={isActionLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="border rounded-md p-4 bg-red-50">
|
||||
<h4 className="text-sm font-medium text-red-800 mb-2">
|
||||
Uninstall Slack App
|
||||
</h4>
|
||||
<p className="text-xs text-red-700 mb-3">
|
||||
Removes stored tokens in OneUptime. (Remove the app in Slack
|
||||
admin to fully revoke.)
|
||||
</p>
|
||||
<Button
|
||||
className="-ml-3"
|
||||
title="Uninstall Integration"
|
||||
buttonStyle={SharedButtonStyleType.DANGER}
|
||||
icon={IconProp.Trash}
|
||||
onClick={() => {
|
||||
return setShowUninstallConfirm(true);
|
||||
}}
|
||||
isLoading={isActionLoading}
|
||||
disabled={isActionLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
{showUninstallConfirm && (
|
||||
<ConfirmModal
|
||||
title="Uninstall Slack Integration"
|
||||
description={
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>
|
||||
This will delete both workspace-level and user-level Slack
|
||||
tokens stored in OneUptime.
|
||||
</p>
|
||||
<p className="text-red-600 font-medium">
|
||||
This action cannot be undone here.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
To fully revoke in Slack, remove the installed app from your
|
||||
Slack admin dashboard after uninstalling.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
submitButtonText="Uninstall"
|
||||
submitButtonType={SharedButtonStyleType.DANGER}
|
||||
onSubmit={async () => {
|
||||
await uninstallIntegration();
|
||||
setShowUninstallConfirm(false);
|
||||
}}
|
||||
onClose={() => {
|
||||
return setShowUninstallConfirm(false);
|
||||
}}
|
||||
disableSubmitButton={isActionLoading}
|
||||
isLoading={isActionLoading}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const renderStepContent: () => ReactElement = (): ReactElement => {
|
||||
switch (currentStep) {
|
||||
case "install-app":
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
Step 1: Install & Authorize Slack App
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Install OneUptime in your Slack workspace to enable incident
|
||||
notifications and commands.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<Button
|
||||
className="-ml-3"
|
||||
title={
|
||||
isProjectAccountConnected
|
||||
? "Workspace Connected"
|
||||
: "Install Slack App"
|
||||
}
|
||||
icon={IconProp.Slack}
|
||||
onClick={() => {
|
||||
return connectWithSlack();
|
||||
}}
|
||||
disabled={isProjectAccountConnected}
|
||||
buttonStyle={
|
||||
isProjectAccountConnected
|
||||
? SharedButtonStyleType.SUCCESS
|
||||
: SharedButtonStyleType.PRIMARY
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case "user-account":
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
Step 2: Connect Your Slack User
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Authorize your personal user so OneUptime can attribute actions
|
||||
and send you direct messages where applicable.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<Button
|
||||
className="-ml-3"
|
||||
title={
|
||||
isUserAccountConnected ? "User Connected" : "Connect User"
|
||||
}
|
||||
icon={isUserAccountConnected ? IconProp.Check : IconProp.User}
|
||||
onClick={() => {
|
||||
return connectWithSlack();
|
||||
}}
|
||||
disabled={isUserAccountConnected || !isProjectAccountConnected}
|
||||
buttonStyle={
|
||||
isUserAccountConnected
|
||||
? SharedButtonStyleType.SUCCESS
|
||||
: SharedButtonStyleType.PRIMARY
|
||||
}
|
||||
/>
|
||||
{isUserAccountConnected && (
|
||||
<Button
|
||||
title="Log Out User"
|
||||
icon={IconProp.Logout}
|
||||
buttonStyle={SharedButtonStyleType.OUTLINE}
|
||||
onClick={() => {
|
||||
void logoutUser();
|
||||
}}
|
||||
disabled={isActionLoading}
|
||||
isLoading={isActionLoading}
|
||||
/>
|
||||
)}
|
||||
{isProjectAccountConnected &&
|
||||
isUserAccountConnected &&
|
||||
!isFinished && (
|
||||
<Button
|
||||
title="Finish"
|
||||
icon={IconProp.Check}
|
||||
buttonStyle={SharedButtonStyleType.SUCCESS}
|
||||
onClick={() => {
|
||||
setIsFinished(true);
|
||||
setCurrentStep("finish");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case "finish":
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
Setup Complete
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Slack integration is fully configured for workspace{" "}
|
||||
<strong>{slackTeamName || "your Slack Workspace"}</strong>. You
|
||||
can now receive notifications and use Slack commands.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<Button
|
||||
className="-ml-3"
|
||||
title="Manage Integration"
|
||||
icon={IconProp.Settings}
|
||||
buttonStyle={SharedButtonStyleType.OUTLINE}
|
||||
onClick={() => {
|
||||
setCurrentStep("user-account");
|
||||
setIsFinished(false);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="Uninstall"
|
||||
icon={IconProp.Trash}
|
||||
buttonStyle={SharedButtonStyleType.DANGER_OUTLINE}
|
||||
onClick={() => {
|
||||
return setShowUninstallConfirm(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <div>Unknown step</div>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div>
|
||||
<div className="w-full">
|
||||
<Card
|
||||
title={cardTitle}
|
||||
description={cardDescription}
|
||||
buttons={cardButtons}
|
||||
/>
|
||||
title="Slack Integration Setup"
|
||||
description="Follow these steps to connect your Slack workspace with OneUptime."
|
||||
>
|
||||
<div className="lg:grid lg:grid-cols-12 lg:gap-x-8">
|
||||
<aside className="lg:col-span-4 mb-8 lg:mb-0">
|
||||
<div className="bg-gray-50 rounded-lg p-6 border border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||||
Setup Progress
|
||||
</h3>
|
||||
<Steps
|
||||
steps={integrationSteps}
|
||||
currentFormStepId={currentStep}
|
||||
onClick={(step: FormStep<FormValues<unknown>>) => {
|
||||
const targetIndex: number = integrationSteps.findIndex(
|
||||
(s: FormStep<FormValues<unknown>>) => {
|
||||
return s.id === step.id;
|
||||
},
|
||||
);
|
||||
const currentIndex: number = integrationSteps.findIndex(
|
||||
(s: FormStep<FormValues<unknown>>) => {
|
||||
return s.id === currentStep;
|
||||
},
|
||||
);
|
||||
if (targetIndex <= currentIndex) {
|
||||
setCurrentStep(step.id);
|
||||
}
|
||||
}}
|
||||
formValues={{} as FormValues<unknown>}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
<div className="lg:col-span-8">{renderStepContent()}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
{showUninstallConfirm && (
|
||||
<ConfirmModal
|
||||
title="Uninstall Slack Integration"
|
||||
description={
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>
|
||||
This will delete both workspace-level and user-level Slack
|
||||
tokens stored in OneUptime.
|
||||
</p>
|
||||
<p className="text-red-600 font-medium">
|
||||
This action cannot be undone here.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
To fully revoke in Slack, remove the installed app from your
|
||||
Slack admin dashboard after uninstalling.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
submitButtonText="Uninstall"
|
||||
submitButtonType={SharedButtonStyleType.DANGER}
|
||||
onSubmit={async () => {
|
||||
await uninstallIntegration();
|
||||
setShowUninstallConfirm(false);
|
||||
}}
|
||||
onClose={() => {
|
||||
return setShowUninstallConfirm(false);
|
||||
}}
|
||||
disableSubmitButton={isActionLoading}
|
||||
isLoading={isActionLoading}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import BaseNotificationRule from "Common/Types/Workspace/NotificationRules/BaseN
|
||||
import AlertNotificationRule from "Common/Types/Workspace/NotificationRules/NotificationRuleTypes/AlertNotificationRule";
|
||||
import ScheduledMaintenanceNotificationRule from "Common/Types/Workspace/NotificationRules/NotificationRuleTypes/ScheduledMaintenanceNotificationRule";
|
||||
import MonitorNotificationRule from "Common/Types/Workspace/NotificationRules/NotificationRuleTypes/MonitorNotificationRule";
|
||||
import WorkspaceUtil from "../../../Utils/Workspace/Workspace";
|
||||
|
||||
export interface ComponentProps {
|
||||
value?: undefined | IncidentNotificationRule;
|
||||
@@ -45,6 +46,10 @@ export interface ComponentProps {
|
||||
const NotificationRuleForm: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const workspaceDisplayName: string = WorkspaceUtil.getWorkspaceDisplayName(
|
||||
props.workspaceType,
|
||||
);
|
||||
|
||||
type NotificationRulesType =
|
||||
| IncidentNotificationRule
|
||||
| AlertNotificationRule
|
||||
@@ -113,8 +118,8 @@ const NotificationRuleForm: FunctionComponent<ComponentProps> = (
|
||||
field: {
|
||||
shouldPostToExistingChannel: true,
|
||||
},
|
||||
title: `Post to Existing ${props.workspaceType} Channel`,
|
||||
description: `When above conditions are met, post to an existing ${props.workspaceType} channel.`,
|
||||
title: `Post to Existing ${workspaceDisplayName} Channel`,
|
||||
description: `When above conditions are met, post to an existing ${workspaceDisplayName} channel.`,
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
},
|
||||
@@ -122,8 +127,8 @@ const NotificationRuleForm: FunctionComponent<ComponentProps> = (
|
||||
field: {
|
||||
existingChannelNames: true,
|
||||
},
|
||||
title: `Existing ${props.workspaceType} Channel Name to Post To`,
|
||||
description: `Please provide the name of the ${props.workspaceType} channel you want to post to.`,
|
||||
title: `Existing ${workspaceDisplayName} Channel Name to Post To`,
|
||||
description: `Please provide the name of the ${workspaceDisplayName} channel you want to post to.`,
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
placeholder: `#channel-name, #general, etc.`,
|
||||
required: true,
|
||||
@@ -133,34 +138,34 @@ const NotificationRuleForm: FunctionComponent<ComponentProps> = (
|
||||
},
|
||||
];
|
||||
|
||||
let archiveTitle: string = `Archive ${props.workspaceType} Channel`;
|
||||
let archiveDescription: string = `When above conditions are met, archive the ${props.workspaceType} channel.`;
|
||||
let archiveTitle: string = `Archive ${workspaceDisplayName} Channel`;
|
||||
let archiveDescription: string = `When above conditions are met, archive the ${workspaceDisplayName} channel.`;
|
||||
|
||||
if (props.eventType === NotificationRuleEventType.Monitor) {
|
||||
archiveTitle = `Archive ${props.workspaceType} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${props.workspaceType} channel automatically when the monitor is deleted.`;
|
||||
archiveTitle = `Archive ${workspaceDisplayName} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${workspaceDisplayName} channel automatically when the monitor is deleted.`;
|
||||
}
|
||||
|
||||
if (props.eventType === NotificationRuleEventType.ScheduledMaintenance) {
|
||||
archiveTitle = `Archive ${props.workspaceType} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${props.workspaceType} channel automatically when the scheduled maintenance is completed.`;
|
||||
archiveTitle = `Archive ${workspaceDisplayName} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${workspaceDisplayName} channel automatically when the scheduled maintenance is completed.`;
|
||||
}
|
||||
|
||||
if (props.eventType === NotificationRuleEventType.OnCallDutyPolicy) {
|
||||
archiveTitle = `Archive ${props.workspaceType} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${props.workspaceType} channel automatically when the on call duty policy is deleted.`;
|
||||
archiveTitle = `Archive ${workspaceDisplayName} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${workspaceDisplayName} channel automatically when the on call duty policy is deleted.`;
|
||||
}
|
||||
|
||||
// incident.
|
||||
if (props.eventType === NotificationRuleEventType.Incident) {
|
||||
archiveTitle = `Archive ${props.workspaceType} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${props.workspaceType} channel automatically when the incident is resolved.`;
|
||||
archiveTitle = `Archive ${workspaceDisplayName} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${workspaceDisplayName} channel automatically when the incident is resolved.`;
|
||||
}
|
||||
|
||||
// alert
|
||||
if (props.eventType === NotificationRuleEventType.Alert) {
|
||||
archiveTitle = `Archive ${props.workspaceType} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${props.workspaceType} channel automatically when the alert is resolved.`;
|
||||
archiveTitle = `Archive ${workspaceDisplayName} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${workspaceDisplayName} channel automatically when the alert is resolved.`;
|
||||
}
|
||||
|
||||
formFields = formFields.concat([
|
||||
@@ -168,8 +173,8 @@ const NotificationRuleForm: FunctionComponent<ComponentProps> = (
|
||||
field: {
|
||||
shouldCreateNewChannel: true,
|
||||
},
|
||||
title: `Create ${props.workspaceType} Channel`,
|
||||
description: `When above conditions are met, create a new ${props.workspaceType} channel.`,
|
||||
title: `Create ${workspaceDisplayName} Channel`,
|
||||
description: `When above conditions are met, create a new ${workspaceDisplayName} channel.`,
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
showHorizontalRuleAbove: true,
|
||||
required: false,
|
||||
@@ -178,7 +183,7 @@ const NotificationRuleForm: FunctionComponent<ComponentProps> = (
|
||||
field: {
|
||||
newChannelTemplateName: true,
|
||||
},
|
||||
title: `New ${props.workspaceType} Channel Name`,
|
||||
title: `New ${workspaceDisplayName} Channel Name`,
|
||||
showIf: (formValue: FormValues<NotificationRulesType>) => {
|
||||
return (
|
||||
(formValue as CreateNewSlackChannelNotificationRuleType)
|
||||
@@ -200,8 +205,8 @@ const NotificationRuleForm: FunctionComponent<ComponentProps> = (
|
||||
.shouldCreateNewChannel || false
|
||||
);
|
||||
},
|
||||
title: `Invite ${props.eventType} owners to new ${props.workspaceType} Channel`,
|
||||
description: `When new ${props.workspaceType} channel is created, invite ${props.eventType} owners.`,
|
||||
title: `Invite ${props.eventType} owners to new ${workspaceDisplayName} Channel`,
|
||||
description: `When new ${workspaceDisplayName} channel is created, invite ${props.eventType} owners.`,
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
},
|
||||
@@ -209,8 +214,8 @@ const NotificationRuleForm: FunctionComponent<ComponentProps> = (
|
||||
field: {
|
||||
inviteTeamsToNewChannel: true,
|
||||
},
|
||||
title: `Invite Teams to New ${props.workspaceType} Channel`,
|
||||
description: `When new ${props.workspaceType} channel is created, invite these teams.`,
|
||||
title: `Invite Teams to New ${workspaceDisplayName} Channel`,
|
||||
description: `When new ${workspaceDisplayName} channel is created, invite these teams.`,
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
required: false,
|
||||
showIf: (formValue: FormValues<NotificationRulesType>) => {
|
||||
@@ -230,8 +235,8 @@ const NotificationRuleForm: FunctionComponent<ComponentProps> = (
|
||||
field: {
|
||||
inviteUsersToNewChannel: true,
|
||||
},
|
||||
title: `Invite Users to New ${props.workspaceType} Channel`,
|
||||
description: `When new ${props.workspaceType} channel is created, invite these users.`,
|
||||
title: `Invite Users to New ${workspaceDisplayName} Channel`,
|
||||
description: `When new ${workspaceDisplayName} channel is created, invite these users.`,
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
required: false,
|
||||
showIf: (formValue: FormValues<NotificationRulesType>) => {
|
||||
@@ -276,8 +281,8 @@ const NotificationRuleForm: FunctionComponent<ComponentProps> = (
|
||||
field: {
|
||||
shouldAutomaticallyInviteOnCallUsersToNewChannel: true,
|
||||
},
|
||||
title: `Automatically Invite On Call Users to New ${props.workspaceType} Channel`,
|
||||
description: `If this is enabled then all on call users will be invited to the new ${props.workspaceType} channel as they are alerted.`,
|
||||
title: `Automatically Invite On Call Users to New ${workspaceDisplayName} Channel`,
|
||||
description: `If this is enabled then all on call users will be invited to the new ${workspaceDisplayName} channel as they are alerted.`,
|
||||
fieldType: FormFieldSchemaType.Checkbox,
|
||||
required: false,
|
||||
showIf: (formValue: FormValues<NotificationRulesType>) => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import ScheduledMaintenanceState from "Common/Models/DatabaseModels/ScheduledMai
|
||||
import MonitorStatus from "Common/Models/DatabaseModels/MonitorStatus";
|
||||
import TeamsElement from "../../Team/TeamsElement";
|
||||
import UsersElement from "../../User/Users";
|
||||
import WorkspaceUtil from "../../../Utils/Workspace/Workspace";
|
||||
|
||||
export interface ComponentProps {
|
||||
value:
|
||||
@@ -46,6 +47,10 @@ export interface ComponentProps {
|
||||
const NotificationRuleViewElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const workspaceDisplayName: string = WorkspaceUtil.getWorkspaceDisplayName(
|
||||
props.workspaceType,
|
||||
);
|
||||
|
||||
let detailFields: Array<
|
||||
Field<
|
||||
| IncidentNotificationRule
|
||||
@@ -90,14 +95,14 @@ const NotificationRuleViewElement: FunctionComponent<ComponentProps> = (
|
||||
},
|
||||
{
|
||||
key: "shouldPostToExistingChannel",
|
||||
title: `Post to Existing ${props.workspaceType} Channel`,
|
||||
description: `When above conditions are met, post to an existing ${props.workspaceType} channel.`,
|
||||
title: `Post to Existing ${workspaceDisplayName} Channel`,
|
||||
description: `When above conditions are met, post to an existing ${workspaceDisplayName} channel.`,
|
||||
fieldType: FieldType.Boolean,
|
||||
},
|
||||
{
|
||||
key: "existingChannelNames",
|
||||
title: `Existing ${props.workspaceType} Channel Name to Post To`,
|
||||
description: `Please provide the name of the ${props.workspaceType} channel you want to post to.`,
|
||||
title: `Existing ${workspaceDisplayName} Channel Name to Post To`,
|
||||
description: `Please provide the name of the ${workspaceDisplayName} channel you want to post to.`,
|
||||
fieldType: FieldType.Text,
|
||||
showIf: (
|
||||
formValue:
|
||||
@@ -111,29 +116,29 @@ const NotificationRuleViewElement: FunctionComponent<ComponentProps> = (
|
||||
},
|
||||
];
|
||||
|
||||
let archiveTitle: string = `Archive ${props.workspaceType} Channel`;
|
||||
let archiveDescription: string = `When above conditions are met, archive the ${props.workspaceType} channel.`;
|
||||
let archiveTitle: string = `Archive ${workspaceDisplayName} Channel`;
|
||||
let archiveDescription: string = `When above conditions are met, archive the ${workspaceDisplayName} channel.`;
|
||||
|
||||
if (props.eventType === NotificationRuleEventType.Monitor) {
|
||||
archiveTitle = `Archive ${props.workspaceType} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${props.workspaceType} channel automatically when the monitor is deleted.`;
|
||||
archiveTitle = `Archive ${workspaceDisplayName} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${workspaceDisplayName} channel automatically when the monitor is deleted.`;
|
||||
}
|
||||
|
||||
if (props.eventType === NotificationRuleEventType.ScheduledMaintenance) {
|
||||
archiveTitle = `Archive ${props.workspaceType} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${props.workspaceType} channel automatically when the scheduled maintenance is completed.`;
|
||||
archiveTitle = `Archive ${workspaceDisplayName} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${workspaceDisplayName} channel automatically when the scheduled maintenance is completed.`;
|
||||
}
|
||||
|
||||
// incident.
|
||||
if (props.eventType === NotificationRuleEventType.Incident) {
|
||||
archiveTitle = `Archive ${props.workspaceType} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${props.workspaceType} channel automatically when the incident is resolved.`;
|
||||
archiveTitle = `Archive ${workspaceDisplayName} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${workspaceDisplayName} channel automatically when the incident is resolved.`;
|
||||
}
|
||||
|
||||
// alert
|
||||
if (props.eventType === NotificationRuleEventType.Alert) {
|
||||
archiveTitle = `Archive ${props.workspaceType} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${props.workspaceType} channel automatically when the alert is resolved.`;
|
||||
archiveTitle = `Archive ${workspaceDisplayName} Channel Automatically`;
|
||||
archiveDescription = `Archive the ${workspaceDisplayName} channel automatically when the alert is resolved.`;
|
||||
}
|
||||
|
||||
const incidentAlertMaintenanceFields: Array<
|
||||
@@ -145,13 +150,13 @@ const NotificationRuleViewElement: FunctionComponent<ComponentProps> = (
|
||||
> = [
|
||||
{
|
||||
key: "shouldCreateNewChannel",
|
||||
title: `Create ${props.workspaceType} Channel`,
|
||||
description: `When above conditions are met, create a new ${props.workspaceType} channel.`,
|
||||
title: `Create ${workspaceDisplayName} Channel`,
|
||||
description: `When above conditions are met, create a new ${workspaceDisplayName} channel.`,
|
||||
fieldType: FieldType.Boolean,
|
||||
},
|
||||
{
|
||||
key: "newChannelTemplateName",
|
||||
title: `${props.workspaceType} Channel Template Name`,
|
||||
title: `${workspaceDisplayName} Channel Template Name`,
|
||||
description: `If your new channel name is "oneuptime-${props.eventType?.toLowerCase()}-", then we will append the ${props.eventType} in the end so, it'll look like "oneuptime-${props.eventType?.toLowerCase()}-X".`,
|
||||
fieldType: FieldType.Text,
|
||||
placeholder: `oneuptime-${props.eventType?.toLowerCase()}-`,
|
||||
@@ -166,8 +171,8 @@ const NotificationRuleViewElement: FunctionComponent<ComponentProps> = (
|
||||
},
|
||||
{
|
||||
key: "shouldInviteOwnersToNewChannel",
|
||||
title: `Invite ${props.eventType} owners to new ${props.workspaceType} Channel`,
|
||||
description: `When new ${props.workspaceType} channel is created, invite ${props.eventType} owners.`,
|
||||
title: `Invite ${props.eventType} owners to new ${workspaceDisplayName} Channel`,
|
||||
description: `When new ${workspaceDisplayName} channel is created, invite ${props.eventType} owners.`,
|
||||
fieldType: FieldType.Boolean,
|
||||
showIf: (
|
||||
formValue:
|
||||
@@ -180,8 +185,8 @@ const NotificationRuleViewElement: FunctionComponent<ComponentProps> = (
|
||||
},
|
||||
{
|
||||
key: "inviteTeamsToNewChannel",
|
||||
title: `Invite Teams to New ${props.workspaceType} Channel`,
|
||||
description: `When new ${props.workspaceType} channel is created, invite these teams.`,
|
||||
title: `Invite Teams to New ${workspaceDisplayName} Channel`,
|
||||
description: `When new ${workspaceDisplayName} channel is created, invite these teams.`,
|
||||
fieldType: FieldType.Element,
|
||||
showIf: (
|
||||
formValue:
|
||||
@@ -208,8 +213,8 @@ const NotificationRuleViewElement: FunctionComponent<ComponentProps> = (
|
||||
},
|
||||
{
|
||||
key: "inviteUsersToNewChannel",
|
||||
title: `Invite Users to New ${props.workspaceType} Channel`,
|
||||
description: `When new ${props.workspaceType} channel is created, invite these users.`,
|
||||
title: `Invite Users to New ${workspaceDisplayName} Channel`,
|
||||
description: `When new ${workspaceDisplayName} channel is created, invite these users.`,
|
||||
fieldType: FieldType.Element,
|
||||
showIf: (
|
||||
formValue:
|
||||
@@ -270,8 +275,8 @@ const NotificationRuleViewElement: FunctionComponent<ComponentProps> = (
|
||||
> = [
|
||||
{
|
||||
key: "shouldAutomaticallyInviteOnCallUsersToNewChannel",
|
||||
title: `Automatically Invite On Call Users to New ${props.workspaceType} Channel`,
|
||||
description: `If this is enabled then all on call users will be invited to the new ${props.workspaceType} channel as they are alerted.`,
|
||||
title: `Automatically Invite On Call Users to New ${workspaceDisplayName} Channel`,
|
||||
description: `If this is enabled then all on call users will be invited to the new ${workspaceDisplayName} channel as they are alerted.`,
|
||||
fieldType: FieldType.Boolean,
|
||||
showIf: (
|
||||
formValue: IncidentNotificationRule | AlertNotificationRule,
|
||||
|
||||
@@ -51,6 +51,7 @@ import EmptyResponseData from "Common/Types/API/EmptyResponse";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import { APP_API_URL } from "Common/UI/Config";
|
||||
import WorkspaceUtil from "../../Utils/Workspace/Workspace";
|
||||
|
||||
export interface ComponentProps {
|
||||
workspaceType: WorkspaceType;
|
||||
@@ -405,6 +406,8 @@ const WorkspaceNotificationRuleTable: FunctionComponent<ComponentProps> = (
|
||||
query={{
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
eventType: props.eventType,
|
||||
// Ensure we only fetch rules for the current workspace (Slack / MicrosoftTeams)
|
||||
workspaceType: props.workspaceType,
|
||||
}}
|
||||
userPreferencesKey="workspace-notification-rules-table"
|
||||
actionButtons={[
|
||||
@@ -438,8 +441,8 @@ const WorkspaceNotificationRuleTable: FunctionComponent<ComponentProps> = (
|
||||
createEditModalWidth={ModalWidth.Large}
|
||||
isCreateable={true}
|
||||
cardProps={{
|
||||
title: `${props.eventType} - ${props.workspaceType} Notification Rules`,
|
||||
description: `Manage ${props.eventType} notification rules for ${props.workspaceType}.`,
|
||||
title: `${props.eventType} - ${WorkspaceUtil.getWorkspaceDisplayName(props.workspaceType)} Notification Rules`,
|
||||
description: `Manage ${props.eventType} notification rules for ${WorkspaceUtil.getWorkspaceDisplayName(props.workspaceType)}.`,
|
||||
}}
|
||||
showAs={ShowAs.List}
|
||||
noItemsMessage={"No notification rules found."}
|
||||
@@ -486,8 +489,8 @@ const WorkspaceNotificationRuleTable: FunctionComponent<ComponentProps> = (
|
||||
field: {
|
||||
notificationRule: true,
|
||||
},
|
||||
title: `Notify ${props.workspaceType} on ${props.eventType} when...`,
|
||||
description: `Set the conditions to notify ${props.workspaceType} on ${props.eventType}. If you do not set any conditions, then this rule will trigger for every ${props.eventType}.`,
|
||||
title: `Notify ${WorkspaceUtil.getWorkspaceDisplayName(props.workspaceType)} on ${props.eventType} when...`,
|
||||
description: `Set the conditions to notify ${WorkspaceUtil.getWorkspaceDisplayName(props.workspaceType)} on ${props.eventType}. If you do not set any conditions, then this rule will trigger for every ${props.eventType}.`,
|
||||
fieldType: FormFieldSchemaType.CustomComponent,
|
||||
required: true,
|
||||
stepId: "rules",
|
||||
@@ -607,7 +610,7 @@ const WorkspaceNotificationRuleTable: FunctionComponent<ComponentProps> = (
|
||||
<ConfirmModal
|
||||
title={`Test Rule`}
|
||||
error={testError}
|
||||
description={`Test the rule ${testNotificationRule.name} by sending a test notification to ${props.workspaceType}.`}
|
||||
description={`Test the rule ${testNotificationRule.name} by sending a test notification to ${WorkspaceUtil.getWorkspaceDisplayName(props.workspaceType)}.`}
|
||||
submitButtonText={"Test"}
|
||||
onClose={() => {
|
||||
setShowTestModal(false);
|
||||
@@ -631,7 +634,7 @@ const WorkspaceNotificationRuleTable: FunctionComponent<ComponentProps> = (
|
||||
<ConfirmModal
|
||||
title={testError ? `Test Failed` : `Test Executed Successfully`}
|
||||
error={testError}
|
||||
description={`Test executed successfully. You should now see a notification in ${props.workspaceType}.`}
|
||||
description={`Test executed successfully. You should now see a notification in ${WorkspaceUtil.getWorkspaceDisplayName(props.workspaceType)}.`}
|
||||
submitButtonType={ButtonStyleType.NORMAL}
|
||||
submitButtonText={"Close"}
|
||||
onSubmit={async () => {
|
||||
|
||||
@@ -11,7 +11,6 @@ import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import EmptyState from "Common/UI/Components/EmptyState/EmptyState";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import ComingSoon from "Common/UI/Components/ComingSoon/ComingSoon";
|
||||
|
||||
const IncidentsPage: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -21,10 +20,7 @@ const IncidentsPage: FunctionComponent<
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const [showComingSoon, setShowComingSoon] = React.useState<boolean>(false);
|
||||
|
||||
const loadItems: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setShowComingSoon(true);
|
||||
try {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
@@ -53,15 +49,6 @@ const IncidentsPage: FunctionComponent<
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (showComingSoon) {
|
||||
return (
|
||||
<ComingSoon
|
||||
title="Microsoft Teams Integration is coming soon, but you can still integrate Teams with Workflows!"
|
||||
description="We are working hard to bring you the Microsoft Teams integration. In the meantime, you can still integrate with Workflows to receive alerts in Microsoft Teams. Please click on Workflows in the top navigation to get started."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isMicrosoftTeamsConnected && (
|
||||
@@ -75,7 +62,7 @@ const IncidentsPage: FunctionComponent<
|
||||
<EmptyState
|
||||
id="MicrosoftTeams-connection"
|
||||
icon={IconProp.MicrosoftTeams}
|
||||
title="MicrosoftTeams is not connected yet!"
|
||||
title="Microsoft Teams is not connected yet!"
|
||||
description="Connect your Microsoft Teams workspace to receive alert notifications. Please go to Project Settings > Workspace Connections > Microsoft Teams to connect your workspace."
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@ import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import EmptyState from "Common/UI/Components/EmptyState/EmptyState";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import ComingSoon from "Common/UI/Components/ComingSoon/ComingSoon";
|
||||
|
||||
const IncidentsPage: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -21,10 +20,7 @@ const IncidentsPage: FunctionComponent<
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const [showComingSoon, setShowComingSoon] = React.useState<boolean>(false);
|
||||
|
||||
const loadItems: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setShowComingSoon(true);
|
||||
try {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
@@ -53,14 +49,7 @@ const IncidentsPage: FunctionComponent<
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (showComingSoon) {
|
||||
return (
|
||||
<ComingSoon
|
||||
title="Microsoft Teams Integration is coming soon, but you can still integrate Teams with Workflows!"
|
||||
description="We are working hard to bring you the Microsoft Teams integration. In the meantime, you can still integrate with Workflows to receive alerts in Microsoft Teams. Please click on Workflows in the top navigation to get started."
|
||||
/>
|
||||
);
|
||||
}
|
||||
// showComingSoon removed; render table/empty state instead
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -75,7 +64,7 @@ const IncidentsPage: FunctionComponent<
|
||||
<EmptyState
|
||||
id="MicrosoftTeams-connection"
|
||||
icon={IconProp.MicrosoftTeams}
|
||||
title="MicrosoftTeams is not connected yet!"
|
||||
title="Microsoft Teams is not connected yet!"
|
||||
description="Connect your Microsoft Teams workspace to receive alert notifications. Please go to Project Settings > Workspace Connections > Microsoft Teams to connect your workspace."
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@ import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import EmptyState from "Common/UI/Components/EmptyState/EmptyState";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import ComingSoon from "Common/UI/Components/ComingSoon/ComingSoon";
|
||||
|
||||
const IncidentsPage: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -21,10 +20,7 @@ const IncidentsPage: FunctionComponent<
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const [showComingSoon, setShowComingSoon] = React.useState<boolean>(false);
|
||||
|
||||
const loadItems: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setShowComingSoon(true);
|
||||
try {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
@@ -53,14 +49,7 @@ const IncidentsPage: FunctionComponent<
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (showComingSoon) {
|
||||
return (
|
||||
<ComingSoon
|
||||
title="Microsoft Teams Integration is coming soon, but you can still integrate Teams with Workflows!"
|
||||
description="We are working hard to bring you the Microsoft Teams integration. In the meantime, you can still integrate with Workflows to receive incidents in Microsoft Teams. Please click on Workflows in the top navigation to get started."
|
||||
/>
|
||||
);
|
||||
}
|
||||
// showComingSoon removed; render table/empty state instead
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -75,7 +64,7 @@ const IncidentsPage: FunctionComponent<
|
||||
<EmptyState
|
||||
id="MicrosoftTeams-connection"
|
||||
icon={IconProp.MicrosoftTeams}
|
||||
title="MicrosoftTeams is not connected yet!"
|
||||
title="Microsoft Teams is not connected yet!"
|
||||
description="Connect your Microsoft Teams workspace to receive alert notifications. Please go to Project Settings > Workspace Connections > Microsoft Teams to connect your workspace."
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@ import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import EmptyState from "Common/UI/Components/EmptyState/EmptyState";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import ComingSoon from "Common/UI/Components/ComingSoon/ComingSoon";
|
||||
|
||||
const MonitorsPage: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -21,10 +20,7 @@ const MonitorsPage: FunctionComponent<
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const [showComingSoon, setShowComingSoon] = React.useState<boolean>(false);
|
||||
|
||||
const loadItems: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setShowComingSoon(true);
|
||||
try {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
@@ -53,14 +49,7 @@ const MonitorsPage: FunctionComponent<
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (showComingSoon) {
|
||||
return (
|
||||
<ComingSoon
|
||||
title="Microsoft Teams Integration is coming soon, but you can still integrate Teams with Workflows!"
|
||||
description="We are working hard to bring you the Microsoft Teams integration. In the meantime, you can still integrate with Workflows to receive monitors in Microsoft Teams. Please click on Workflows in the top navigation to get started."
|
||||
/>
|
||||
);
|
||||
}
|
||||
// showComingSoon removed; render table/empty state instead
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -75,8 +64,8 @@ const MonitorsPage: FunctionComponent<
|
||||
<EmptyState
|
||||
id="MicrosoftTeams-connection"
|
||||
icon={IconProp.MicrosoftTeams}
|
||||
title="MicrosoftTeams is not connected yet!"
|
||||
description="Connect your Microsoft Teams workspace to receive alert notifications. Please go to Project Settings > Workspace Connections > Microsoft Teams to connect your workspace."
|
||||
title="Microsoft Teams is not connected yet!"
|
||||
description="Connect your Microsoft Teams workspace to receive monitor notifications. Please go to Project Settings > Workspace Connections > Microsoft Teams to connect your workspace."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,6 @@ import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import EmptyState from "Common/UI/Components/EmptyState/EmptyState";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import ComingSoon from "Common/UI/Components/ComingSoon/ComingSoon";
|
||||
|
||||
const MonitorsPage: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -21,10 +20,7 @@ const MonitorsPage: FunctionComponent<
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const [showComingSoon, setShowComingSoon] = React.useState<boolean>(false);
|
||||
|
||||
const loadItems: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setShowComingSoon(true);
|
||||
try {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
@@ -53,14 +49,7 @@ const MonitorsPage: FunctionComponent<
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (showComingSoon) {
|
||||
return (
|
||||
<ComingSoon
|
||||
title="Microsoft Teams Integration is coming soon, but you can still integrate Teams with Workflows!"
|
||||
description="We are working hard to bring you the Microsoft Teams integration. In the meantime, you can still integrate with Workflows to receive on call duty alerts in Microsoft Teams. Please click on Workflows in the top navigation to get started."
|
||||
/>
|
||||
);
|
||||
}
|
||||
// showComingSoon removed; render table/empty state instead
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -75,8 +64,8 @@ const MonitorsPage: FunctionComponent<
|
||||
<EmptyState
|
||||
id="MicrosoftTeams-connection"
|
||||
icon={IconProp.MicrosoftTeams}
|
||||
title="MicrosoftTeams is not connected yet!"
|
||||
description="Connect your Microsoft Teams workspace to receive alert notifications. Please go to Project Settings > Workspace Connections > Microsoft Teams to connect your workspace."
|
||||
title="Microsoft Teams is not connected yet!"
|
||||
description="Connect your Microsoft Teams workspace to receive on call duty notifications. Please go to Project Settings > Workspace Connections > Microsoft Teams to connect your workspace."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,6 @@ import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import EmptyState from "Common/UI/Components/EmptyState/EmptyState";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import ComingSoon from "Common/UI/Components/ComingSoon/ComingSoon";
|
||||
|
||||
const IncidentsPage: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -21,10 +20,7 @@ const IncidentsPage: FunctionComponent<
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const [showComingSoon, setShowComingSoon] = React.useState<boolean>(false);
|
||||
|
||||
const loadItems: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setShowComingSoon(true);
|
||||
try {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
@@ -53,14 +49,7 @@ const IncidentsPage: FunctionComponent<
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (showComingSoon) {
|
||||
return (
|
||||
<ComingSoon
|
||||
title="Microsoft Teams Integration is coming soon, but you can still integrate Teams with Workflows!"
|
||||
description="We are working hard to bring you the Microsoft Teams integration. In the meantime, you can still integrate with Workflows to receive incidents in Microsoft Teams. Please click on Workflows in the top navigation to get started."
|
||||
/>
|
||||
);
|
||||
}
|
||||
// showComingSoon removed; render table/empty state instead
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -75,7 +64,7 @@ const IncidentsPage: FunctionComponent<
|
||||
<EmptyState
|
||||
id="MicrosoftTeams-connection"
|
||||
icon={IconProp.MicrosoftTeams}
|
||||
title="MicrosoftTeams is not connected yet!"
|
||||
title="Microsoft Teams is not connected yet!"
|
||||
description="Connect your Microsoft Teams workspace to receive scheduled maintenance notifications. Please go to Project Settings > Workspace Connections > Microsoft Teams to connect your workspace."
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,7 @@ const IncidentsPage: FunctionComponent<
|
||||
<EmptyState
|
||||
id="MicrosoftTeams-connection"
|
||||
icon={IconProp.MicrosoftTeams}
|
||||
title="MicrosoftTeams is not connected yet!"
|
||||
title="Microsoft Teams is not connected yet!"
|
||||
description="Connect your Microsoft Teams workspace to receive scheduled maintenance notifications. Please go to Project Settings > Workspace Connections > Microsoft Teams to connect your workspace."
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import ComingSoon from "Common/UI/Components/ComingSoon/ComingSoon";
|
||||
import MicrosoftTeamsIntegration from "../../Components/MicrosoftTeams/MicrosoftTeamsIntegration";
|
||||
|
||||
const SlackIntegrationPage: FunctionComponent<PageComponentProps> = (
|
||||
const MicrosoftTeamsIntegrationPage: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<div>
|
||||
<ComingSoon
|
||||
title="Microsoft Teams Integration is coming soon, but you can still integrate Teams with Workflows!"
|
||||
description="We are working hard to bring you the Microsoft Teams integration. In the meantime, you can still integrate with Workflows to receive alerts in Microsoft Teams. Please click on Workflows in the top navigation to get started."
|
||||
<MicrosoftTeamsIntegration
|
||||
onConnected={() => {}}
|
||||
onDisconnected={() => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SlackIntegrationPage;
|
||||
export default MicrosoftTeamsIntegrationPage;
|
||||
|
||||
@@ -6,6 +6,17 @@ import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
|
||||
export default class WorkspaceUtil {
|
||||
public static getWorkspaceDisplayName(workspaceType: WorkspaceType): string {
|
||||
switch (workspaceType) {
|
||||
case WorkspaceType.MicrosoftTeams:
|
||||
return "Microsoft Teams";
|
||||
case WorkspaceType.Slack:
|
||||
return "Slack";
|
||||
default:
|
||||
return workspaceType;
|
||||
}
|
||||
}
|
||||
|
||||
public static async isWorkspaceConnected(
|
||||
workspaceType: WorkspaceType,
|
||||
): Promise<boolean> {
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
# Connecting OneUptime to Slack
|
||||
|
||||
### Steps to Connect OneUptime to Slack
|
||||
OneUptime provides deep integration with Slack, allowing you to receive notifications, manage incidents, and collaborate directly from your Slack workspace.
|
||||
|
||||
1. **Create an Account on OneUptime**
|
||||
- Visit [OneUptime.com](https://oneuptime.com) and create an account.
|
||||
- Once the account is created, create a new project.
|
||||
## Features
|
||||
|
||||
2. **Connect Slack to OneUptime Project**
|
||||
- Navigate to **Project Settings** > **Slack** within your OneUptime project.
|
||||
- Follow the prompts to connect your Slack account with the OneUptime project.
|
||||
- **Real-time Notifications**: Get instant alerts for incidents, monitors, and scheduled maintenance
|
||||
- **Channel Management**: Automatically create and manage dedicated channels for incidents
|
||||
- **Interactive Actions**: Acknowledge alerts, update incident status, and manage resources directly from Slack
|
||||
- **User Invitations**: Automatically invite relevant team members to incident channels
|
||||
- **Status Updates**: Post status updates and communicate with your team during incidents
|
||||
|
||||
3. **Configure Incident Notifications**
|
||||
- After connecting your Slack account, go to **Incidents Page** > **Slack**.
|
||||
- Add rules to send incident notifications to Slack. For example, you can create a rule that creates a new Slack channel and invites incident owners when an incident is created.
|
||||
## Steps to Connect OneUptime to Slack
|
||||
|
||||
4. **Configure Alerts and Scheduled Maintenance Notifications**
|
||||
- Similar rules can be applied to Alerts and Scheduled Maintenance by navigating to their respective pages and configuring the desired rules.
|
||||
### 1. Create an Account on OneUptime
|
||||
- Visit [OneUptime.com](https://oneuptime.com) and create an account.
|
||||
- Once the account is created, create a new project.
|
||||
|
||||
### 2. Connect Slack to OneUptime Project
|
||||
- Navigate to **Project Settings** > **Slack** within your OneUptime project.
|
||||
- Follow the prompts to connect your Slack account with the OneUptime project.
|
||||
|
||||
### 3. Configure Incident Notifications
|
||||
- After connecting your Slack account, go to **Incidents Page** > **Slack**.
|
||||
- Add rules to send incident notifications to Slack. For example, you can create a rule that creates a new Slack channel and invites incident owners when an incident is created.
|
||||
|
||||
### 4. Configure Alerts and Scheduled Maintenance Notifications
|
||||
- Similar rules can be applied to Alerts and Scheduled Maintenance by navigating to their respective pages and configuring the desired rules.
|
||||
|
||||
## Self-Hosted Installation
|
||||
|
||||
If you're running OneUptime on your own infrastructure, you'll need to create a custom Slack app. See our [Self-Hosted Slack Integration Guide](../self-hosted/slack-integration.md) for detailed instructions.
|
||||
|
||||
@@ -3093,6 +3093,12 @@ img,video {
|
||||
--tw-prose-underline-size:6px
|
||||
}
|
||||
|
||||
/* OneUptime override (appended): remove decorative inline code backticks */
|
||||
:not(pre) > code::before,
|
||||
:not(pre) > code::after {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.dark\:prose-code\:text-slate-300 :is(:where(code):not(:where([class~=not-prose],[class~=not-prose] *))):where(.dark,.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(203 213 225/var(--tw-text-opacity))
|
||||
|
||||
@@ -22,6 +22,8 @@ Usage:
|
||||
{{- end }}
|
||||
- name: SLACK_APP_CLIENT_ID
|
||||
value: {{ $.Values.slackApp.clientId | quote }}
|
||||
- name: MICROSOFT_TEAMS_APP_CLIENT_ID
|
||||
value: {{ $.Values.microsoftTeamsApp.clientId | quote }}
|
||||
- name: HOST
|
||||
value: {{ $.Values.host }}
|
||||
- name: STATUS_PAGE_CNAME_RECORD
|
||||
@@ -171,6 +173,9 @@ Usage:
|
||||
- name: SLACK_APP_SIGNING_SECRET
|
||||
value: {{ $.Values.slackApp.signingSecret }}
|
||||
|
||||
- name: MICROSOFT_TEAMS_APP_CLIENT_SECRET
|
||||
value: {{ $.Values.microsoftTeamsApp.clientSecret }}
|
||||
|
||||
- name: NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER
|
||||
value: {{ $.Values.notifications.webhooks.slack.onCreateUser }}
|
||||
|
||||
|
||||
@@ -754,6 +754,13 @@ slackApp:
|
||||
clientSecret:
|
||||
signingSecret:
|
||||
|
||||
# Azure / Microsoft Teams App Configuration
|
||||
# IMPORTANT: Use the SECRET VALUE, not the SECRET ID from Azure App Registration
|
||||
# The secret value is typically longer and includes more characters
|
||||
# Client ID (Application (client) ID) is found in the app registration overview page
|
||||
microsoftTeamsApp:
|
||||
clientId:
|
||||
clientSecret:
|
||||
|
||||
keda:
|
||||
enabled: true
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"watch": ["./*","../Common/UI", "../Common/Types", "../Common/Utils", "../Common/Models"],
|
||||
"watch": [
|
||||
"./",
|
||||
"../Common/UI",
|
||||
"../Common/Types",
|
||||
"../Common/Utils",
|
||||
"../Common/Models"
|
||||
],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"ignore": [
|
||||
"./public/**",
|
||||
@@ -10,5 +16,7 @@
|
||||
"./build/dist/**",
|
||||
"../Common/Server/**"
|
||||
],
|
||||
"exec": " npm run dev-build && npm run start"
|
||||
"exec": "sh -c 'npm run dev-build && node esbuild.config.js --watch & npm run start'",
|
||||
"delay": "200ms",
|
||||
"legacyWatch": true
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
"version": "0.1.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "npx nodemon",
|
||||
"build": "NODE_ENV=production node esbuild.config.js",
|
||||
"dev-build": "NODE_ENV=development node esbuild.config.js",
|
||||
"dev": "npx nodemon",
|
||||
"build": "NODE_ENV=production node esbuild.config.js",
|
||||
"dev-build": "echo 'Preparing StatusPage dev build (pre-step placeholder)'",
|
||||
"analyze": "analyze=true NODE_ENV=production node esbuild.config.js",
|
||||
"test": "",
|
||||
"compile": "tsc",
|
||||
@@ -28,7 +28,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"Common": "file:../Common",
|
||||
"Common": "link:../Common",
|
||||
|
||||
"ejs": "^3.1.10",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -335,6 +335,10 @@ SLACK_APP_CLIENT_ID=
|
||||
SLACK_APP_CLIENT_SECRET=
|
||||
SLACK_APP_SIGNING_SECRET=
|
||||
|
||||
# Microsoft Teams Configuration
|
||||
MICROSOFT_TEAMS_APP_CLIENT_ID=
|
||||
MICROSOFT_TEAMS_APP_CLIENT_SECRET=
|
||||
|
||||
# Example -
|
||||
# IPv6 only:
|
||||
# NGINX_LISTEN_ADDRESS=[::]:
|
||||
|
||||
@@ -71,6 +71,8 @@ x-common-variables: &common-variables
|
||||
OPENTELEMETRY_EXPORTER_OTLP_HEADERS: ${OPENTELEMETRY_EXPORTER_OTLP_HEADERS}
|
||||
|
||||
SLACK_APP_CLIENT_ID: ${SLACK_APP_CLIENT_ID}
|
||||
|
||||
MICROSOFT_TEAMS_APP_CLIENT_ID: ${MICROSOFT_TEAMS_APP_CLIENT_ID}
|
||||
|
||||
x-common-ui-variables: &common-ui-variables
|
||||
<<: *common-variables
|
||||
@@ -132,6 +134,8 @@ x-common-server-variables: &common-server-variables
|
||||
SLACK_APP_CLIENT_SECRET: ${SLACK_APP_CLIENT_SECRET}
|
||||
SLACK_APP_SIGNING_SECRET: ${SLACK_APP_SIGNING_SECRET}
|
||||
|
||||
MICROSOFT_TEAMS_APP_CLIENT_SECRET: ${MICROSOFT_TEAMS_APP_CLIENT_SECRET}
|
||||
|
||||
services:
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user