Compare commits

...

119 Commits

Author SHA1 Message Date
Simon Larsen
b976805076 fix: Improve error handling for Microsoft Teams provisioning issues 2025-09-12 13:08:25 +01:00
Simon Larsen
037f2d5dea fix: Update button class for responsive margin adjustments 2025-09-12 12:50:30 +01:00
Simon Larsen
7bdf84ab12 fix: Update button title from "Change Team" to "Select Team" for clarity 2025-09-12 12:48:52 +01:00
Simon Larsen
d91c3db24d docs: Update Microsoft Teams integration documentation with additional Redirect URIs 2025-09-12 12:45:44 +01:00
Simon Larsen
fe7eb1c45a docs: Update Microsoft Teams integration documentation and values.yaml with configuration details 2025-09-12 10:08:53 +01:00
Simon Larsen
1b0bd34279 chore: Remove obsolete permission references from Microsoft Teams documentation 2025-09-12 09:47:08 +01:00
Simon Larsen
2bd8e8782a Merge branch 'master' into teams-integration 2025-09-10 20:08:44 +01:00
Simon Larsen
7c0ba6566e Merge branch 'master' into teams-integration 2025-09-10 19:10:05 +01:00
Simon Larsen
a7bf69269f Merge branch 'master' into teams-integration 2025-09-10 18:55:54 +01:00
Simon Larsen
4aaa059793 refactor: Remove MicrosoftTeamsPermissions module and related validation logic for cleaner codebase 2025-09-02 11:54:19 +01:00
Simon Larsen
be040a964f feat: Update Microsoft Teams error messages and permissions descriptions for clarity and accuracy 2025-09-02 11:47:29 +01:00
Simon Larsen
d966e19751 feat: Update dev-build script to use esbuild for development environment 2025-09-02 11:41:56 +01:00
Simon Larsen
be691cd7f8 feat: Update error messages and permissions descriptions for Microsoft Teams integration to clarify Import API requirements 2025-09-02 09:12:57 +01:00
Simon Larsen
9bd00b4e6a feat: Simplify Markdown renderer by removing redundant overrides and enhancing inline code styling 2025-09-01 21:29:54 +01:00
Simon Larsen
0f127f2f10 Merge branch 'release' into teams-integration 2025-09-01 21:22:50 +01:00
Simon Larsen
e2c070ec63 feat: Replace generic error with BadDataException for invalid state data in Microsoft Teams API 2025-08-29 15:42:35 +01:00
Nawaz Dhandala
0bbf82c7a4 refactor: Simplify error handling and enhance type annotations in Microsoft Teams integration 2025-08-29 15:41:16 +01:00
Nawaz Dhandala
015f22c01a refactor: Improve error handling and type annotations in Microsoft Teams integration 2025-08-29 14:57:36 +01:00
Nawaz Dhandala
9c05035f85 feat: Enhance type annotations and error handling across Microsoft Teams integration components 2025-08-29 14:49:36 +01:00
Nawaz Dhandala
03bc9136a7 Refactor SlackIntegration and NotificationRule components for improved readability and consistency
- Reformatted code in SlackIntegration.tsx for better alignment and readability.
- Updated NotificationRuleForm.tsx and NotificationRuleViewElement.tsx to enhance code clarity by breaking long lines.
- Adjusted MicrosoftTeamsIntegration.tsx for consistent formatting of component props.
2025-08-29 14:20:57 +01:00
Simon Larsen
e7948e0f11 feat: Update button title from "Select Team" to "Change Team" for clarity 2025-08-29 14:19:29 +01:00
Simon Larsen
6b436cd51a feat: Refine team selection logic and messaging for improved user experience 2025-08-29 14:18:05 +01:00
Simon Larsen
4e93e9a4e1 feat: Update team selection card styling for improved visibility and UX 2025-08-29 14:16:19 +01:00
Simon Larsen
bb199506e1 feat: Update setup finished state to default to true and refine step logic for better flow 2025-08-29 14:15:23 +01:00
Simon Larsen
87be845a8c feat: Enhance setup flow by refining step conditions and improving admin consent messaging 2025-08-29 14:11:57 +01:00
Simon Larsen
0c35a704ab feat: Implement auto-selection of the first team in Microsoft Teams integration if none is set 2025-08-29 14:06:03 +01:00
Simon Larsen
48af4d346f feat: Improve team selection UX by opening modal immediately and handling empty teams gracefully 2025-08-29 13:59:11 +01:00
Simon Larsen
af0ea621f7 feat: Refactor setup completion logic to derive state on prerequisites satisfaction 2025-08-29 13:57:52 +01:00
Simon Larsen
8a736d6853 feat: Add persistent setup finished state to Microsoft Teams integration 2025-08-29 13:56:19 +01:00
Simon Larsen
c8f0c540a3 feat: Update border styling for setup progress section in Microsoft Teams and Slack integrations 2025-08-29 13:48:42 +01:00
Simon Larsen
c4ebf42e62 feat: Update nodemon configuration and package scripts for improved development workflow 2025-08-29 13:45:58 +01:00
Simon Larsen
5f3d84e44c feat: Refactor integration setup logic to auto-complete on initial load and remove auto-select team functionality 2025-08-29 13:30:32 +01:00
Simon Larsen
1e34c33006 feat: Change logout button style from outline to normal for better visibility 2025-08-29 13:22:50 +01:00
Simon Larsen
43dbe62a9a feat: Update button styles for improved layout and consistency in Slack integration 2025-08-29 12:44:19 +01:00
Simon Larsen
ba650466f0 feat: Enhance Slack integration with step-by-step setup and management options 2025-08-29 12:42:09 +01:00
Simon Larsen
290a46ab81 feat: Automatically mark setup as finished if all integration prerequisites are met 2025-08-29 12:13:23 +01:00
Simon Larsen
83cececd3c feat: Update button styles and titles for admin consent and user connection actions 2025-08-29 12:11:15 +01:00
Simon Larsen
21edefb6da feat: Add confirmation modal for revoking admin consent and update button action 2025-08-29 12:05:45 +01:00
Simon Larsen
099e00e907 feat: Add revoke admin consent functionality and update button labels for clarity 2025-08-29 12:04:47 +01:00
Simon Larsen
c916e2b596 feat: Add management actions for Microsoft Teams integration, including logout and uninstall functionality 2025-08-29 12:04:02 +01:00
Simon Larsen
68ad1babf9 feat: Add finish step to Microsoft Teams integration process and update button components for improved UI 2025-08-29 11:44:04 +01:00
Simon Larsen
f40c2f4628 feat: Reorder integration steps for Microsoft Teams connection and update titles for clarity 2025-08-29 11:34:35 +01:00
Simon Larsen
e6fe8c436a style: Add ring to sidebar for improved visual separation in setup progress 2025-08-29 11:31:07 +01:00
Simon Larsen
38ba91e411 feat: Implement multi-step integration process for Microsoft Teams with improved UI and state management 2025-08-29 11:11:08 +01:00
Simon Larsen
92505cc3c2 feat: Retrieve Microsoft Teams user ID from Graph API and use it in workspace data 2025-08-28 20:00:35 +01:00
Simon Larsen
33d671ded7 refactor: Simplify Graph API call method by removing fallback token handling 2025-08-28 19:48:21 +01:00
Simon Larsen
e10ad8fbe3 feat: Enhance user addition to Microsoft Teams channels with fallback to team membership 2025-08-28 19:45:15 +01:00
Simon Larsen
b793a65286 refactor: Enhance application token handling in Microsoft Teams integration to ensure proper authorization flow 2025-08-28 19:42:20 +01:00
Simon Larsen
0c073dd5cb feat: Implement admin consent handling for Microsoft Teams integration, including state management and UI updates 2025-08-28 17:48:55 +01:00
Simon Larsen
536c59e5bd refactor: Update delegated scopes for Microsoft Teams integration to include additional permissions 2025-08-28 17:30:38 +01:00
Simon Larsen
fc85a9451e refactor: Enhance debug logging in Microsoft Teams channel creation for better traceability 2025-08-28 17:18:59 +01:00
Simon Larsen
54a1a56a83 refactor: Enhance logging in createChannelsAndInviteUsersToChannelsBasedOnRules for better event type tracking 2025-08-28 16:59:58 +01:00
Simon Larsen
ee2acf6ecf refactor: Improve logging messages in createChannelsAndInviteUsersToChannelsBasedOnRules method for better clarity 2025-08-28 16:55:11 +01:00
Simon Larsen
9552063e89 feat: Add markdown to HTML conversion for improved message rendering in Microsoft Teams 2025-08-28 16:38:22 +01:00
Simon Larsen
bdeac99f5b feat: Implement fallback token mechanism for Microsoft Graph API calls to enhance error handling 2025-08-28 16:34:13 +01:00
Simon Larsen
a4ae41010e refactor: Remove unused app manifest endpoint and streamline delegated permissions in Microsoft Teams integration 2025-08-28 16:10:33 +01:00
Simon Larsen
4af5a99519 refactor: Remove unused permission checklist import from MicrosoftTeamsAPI 2025-08-28 15:17:28 +01:00
Simon Larsen
ff19e4069a feat: Enhance Microsoft Teams integration with updated permission scopes and validation utilities 2025-08-28 15:17:08 +01:00
Simon Larsen
0600cd4b69 feat: Add Teamwork scopes for enhanced Microsoft Teams API permissions and update documentation 2025-08-28 15:06:04 +01:00
Simon Larsen
41965c7c56 feat: Update Microsoft Teams integration to include openid, profile, and offline_access scopes for improved authentication and token management 2025-08-28 13:19:58 +01:00
Simon Larsen
fa19ff280f fix: Update fallback tenant ID to 'organizations' for improved Azure authentication 2025-08-28 13:08:14 +01:00
Simon Larsen
f3bba038ca fix: Update fallback tenant ID to 'common' and improve error logging for Azure App Registration client secret 2025-08-28 12:39:53 +01:00
Simon Larsen
c9783f803f feat: Enhance error handling for invalid Microsoft Teams client secret in token retrieval and refresh processes 2025-08-28 12:09:08 +01:00
Simon Larsen
ff5583839a refactor: Remove MicrosoftTenantId references and update documentation for Microsoft Teams integration 2025-08-28 11:04:25 +01:00
Simon Larsen
40f6717033 fix: Re-throw configuration errors in channel existence check for better error handling 2025-08-28 10:53:32 +01:00
Simon Larsen
c485d1beb1 feat: Enhance error handling for Microsoft Teams provisioning in API responses 2025-08-28 10:36:35 +01:00
Simon Larsen
45f955e73d fix: Improve error handling and logging for Microsoft Teams application access token retrieval 2025-08-28 10:23:52 +01:00
Simon Larsen
6ee9917e60 feat: Add offline_access permission to Microsoft Teams integration documentation for token refresh 2025-08-28 10:14:34 +01:00
Simon Larsen
4b0b3182c2 feat: Add offline_access scope for token refresh in Microsoft Teams integration 2025-08-28 10:12:14 +01:00
Simon Larsen
bad1b899ad feat: Update workspace display names in notification rules for improved clarity 2025-08-28 09:53:52 +01:00
Simon Larsen
b12dbab76f fix: Correct title formatting for Microsoft Teams connection messages across multiple components 2025-08-28 09:43:48 +01:00
Simon Larsen
ec90a58602 feat: Implement project auth token refresh mechanism for Microsoft Teams integration 2025-08-28 09:38:34 +01:00
Simon Larsen
08d591211d feat: Add detailed logging for application token requests and Microsoft Graph API calls to improve debugging 2025-08-28 09:21:20 +01:00
Simon Larsen
10e8f0530f feat: Enhance user experience by auto-selecting the first joined team and prompting team selection when necessary 2025-08-27 21:24:09 +01:00
Simon Larsen
b870de33fa feat: Remove readiness diagnostics endpoint from MicrosoftTeamsAPI 2025-08-27 21:12:27 +01:00
Simon Larsen
d28e6dfece feat: Implement application access token handling for Microsoft Teams integration; add fallback to user token for message posting 2025-08-27 21:11:23 +01:00
Simon Larsen
8d27414c08 Merge branch 'master' into teams-integration 2025-08-27 14:56:50 +01:00
Simon Larsen
ce84cbd93e Merge branch 'master' into teams-integration 2025-08-27 11:33:41 +01:00
Simon Larsen
048661b8ae feat: Add endpoint to fetch available teams for a user and integrate team selection in Microsoft Teams component 2025-08-25 13:30:04 +01:00
Simon Larsen
a041e8eb2d feat: Enhance getTeamId method to retrieve team ID from Microsoft Graph API 2025-08-25 13:22:18 +01:00
Simon Larsen
fc98103519 feat: Add Microsoft Teams channel management methods and Graph API integration 2025-08-25 12:55:42 +01:00
Simon Larsen
a8f4893403 Merge branch 'master' into teams-integration 2025-08-25 10:55:32 +01:00
Simon Larsen
58979ec3e1 feat: Add getByAuthToken method to retrieve user authentication by token 2025-08-22 21:15:10 +01:00
Simon Larsen
09d1d52544 feat: Implement project access token refresh logic for Microsoft Teams integration 2025-08-22 17:46:00 +01:00
Simon Larsen
3d9d2ab6d1 feat: Revise Slack integration documentation to enhance clarity and structure 2025-08-22 15:42:30 +01:00
Simon Larsen
f9332fb678 feat: Add markdown to HTML conversion for Teams message body 2025-08-22 14:20:59 +01:00
Simon Larsen
60f267ee94 feat: Implement Microsoft Teams token refresher for automatic token expiration handling 2025-08-22 13:38:25 +01:00
Simon Larsen
108545e590 feat: Add getByAuthToken method to retrieve project authentication by token 2025-08-22 13:12:18 +01:00
Simon Larsen
64eb4e47bd feat: Enhance Microsoft Teams authentication by adding refresh token and expiration handling 2025-08-22 12:52:37 +01:00
Simon Larsen
ff1b192ef6 feat: Implement block builders for Adaptive Card JSON in MicrosoftTeamsUtil 2025-08-22 11:55:36 +01:00
Simon Larsen
0819d0b565 feat: Fetch notification rules based on the current workspace type 2025-08-22 11:36:33 +01:00
Simon Larsen
66d154fad7 fix: Remove accidental decorative inline code backticks from rendered output 2025-08-22 11:33:54 +01:00
Simon Larsen
49ecf62ecb fix: Remove accidental wrapping backticks from inline code rendering 2025-08-22 11:24:27 +01:00
Simon Larsen
17457bbf3c fix: Remove unnecessary styling from code block in Markdown renderer 2025-08-22 11:23:26 +01:00
Simon Larsen
dd8692b97c fix: Remove unnecessary styling from code block in Markdown renderer 2025-08-22 11:15:22 +01:00
Simon Larsen
b9d22bc6c7 feat: Enhance Docs Renderer with improved styling, unique heading IDs, and additional element overrides 2025-08-22 10:52:54 +01:00
Simon Larsen
2c05f36853 feat: Enhance Microsoft Teams integration with multi-tenant support and ID token handling 2025-08-21 21:55:06 +01:00
Simon Larsen
42404ab02e feat: Add logging for token response errors in Microsoft Teams API 2025-08-21 21:51:03 +01:00
Simon Larsen
e8355a9f08 refactor: Simplify OAuth redirect URIs and enhance state parameter handling for Microsoft Teams integration 2025-08-21 21:24:39 +01:00
Simon Larsen
7c5388e078 fix: Add missing line break for improved code readability in MicrosoftTeamsIntegration component 2025-08-21 21:19:37 +01:00
Simon Larsen
3d2eedec82 feat: Add Microsoft Teams API integration with authentication and app manifest endpoints 2025-08-21 20:34:53 +01:00
Simon Larsen
c052d79949 feat: Implement Microsoft Teams incident actions including acknowledgment, resolution, and note handling
- Added MicrosoftTeamsIncidentActions class with methods for incident actions
- Implemented acknowledgment and resolution of incidents
- Added placeholder methods for viewing and submitting incident notes
- Created action handling logic for various incident-related actions

feat: Add Microsoft Teams monitor actions for viewing monitors

- Introduced MicrosoftTeamsMonitorActions class
- Implemented action handling for viewing monitors

feat: Create Microsoft Teams on-call duty actions for viewing policies

- Added MicrosoftTeamsOnCallDutyActions class
- Implemented action handling for viewing on-call duty policies

feat: Develop Microsoft Teams scheduled maintenance actions

- Introduced MicrosoftTeamsScheduledMaintenanceActions class
- Implemented actions for marking maintenance as ongoing/completed
- Added functionality for viewing and submitting scheduled maintenance notes
- Created modals for changing maintenance states and adding notes
2025-08-21 20:28:41 +01:00
Simon Larsen
5326bbe3be feat: Add Microsoft Teams utility class for channel and message management 2025-08-21 20:27:42 +01:00
Simon Larsen
9df7e58127 feat: Implement Microsoft Teams authentication request handling 2025-08-21 20:27:10 +01:00
Simon Larsen
1f0f92b133 refactor: Replace SlackMiscData with MiscData in WorkspaceUserAuthTokenService and update userId handling in WorkspaceUtil 2025-08-21 20:26:06 +01:00
Simon Larsen
9bb6c2d8bf refactor: Update SlackSettings to Settings in WorkspaceSetting and WorkspaceSettingService 2025-08-21 20:25:21 +01:00
Simon Larsen
32c93575b1 feat: Add Microsoft Teams action types enumeration 2025-08-21 18:02:36 +01:00
Simon Larsen
fb8dca8d1c feat: Implement Microsoft Teams integration component 2025-08-21 18:01:54 +01:00
Simon Larsen
4707e16318 refactor: Remove 'Coming Soon' feature from Microsoft Teams integration page 2025-08-21 17:31:43 +01:00
Simon Larsen
eadb24b7c2 refactor: Remove 'Coming Soon' feature from Microsoft Teams integration page 2025-08-21 17:31:36 +01:00
Simon Larsen
dc22bececf feat: Add Microsoft Teams App Client ID configuration to environment variables 2025-08-21 17:30:26 +01:00
Simon Larsen
768281d1f4 feat: Add Microsoft Teams integration documentation component 2025-08-21 17:30:09 +01:00
Simon Larsen
3a994e98d7 feat: Implement Microsoft Teams integration page and remove 'Coming Soon' placeholder 2025-08-21 17:29:56 +01:00
Simon Larsen
2a41dfe5bb refactor: Remove 'Coming Soon' feature from Microsoft Teams integration pages 2025-08-21 17:28:34 +01:00
Simon Larsen
4fc697d552 feat: Add Microsoft Teams API integration and environment configuration variables 2025-08-21 17:22:02 +01:00
Simon Larsen
01e619a283 feat: Add Microsoft Teams app configuration variables to environment and Docker Compose files 2025-08-21 17:20:27 +01:00
Simon Larsen
16e049a564 feat: Add Microsoft Teams app configuration options to Helm chart 2025-08-21 17:20:12 +01:00
Simon Larsen
258870fe2e fix: Correct changelog delimiter in release workflow 2025-08-21 16:55:03 +01:00
Simon Larsen
7341b6170a chore: Remove Microsoft Teams integration files
- Deleted unused Microsoft Teams related files including message handling and action types to streamline the codebase.
- This cleanup helps in maintaining focus on active integrations and reduces potential confusion with deprecated code.
2025-08-21 14:50:00 +01:00
53 changed files with 7039 additions and 890 deletions

View File

@@ -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
}

View File

@@ -28,7 +28,7 @@
]
},
"dependencies": {
"Common": "file:../Common",
"Common": "link:../Common",
"ejs": "^3.1.10",
"react": "^18.3.1",

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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(),

View File

@@ -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: [

View File

@@ -109,7 +109,7 @@ class WorkspaceSetting extends BaseModel {
unique: false,
nullable: false,
})
public settings?: SlackSettings = undefined;
public settings?: Settings = undefined;
@ColumnAccessControl({
create: [],

View 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;
}
}

View File

@@ -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;

View File

@@ -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]:

View File

@@ -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: {

View File

@@ -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();

View File

@@ -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: {

View File

@@ -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;

View 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);
}
}
}

View 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}`,
);
}
}
}

View 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"),
);
}
}
}

View File

@@ -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"),
);
}
}

View File

@@ -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"),
);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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,

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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>) => {

View File

@@ -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,

View File

@@ -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 () => {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
)}

View File

@@ -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>
)}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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> {

View File

@@ -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.

View File

@@ -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))

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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=[::]:

View File

@@ -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: