Compare commits

...

252 Commits

Author SHA1 Message Date
Simon Larsen
30940991e0 feat: add endpoint to calculate uptime percentage for status page resources 2025-02-20 19:07:45 +00:00
Simon Larsen
a6d3047007 refactor: comment out workspace connections sections in side menus 2025-02-20 14:05:40 +00:00
Simon Larsen
2ae0d139ea refactor: simplify WorkflowTimeoutInMs declaration in EnvironmentConfig 2025-02-20 14:02:49 +00:00
Simon Larsen
1507583cf3 feat: add workflow timeout configuration and update related files 2025-02-20 14:00:28 +00:00
Simon Larsen
310e1b764c fix: log complete request body instead of just code in VM sandbox execution 2025-02-20 13:51:40 +00:00
Simon Larsen
1d7a25f2bb refactor: clean up code formatting and remove unnecessary whitespace in various files 2025-02-14 21:46:02 +00:00
Simon Larsen
3bc89a8cae feat: add migration to update postUpdatesToWorkspaceChannels column type to JSONB in Incident, Alert, and ScheduledMaintenance tables 2025-02-14 21:43:06 +00:00
Simon Larsen
ff54b5a26c feat: add notification event type support in WorkspaceNotificationRuleService and update NotificationRuleForm validation 2025-02-14 21:41:46 +00:00
Simon Larsen
07b9f38e90 feat: enhance Notification Rule form to support new channel template name and improve validation messages for existing channels 2025-02-14 19:45:40 +00:00
Simon Larsen
f612940668 feat: update Alert, Incident, and ScheduledMaintenance models to support multiple workspace channels and change column types to JSON 2025-02-14 18:35:44 +00:00
Simon Larsen
6046bda443 feat: update WorkspaceNotificationRuleService to return created channels and enhance WorkspaceChannel interface 2025-02-14 17:53:40 +00:00
Simon Larsen
cd97a72ef2 refactor: clean up whitespace and improve formatting in workspace utility classes 2025-02-14 17:51:05 +00:00
Simon Larsen
c20fbcc7a4 fix: correct spelling errors in comments and update variable names for clarity 2025-02-14 17:27:27 +00:00
Simon Larsen
f22e410860 feat: implement MicrosoftTeams workspace utility and enhance notification rule interfaces 2025-02-14 16:28:02 +00:00
Simon Larsen
3059ca848e refactor: reorganize Slack utility imports and introduce CreateChannelNotificationRule interface 2025-02-14 15:50:20 +00:00
Simon Larsen
ce8b8c3b58 refactor: update import path for IncidentNotificationRule to improve module organization 2025-02-14 13:13:29 +00:00
Simon Larsen
78846a3f95 refactor: improve code formatting and add documentation for non-HTTP methods in API reference 2025-02-14 13:12:44 +00:00
Simon Larsen
994f329a49 fix: update example API request URLs to use dynamic ID placeholders 2025-02-14 13:00:44 +00:00
Simon Larsen
d621df58cd refactor: remove obsolete migration file and clean up Slack utility code 2025-02-14 12:48:26 +00:00
Simon Larsen
0e0ccd9651 refactor: update method visibility and improve naming consistency in notification rule conditions 2025-02-14 12:36:54 +00:00
Simon Larsen
cc0adea216 refactor: remove unnecessary blank line in BaseAPI 2025-02-14 12:30:08 +00:00
Simon Larsen
8ec0825a52 feat: add update and delete item routes to BaseAPI and improve monitor ID handling in WorkspaceNotificationRuleService 2025-02-14 12:26:47 +00:00
Simon Larsen
4d083f9663 feat: refactor notification rule service to support new notification rule types and improve channel handling 2025-02-14 12:16:02 +00:00
Simon Larsen
81b3795b1c feat: update migration and interfaces to rename Slack channel references to Workspace channel for consistency 2025-02-14 09:58:10 +00:00
Simon Larsen
74cf9ae184 feat: rename Slack channel references to Workspace channel for consistency across models 2025-02-12 17:58:29 +00:00
Simon Larsen
9ba4c0bfdd feat: implement validation for notification rule form inputs to enhance user experience 2025-02-12 17:49:07 +00:00
Simon Larsen
85e83c6822 feat: rename inviteOwnersToNewChannel to shouldInviteOwnersToNewChannel for clarity and update NotificationRuleForm to include new toggle option 2025-02-12 17:33:05 +00:00
Simon Larsen
65eedde511 feat: reintroduce AlertNotificationRule interface and update NotificationRuleForm to support new channel creation and user invitations 2025-02-12 17:29:46 +00:00
Simon Larsen
7d45056ae4 feat: update notification rule form to support dynamic workspace type for channel creation and user invitations 2025-02-12 17:16:52 +00:00
Simon Larsen
f2db382087 feat: introduce new notification rule types for monitor status, alerts, and scheduled maintenance with updated logic for channel management 2025-02-12 17:14:18 +00:00
Simon Larsen
3e99783119 feat: add migration to rename Slack channel ID columns to channel name for improved clarity 2025-02-12 15:36:20 +00:00
Simon Larsen
de6bbbee8c feat: rename Slack channel ID references to channel name and update related logic for improved clarity 2025-02-12 15:35:17 +00:00
Simon Larsen
0309c2d7e8 feat: enhance Slack notification rule handling by adding channel creation logic and user invitation functionality 2025-02-12 14:23:33 +00:00
Simon Larsen
1c9bb0605b feat: update notification rule conditions and enhance incident service methods for improved clarity and functionality 2025-02-12 13:58:03 +00:00
Simon Larsen
b8b98be7a0 feat: rename WorkspacePayloadBlock to WorkspaceMessageBlock and update related methods in WorkspaceNotificationRuleService 2025-02-11 19:20:53 +00:00
Simon Larsen
6c037b0996 feat: rename WorkspaceNotificationPayload to WorkspaceMessagePayload and update related services 2025-02-11 17:49:29 +00:00
Simon Larsen
326b60c260 feat: enhance Slack notification payload structure and improve incident service methods 2025-02-11 17:17:03 +00:00
Simon Larsen
e13ab0b214 feat: implement getWorkspacePayloadForIncidentCreate method and enhance Slack notification payload structure 2025-02-11 16:25:16 +00:00
Simon Larsen
01f8a27dd2 feat: add postUpdatesToSlackChannelId field to Alert, Incident, and ScheduledMaintenance models for Slack integration 2025-02-11 15:51:52 +00:00
Simon Larsen
081029b49a feat: add postUpdatesToSlackChannelId field to Alert, Incident, and ScheduledMaintenance models for Slack integration 2025-02-11 13:57:33 +00:00
Simon Larsen
32d55fdc46 refactor: improve code formatting and enhance readability in various service and utility files 2025-02-11 13:53:21 +00:00
Simon Larsen
69aa680dd1 feat: update user creation to optionally include full name and enhance Slack notification methods 2025-02-11 13:43:37 +00:00
Simon Larsen
117c02d457 refactor: comment out Workspace Connections section in SideMenu components 2025-02-11 10:17:43 +00:00
Simon Larsen
f74fcb3734 refactor: clean up whitespace and improve code formatting in various components 2025-02-10 21:20:43 +00:00
Simon Larsen
676d6598d7 feat: enhance error handling and messaging in MonitorCriteriaInstance and related components 2025-02-10 21:15:38 +00:00
Simon Larsen
af1edfef99 fix: correct validation error check in MonitorStep and tidy up CriteriaFilters component 2025-02-10 20:50:02 +00:00
Simon Larsen
56cadf01fd feat: implement hasValueField utility method for CriteriaFilter validation 2025-02-10 20:35:22 +00:00
Simon Larsen
6a0ef8d940 refactor: improve code formatting and structure in migration files and notification rule validation logic 2025-02-10 20:21:43 +00:00
Simon Larsen
1e01942218 feat: implement validation logic for Slack notification rules in NotificationRuleConditionUtil 2025-02-10 20:16:59 +00:00
Simon Larsen
5dab4f8042 feat: add scheduledMaintenanceNumber column to ScheduledMaintenance table and create corresponding index 2025-02-10 19:54:52 +00:00
Simon Larsen
3e4a50d430 refactor: clean up code by removing unnecessary blank lines in Alert and ScheduledMaintenance models, migration files, and services 2025-02-10 18:54:54 +00:00
Simon Larsen
96d236b034 feat: add scheduledMaintenanceNumber to ScheduledMaintenance model and implement related logic in service and UI components 2025-02-10 18:50:52 +00:00
Simon Larsen
ade5e69aa0 feat: add alertNumber column to Alert table and create corresponding index 2025-02-10 18:06:55 +00:00
Simon Larsen
4733c710b2 feat: add alertNumber field to Alert model and implement related logic in AlertService and UI components 2025-02-10 18:02:54 +00:00
Simon Larsen
1c8922249e feat: add WorkspaceNotificationPayload interface and update notifyWorkspace method to accept new payload structure 2025-02-10 17:55:50 +00:00
Simon Larsen
73c787836f feat: introduce WorkspaceType and NotificationRuleEventType enums; implement SlackNotificationRule interface and related services 2025-02-10 17:43:49 +00:00
Simon Larsen
ec6f3d84d7 refactor: rename ServiceProviderType to WorkspaceType and update related components for consistency 2025-02-10 17:40:07 +00:00
Simon Larsen
1f2df5f3ee refactor: rename initialValue prop to value in notification rule components for consistency 2025-02-10 14:47:33 +00:00
Simon Larsen
121a78ea8d refactor: improve code formatting and readability in form components 2025-02-10 14:42:34 +00:00
Simon Larsen
832ab4ab24 Merge branch 'master' of github.com:OneUptime/oneuptime 2025-02-10 14:25:33 +00:00
Simon Larsen
445f3906ee feat: improve notification rule forms and conditions; enhance filter handling and UI elements 2025-02-10 14:25:30 +00:00
Simon Larsen
c869acc0e5 Merge pull request #1831 from diabolocom/nginx_ipv6
Feature: Nginx: Allow to specify listen options
2025-02-10 13:59:24 +00:00
Simon Larsen
0d2b2a272b feat: enhance notification rule conditions and form fields with additional labels and descriptions 2025-02-10 13:18:37 +00:00
Simon Larsen
d8ac1c39b7 feat: add option to show horizontal rule in form fields 2025-02-10 13:03:21 +00:00
Simon Larsen
e31dbe935c feat: enhance Slack integration and notification rule forms with additional descriptions and conditions 2025-02-10 12:59:00 +00:00
Jules Lefebvre
35e46cebfc feat(helm): add nginx listen config values
Add the posibility to define `NGINX_LISTEN_ADDRESS` and `NGINX_LISTEN_OPTIONS` via the `nginx.listenAddress` and `nginx.listenOptions` to allow to listen on all single and dual stack
2025-02-10 13:58:28 +01:00
Jules Lefebvre
891861f396 feat(docker-compose): add nginx listen enviroment variables
Add the posibility to define `NGINX_LISTEN_ADDRESS` and `NGINX_LISTEN_OPTIONS` to allow to listen on all single and dual stack
2025-02-10 13:57:36 +01:00
Jules Lefebvre
fbc38230b8 feat(nginx): allow to specify listen options 2025-02-10 13:44:35 +01:00
Simon Larsen
61561f9745 fix: add missing comma in schema migrations index for consistency 2025-02-10 12:25:09 +00:00
Simon Larsen
13e5f57160 feat: add migration for ServiceProviderUserAuthToken table and update index; log project creation for user 2025-02-10 11:43:03 +00:00
Simon Larsen
7600085473 feat: add migration for ServiceProviderUserAuthToken, ServiceProviderProjectAuthToken, ServiceProviderSetting, and ServiceProviderNotificationRule tables 2025-02-10 11:29:53 +00:00
Simon Larsen
99641d6994 refactor: remove obsolete migration files and clean up schema migrations index 2025-02-10 11:25:27 +00:00
Simon Larsen
dea66cc8d8 refactor: improve formatting of radioButtonOptions mapping in FormField component 2025-02-07 20:02:25 +00:00
Simon Larsen
1dd43a69a0 refactor: clean up formatting and improve readability in various components 2025-02-07 19:43:09 +00:00
Simon Larsen
e539cb7ae3 feat: add onBeforeCreate handler to ServiceProviderNotificationRulesTable for event type and project ID assignment 2025-02-07 19:33:50 +00:00
Simon Larsen
5269f7a164 feat: replace RadioButton with OptionChooserButton in form field schema and update related components 2025-02-07 19:29:02 +00:00
Simon Larsen
8e1d6b420f feat: enhance SlackIntegration component with connection handlers and improve notification rule naming 2025-02-07 19:10:30 +00:00
Simon Larsen
311f7dbb5b refactor: rename connection state variables for clarity in SlackIntegration component 2025-02-07 18:51:54 +00:00
Simon Larsen
9ac64b5873 feat: add color selection to ServiceProviderNotificationRulesTable and update SlackIntegration component props 2025-02-07 18:45:56 +00:00
Simon Larsen
e54126e6bf refactor: clean up permission definitions and formatting in ServiceProvider models 2025-02-07 18:36:56 +00:00
Simon Larsen
a4acc59505 fix: remove unnecessary permissions from ServiceProviderNotificationRule update access control 2025-02-07 18:31:35 +00:00
Simon Larsen
854bc297a6 feat: add permissions for Service Provider Notification Rules and update access control in ServiceProviderProjectAuthToken 2025-02-07 18:29:34 +00:00
Simon Larsen
69bfb48573 refactor: rename Settings component to SlackIntegrationPage for clarity 2025-02-07 18:18:20 +00:00
Simon Larsen
9ef7f720b1 refactor: improve code formatting and readability in ServiceProviderNotificationRulesTable and SlackIntegration components 2025-02-07 18:14:42 +00:00
Simon Larsen
2155dcad65 fix: correct spelling of NotificationRuleForm and NotificationRuleViewElement components; remove unused ServiceProviderNotificationRules component 2025-02-07 18:10:58 +00:00
Simon Larsen
fb4da29ade fix: correct URL formatting for social media and blog links in BlogPostUtil 2025-02-07 14:04:14 +00:00
Simon Larsen
d146d33059 Merge branch 'master' of github.com:OneUptime/oneuptime 2025-02-07 11:03:51 +00:00
Simon Larsen
978ac9155f refactor: update event type cases in NotificationRuleConditionUtil for consistency 2025-02-07 11:03:34 +00:00
Simon Larsen
cf9b4f0eba refactor: simplify value extraction in NotificationRuleConditionElement 2025-02-07 11:01:58 +00:00
Simon Larsen
d26a2aa995 refactor: update type annotations for selected alert and incident conditions in NotificationRuleConditionElement 2025-02-07 10:56:15 +00:00
Simon Larsen
c8301e21eb feat: update NotificationRule components to include event type in titles and descriptions 2025-02-07 10:53:25 +00:00
Simon Larsen
1a24232a1c refactor: improve code formatting and readability in AlertSeverityElement, IncidentStateElement, and NotificationRuleForm components 2025-02-07 10:42:06 +00:00
Simon Larsen
69c94835fe Merge pull request #1830 from OneUptime/slack-app
Slack app
2025-02-07 10:26:00 +00:00
Simon Larsen
706a9145ac feat: add NotificationRuleConditions and NotificationRuleForm components for enhanced notification rule management 2025-02-06 19:57:05 +00:00
Simon Larsen
9bde6194a5 feat: add NotificationRuleConditionElement and related components for alert and incident states 2025-02-06 19:29:55 +00:00
Simon Larsen
3dd8c62bc4 feat: enhance SlackNotificationRule and NotificationRuleForm with new fields and conditions 2025-02-06 18:47:12 +00:00
Simon Larsen
895a434d0e refactor: clean up code formatting and improve readability in various files 2025-02-06 17:49:01 +00:00
Simon Larsen
d3ec3a60df feat: implement NotificationRuleForm component and utility functions for condition checks 2025-02-06 14:25:39 +00:00
Simon Larsen
df15a2dcae feat: add name and description fields to ServiceProviderNotificationRule model and update migration 2025-02-06 12:18:04 +00:00
Simon Larsen
5e4b24dcfb feat: refactor notification rule models to use BaseNotificationRule and update filter conditions 2025-02-06 11:14:52 +00:00
Simon Larsen
0eae73c4e5 feat: add ServiceProviderNotificationRule model and service with migration 2025-02-06 11:03:43 +00:00
Simon Larsen
deb3f81e5d Merge pull request #1817 from OneUptime/slack-app
Slack App
2025-02-05 19:22:02 +00:00
Simon Larsen
dcb3fe6f69 feat: add ServiceProviderSetting model and service with CRUD operations 2025-02-05 17:33:45 +00:00
Simon Larsen
4bbecb3013 feat: refactor ServiceProviderProjectAuthToken and ServiceProviderSetting models, add deletedByUserId field 2025-02-05 16:25:13 +00:00
Simon Larsen
52954f3702 feat: add migration for ServiceProviderUserAuthToken and ServiceProviderProjectAuthToken tables 2025-02-05 16:17:37 +00:00
Simon Larsen
12a7fff668 refactor: update ServiceProviderUserAuthToken to use ServiceProviderType enum 2025-02-05 16:12:14 +00:00
Simon Larsen
14e489a719 feat: implement ServiceProviderType enum and refactor auth token services for Slack and Microsoft Teams 2025-02-05 16:09:50 +00:00
Simon Larsen
83f2935f41 fix: update Slack integration page URL to reflect new settings path 2025-02-05 16:00:49 +00:00
Simon Larsen
28855d482e 2025-02-05 15:56:15 +00:00
Simon Larsen
7c11ebdacf refactor: rename "Workspace Integrations" to "Workspace Connections" in SideMenu 2025-02-05 14:10:17 +00:00
Simon Larsen
592c806465 feat: add user and project auth token services to BaseAPI routing 2025-02-05 14:09:36 +00:00
Simon Larsen
7a3d0266c4 chore: update SlackAPI comments to use sample access tokens for clarity 2025-02-05 14:00:17 +00:00
Simon Larsen
e9b7368cf1 refactor: clean up code by removing unnecessary whitespace and improving formatting 2025-02-05 13:58:27 +00:00
Simon Larsen
f8c0004f85 feat: add Slack integration section to settings menu and routing 2025-02-05 13:45:26 +00:00
Simon Larsen
b007cb8bbd feat: implement Slack integration settings page and enhance auth token services for user and project 2025-02-05 13:43:22 +00:00
Simon Larsen
541015766c feat: rename service type to service provider type and add new fields for user and project IDs in service provider 2025-02-05 11:55:17 +00:00
Simon Larsen
5310087287 feat: update Slack app manifest by refining user and bot scopes and adding a temporary manifest file 2025-02-04 19:33:32 +00:00
Simon Larsen
7649c6c566 feat: update Slack integration authorization URL and add app manifest for enhanced functionality 2025-02-04 19:23:09 +00:00
Simon Larsen
1b2650f6df feat: expand Slack app manifest scopes for enhanced functionality 2025-02-04 19:14:02 +00:00
Simon Larsen
de3586d60e feat: update Slack integration error handling and redirect logic in user settings 2025-02-04 19:08:17 +00:00
Simon Larsen
a0ca579c2f feat: enhance Slack integration error handling and redirect logic in user settings 2025-02-04 19:04:39 +00:00
Simon Larsen
9bc7a115a1 feat: implement Slack app manifest retrieval and enhance scope validation in user settings 2025-02-04 18:51:26 +00:00
Simon Larsen
aef7af0a9a feat: refactor Slack utility imports and add new Slack utility class 2025-02-04 18:35:34 +00:00
Simon Larsen
e9f2e46e16 feat: add endpoint to retrieve Slack app manifest 2025-02-04 18:17:44 +00:00
Simon Larsen
56fd18f7c9 feat: update Slack API with improved error handling and new command shortcuts 2025-02-04 18:16:30 +00:00
Simon Larsen
ecccd8b536 feat: enhance Slack authentication flow with projectId and userId validation 2025-02-04 18:09:35 +00:00
Simon Larsen
dd2f0f37f2 fix: simplify signature validation logic in SlackAuthorization middleware 2025-02-04 18:00:32 +00:00
Simon Larsen
600a5eafe3 fix: update SlackAuthorization middleware to use req.body instead of req.rawBody 2025-02-04 17:54:53 +00:00
Simon Larsen
9b07bf7a08 feat: implement Slack API integration with authorization and event handling 2025-02-04 17:53:59 +00:00
Simon Larsen
d341f6c2b0 feat: enhance Slack integration with error handling and documentation 2025-02-04 15:33:24 +00:00
Simon Larsen
06a030f518 feat: add Slack app client credentials to Helm chart configuration 2025-02-04 14:34:42 +00:00
Simon Larsen
b02fed6e5b feat: add Slack App configuration to environment and UI settings 2025-02-04 14:33:48 +00:00
Simon Larsen
310884bd73 feat: implement Slack OAuth redirection in SlackIntegration component 2025-02-04 14:22:19 +00:00
Simon Larsen
0c625d52c2 feat: update Slack icon SVG paths for improved rendering 2025-02-04 14:11:54 +00:00
Simon Larsen
82ab70f396 refactor: clean up whitespace and formatting in UserSettingsBreadcrumbs and UserSettingsRoutes 2025-02-04 14:08:15 +00:00
Simon Larsen
24d9c9dbc0 feat: add Slack integration to user settings with routing and breadcrumbs 2025-02-04 14:04:49 +00:00
Simon Larsen
d9f1dc9fd2 feat: add UserAuthToken and ProjectAuthToken services and migrations 2025-02-04 13:44:51 +00:00
Simon Larsen
4b503471bd feat: implement Slack status API and initialize routing 2025-02-04 12:40:05 +00:00
Simon Larsen
b523434be3 feat: add Slack app manifest for OneUptime integration 2025-02-04 11:43:22 +00:00
Simon Larsen
f32b1950d9 fix: add return type to sendSubscriptionChangeWebhookSlackNotification method in ProjectService 2025-02-03 18:42:08 +00:00
Simon Larsen
e1c45a5c99 refactor: update property name from 'id' to '_id' and improve Slack message formatting in ProjectService 2025-02-03 18:41:20 +00:00
Simon Larsen
195655b4df refactor: improve code formatting and readability in ProjectService 2025-02-03 18:23:59 +00:00
Simon Larsen
9d7d65f0ef feat: add Slack notification for subscription plan changes in ProjectService 2025-02-03 18:05:08 +00:00
Simon Larsen
985b5410f6 refactor: clean up formatting and improve code readability in environment config and project service 2025-02-03 17:54:41 +00:00
Simon Larsen
d1dd57deec feat: add Slack webhook notifications for user creation, project management, and subscription updates 2025-02-03 17:50:34 +00:00
Simon Larsen
2ec6902537 refactor: improve async handling in BlogPost utility methods 2025-02-03 17:26:01 +00:00
Simon Larsen
cd130bc8ef feat: enhance status page URL handling and add unsubscribe link in subscription emails 2025-01-31 14:34:11 +00:00
Simon Larsen
3fcd1f694e refactor: clean up whitespace and formatting in blog-related files 2025-01-31 14:14:39 +00:00
Simon Larsen
b98b43b9f6 feat: add blog management features and update routing for blog posts 2025-01-31 14:10:54 +00:00
Simon Larsen
b59c76f771 feat: set log limit to 250 in TraceExplorer and Logs components 2025-01-30 20:35:46 +00:00
Simon Larsen
4d2e386328 feat: set default log limit to 250 in DashboardLogsViewer 2025-01-30 17:23:56 +00:00
Simon Larsen
deddcbe152 refactor: add serviceName attribute to telemetry data attributes 2025-01-30 17:00:30 +00:00
Simon Larsen
e305284fe2 refactor: enhance error handling and logging in TelemetryIngest middleware 2025-01-30 16:47:55 +00:00
Simon Larsen
3163debdb8 chore: remove end-to-end test workflow from GitHub Actions 2025-01-30 15:39:59 +00:00
Simon Larsen
f827237a80 refactor: change convertSelectReturnedDataToJson method visibility from private to public in AnalyticsDatabaseService 2025-01-30 15:22:32 +00:00
Simon Larsen
c038e39620 refactor: replace executeQuery calls with execute for consistency in AnalyticsDatabaseService 2025-01-30 15:20:28 +00:00
Simon Larsen
b8c1190c9f refactor: update execute method calls to use executeQuery for consistency in AnalyticsDatabaseService 2025-01-30 14:54:12 +00:00
Simon Larsen
7f5ff5068e refactor: improve code formatting and readability in AnalyticsDatabaseService and LogsViewer components 2025-01-30 14:47:54 +00:00
Simon Larsen
943acc8567 feat: add Clickhouse configuration volume and update AnalyticsDatabaseService to use ResultSet for JSON responses 2025-01-30 11:58:43 +00:00
Simon Larsen
81798211ea chore: update @clickhouse/client and @clickhouse/client-common to version 1.10.1 2025-01-29 21:02:48 +00:00
Simon Larsen
39596f6a42 refactor: replace <div> tags with <pre> for better JSON body rendering in LogsViewer component 2025-01-29 18:35:13 +00:00
Simon Larsen
36a181e77e refactor: replace <pre> tags with <div> for log body rendering in LogsViewer component 2025-01-29 18:25:55 +00:00
Simon Larsen
032c03a877 docs: update health check extension comment to indicate deprecation and suggest upgrade to health_check_v2 2025-01-29 15:42:19 +00:00
Simon Larsen
500299fb2f feat: add liveness, readiness, and startup probes to otel-collector configuration 2025-01-29 15:40:26 +00:00
Simon Larsen
b959e84032 feat: add health check extension to OTel collector configuration 2025-01-29 15:38:57 +00:00
Simon Larsen
30edc194f4 feat: add batch processor configuration for OTLP exporter in collector config template 2025-01-29 14:07:49 +00:00
Simon Larsen
0a20894dd1 feat: add DISABLE_TELEMETRY environment variable to multiple templates for telemetry control 2025-01-28 20:28:03 +00:00
Simon Larsen
1045a7399f fix: change OTLP exporter protocol from http/json to http/protobuf in config template 2025-01-28 19:44:51 +00:00
Simon Larsen
0f3ef0027b fix: update OTLP exporter protocol from http/protobuf to http/json in config template 2025-01-28 19:19:24 +00:00
Simon Larsen
2f43bc5c65 feat: add setup function for ts-node installation in configure script 2025-01-28 18:34:59 +00:00
Simon Larsen
8d9f7e125d feat: add additional breadcrumb links for status pages 2025-01-28 18:27:06 +00:00
Simon Larsen
3ac841ddc1 refactor: improve code formatting and simplify component structure in status pages 2025-01-28 17:27:21 +00:00
Simon Larsen
9fc8c6f7a2 refactor: update route paths and simplify side menu component for status pages 2025-01-28 17:24:22 +00:00
Simon Larsen
62cd974235 refactor: clean up whitespace and improve code formatting in status pages 2025-01-28 13:05:14 +00:00
Simon Larsen
a0d03238ee feat: add announcements route and update breadcrumbs for status pages 2025-01-28 12:55:43 +00:00
Simon Larsen
4303cf00cc feat: add announcements page and update routing for status pages 2025-01-28 12:49:57 +00:00
Simon Larsen
747ea70de5 refactor: update date formatting in scheduled maintenance feed for improved readability 2025-01-28 12:00:19 +00:00
Simon Larsen
6dd4ef22df refactor: enhance scheduled maintenance feed with additional details and improve Recurring class string representation 2025-01-28 11:52:11 +00:00
Simon Larsen
4b97c79ae2 refactor: enhance alert and scheduled maintenance feed updates with additional data fields 2025-01-28 11:36:30 +00:00
Simon Larsen
d34b118c68 refactor: streamline summary prop definition in form components 2025-01-28 11:26:37 +00:00
Simon Larsen
a3ff2e1067 refactor: introduce FormSummaryConfig interface and update form components to use it 2025-01-28 11:23:32 +00:00
Simon Larsen
e6ef2a7945 chore: add concurrency settings to test-release workflow 2025-01-27 19:57:57 +00:00
Simon Larsen
9bf8d5d941 refactor: improve readability of getSummaryElement functions in ScheduledMaintenanceCreate component 2025-01-27 19:53:34 +00:00
Simon Larsen
f3ee93bd48 refactor: remove unused import from Create.tsx in ScheduledMaintenanceEvents 2025-01-27 19:47:06 +00:00
Simon Larsen
5b78fee225 refactor: rename StatusPagesLabel to StatusPagesElement and update imports across components 2025-01-27 19:46:51 +00:00
Simon Larsen
dd73947b7f Merge branch 'release' of github.com:OneUptime/oneuptime into release 2025-01-27 18:25:15 +00:00
Simon Larsen
78998fb123 refactor: simplify form field mapping and improve readability in FormSummary component 2025-01-27 18:22:51 +00:00
Simon Larsen
0bcfccffe0 feat: add margin-bottom to form step title for improved spacing in FormSummary component 2025-01-27 18:19:23 +00:00
Simon Larsen
3ab45f40ca feat: optimize FormSummary component by refining field filtering logic and improving step title styling 2025-01-27 18:16:16 +00:00
Simon Larsen
bb2f610bc8 feat: enhance form summary component to support conditional rendering of form steps and improve layout structure 2025-01-27 18:10:45 +00:00
Simon Larsen
9b685133c4 feat: enhance incident severity and monitor status components with new props for animation and improved data handling 2025-01-27 17:58:21 +00:00
Simon Larsen
33c4943794 refactor: standardize fetch functions to use PromiseVoidFunction type and improve error handling in incident severity, monitors, monitor statuses, and on-call policies components 2025-01-27 17:49:33 +00:00
Simon Larsen
a7f8aa4faa refactor: update state initialization and error handling in Fetch components; improve type definitions for fetch functions 2025-01-27 17:41:27 +00:00
Simon Larsen
722fe30c8f feat: implement FetchMonitors, FetchMonitorStatuses, and FetchOnCallPolicies components; update Field interface for summary element handling 2025-01-27 17:37:08 +00:00
Simon Larsen
500104eb81 feat: add FetchTeams and FetchUsers components; implement label ID handling in IncidentCreate 2025-01-27 17:07:22 +00:00
Simon Larsen
41c3a14dfa fix: change exit code in configure.sh to allow continuation on directory change failure 2025-01-27 16:55:47 +00:00
Simon Larsen
113eda94fa Merge branch 'master' of github.com:OneUptime/oneuptime 2025-01-27 16:51:49 +00:00
Simon Larsen
c6ce43f7cc feat: add FormSummary component and integrate it into BasicForm; implement FetchLabels for incident labels 2025-01-27 16:49:53 +00:00
Simon Larsen
6d5bc60127 fix: update secondTLDs initialization to use split method for better readability 2025-01-27 16:49:46 +00:00
Simon Larsen
b7b7b28834 Merge pull request #1816 from KalvadTech/improve_configure_sh
refactor: modernize configure.sh
2025-01-27 16:48:26 +00:00
Loïc Tosser
5a0b0d7c61 refactor: modernize configure.sh with improved error handling, modularity, and installation process 2025-01-27 18:50:51 +04:00
Simon Larsen
79bf7ce7ee fix: replace props.steps with getFormSteps in BasicForm component 2025-01-27 11:03:31 +00:00
Simon Larsen
4ab150bf75 refactor: clean up code formatting and improve readability in various files 2025-01-27 10:54:46 +00:00
Simon Larsen
951668c982 fix: update debug log messages to specify 'test' for monitor list API 2025-01-27 10:52:59 +00:00
Simon Larsen
d7845407f0 fix: set default limit to 100 in FetchMonitorTest 2025-01-27 10:47:35 +00:00
Simon Larsen
8c5e3187ab feat: add MonitorTestService to services index 2025-01-27 10:44:56 +00:00
Simon Larsen
d2ae1cd845 Merge pull request #1814 from OneUptime/master
chore: update playwright to version 1.50.0 and adjust debugger port i…
2025-01-24 19:18:03 +00:00
Simon Larsen
8cb64fbe66 feat: add isSummaryStep property to BasicForm and FormStep interface 2025-01-24 19:11:49 +00:00
Simon Larsen
092b858873 fix: correct spelling of 'enabled' in BasicForm and update related components 2025-01-24 18:54:28 +00:00
Simon Larsen
81d19722f6 chore: update dependencies in test-release workflow to include infrastructure-agent-deploy 2025-01-24 18:39:11 +00:00
Simon Larsen
1f7b268875 feat: add summary step to BasicForm and implement detail display for form fields 2025-01-24 18:32:10 +00:00
Simon Larsen
38b32a6090 refactor: improve code formatting and consistency in IncidentService 2025-01-24 18:08:21 +00:00
Simon Larsen
373159cb29 feat: enhance incident update feed with detailed information including title, root cause, description, remediation notes, labels, and severity 2025-01-24 17:59:52 +00:00
Simon Larsen
ec86ef4c0e chore: update playwright to version 1.50.0 and adjust debugger port in launch configuration 2025-01-24 16:11:51 +00:00
Simon Larsen
270231374b refactor: clean up migration and improve formatting for subscriber email notification footer text 2025-01-24 10:48:31 +00:00
Simon Larsen
aac4281602 feat: update subscriber email notification footer text to support longer content and add migration 2025-01-24 10:45:51 +00:00
Simon Larsen
bdea1139a4 refactor: improve code formatting for subscriber email notification footer text 2025-01-24 10:36:02 +00:00
Simon Larsen
14fdfa6d17 feat: add subscriber email notification footer text to status page and update notification templates 2025-01-24 10:31:59 +00:00
Simon Larsen
3b22747dbf Merge branch 'release' 2025-01-23 21:51:08 +00:00
Simon Larsen
eb9e20dad5 fix: update OpenTelemetry configuration template to use index for environment variables 2025-01-23 21:31:58 +00:00
Simon Larsen
5aa4b883ad feat: add OpenTelemetry exporter configuration options in Helm chart templates 2025-01-23 21:28:11 +00:00
Simon Larsen
ea38e2621f refactor: clean up code formatting and remove unused props in Scheduled Maintenance components 2025-01-23 16:22:16 +00:00
Simon Larsen
b2cb95e1fc feat: enhance Scheduled Maintenance Table with template creation buttons and update description in create page 2025-01-23 16:17:26 +00:00
Simon Larsen
b174b9795a feat: add imports for form handling and recurring events in Scheduled Maintenance Create page 2025-01-23 16:10:19 +00:00
Simon Larsen
9d1caa8336 feat: add Scheduled Maintenance Event creation page and update routing and breadcrumbs 2025-01-23 16:07:35 +00:00
Simon Larsen
52e8669960 feat: remove unused createInitialValues prop from IncidentsTable component 2025-01-23 15:51:09 +00:00
Simon Larsen
f0505725a7 feat: refine query parameters handling in Route class and improve navigation logic 2025-01-23 15:49:33 +00:00
Simon Larsen
7897641ef7 feat: implement addQueryParams method in Route class and update incident navigation logic 2025-01-23 15:07:11 +00:00
Simon Larsen
761f5f35e9 feat: add query parameters support in navigation and update incident declaration flow 2025-01-23 14:56:59 +00:00
Simon Larsen
241586ff4a feat: add Incident creation page with routing and breadcrumbs integration 2025-01-23 14:39:15 +00:00
Simon Larsen
f8fc1a9dae feat: enhance Monitor layout with hideSideMenu prop and improve component styling 2025-01-23 13:59:24 +00:00
Simon Larsen
6bbcc0a301 feat: add hideSideMenu prop to Monitor layout and update routing logic 2025-01-23 13:43:58 +00:00
Simon Larsen
0e6604aa11 refactor: update description in Monitor creation component for clarity 2025-01-23 13:24:40 +00:00
Simon Larsen
ade84a23ff refactor: clean up Monitor creation component and improve button navigation 2025-01-23 13:10:20 +00:00
Simon Larsen
6ff883b54e feat: add Monitor creation page and update routing and breadcrumbs 2025-01-23 12:52:27 +00:00
Simon Larsen
3546d92143 refactor: update localRoot paths in launch.json and improve logging in CheckHeartbeat and UpdateConnectionStatus 2025-01-23 12:41:34 +00:00
Simon Larsen
e932eb1b1d feat: update DISABLE_TELEMETRY value to false in otel-collector.yaml 2025-01-22 17:49:30 +00:00
Simon Larsen
cbca712af8 feat: add OpenTelemetry exporter environment variables and disable telemetry in otel-collector.yaml 2025-01-22 17:49:00 +00:00
Simon Larsen
8490128833 feat: add DISABLE_TELEMETRY environment variable to otel-collector.yaml 2025-01-22 15:38:10 +00:00
Simon Larsen
b80e126540 chore: update Playwright and related dependencies to version 1.49.1 2025-01-22 13:53:30 +00:00
Simon Larsen
5494a2244e feat: update Dockerfile.tpl to use new OpenTelemetry Collector version and improve gomplate installation 2025-01-22 12:44:22 +00:00
Simon Larsen
23be5b1736 fix: downgrade collector version to 0.104.0 and enhance config output in Dockerfile.tpl 2025-01-22 12:12:15 +00:00
Simon Larsen
9f3a9bc915 fix: update download command in Dockerfile.tpl to use uname for architecture detection 2025-01-22 11:34:00 +00:00
Simon Larsen
84d322f476 fix: correct echo command syntax in OTelCollector Dockerfile.tpl 2025-01-22 11:15:23 +00:00
Simon Larsen
f94fbcc2ae feat: improve architecture detection and installation process in Dockerfile.tpl 2025-01-22 11:11:14 +00:00
Simon Larsen
0e85162b50 fix: update architecture detection syntax in Dockerfile.tpl for compatibility 2025-01-21 15:59:59 +00:00
Simon Larsen
a5927f3681 feat: refactor Dockerfile.tpl to streamline gomplate installation and architecture detection 2025-01-21 15:58:20 +00:00
Simon Larsen
0d37587199 feat: enhance OpenTelemetry Collector configuration with sending queue parameters 2025-01-21 15:20:50 +00:00
Simon Larsen
4674578c90 feat: update OTel Collector Dockerfile and configuration for gomplate integration 2025-01-21 15:10:44 +00:00
Simon Larsen
87d280edbd feat: use sudo for apt-get update in compile workflow 2025-01-21 13:26:03 +00:00
Simon Larsen
d44ddd6781 feat: extend hard delete retention period from 3 to 30 days in WorkflowLogService 2025-01-21 12:42:50 +00:00
Simon Larsen
7271481fb7 feat: add apt-get update step in compile workflow 2025-01-20 17:08:59 +00:00
266 changed files with 15165 additions and 1932 deletions

View File

@@ -189,6 +189,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: latest
- run: sudo apt-get update
- run: cd Common && npm install
- run: cd E2E && npm install && npm run compile && npm run dep-check

View File

@@ -1,5 +1,9 @@
name: Push Test Images to Docker Hub and GitHub Container Registry
concurrency:
group: test-release
cancel-in-progress: true
on:
push:
branches:
@@ -1526,7 +1530,7 @@ jobs:
test-helm-chart:
runs-on: ubuntu-latest
needs: [llm-docker-image-deploy, open-telemetry-ingest-docker-image-deploy, copilot-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, api-reference-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-ingest-docker-image-deploy, probe-docker-image-deploy, haraka-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy, fluent-ingest-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
needs: [infrastructure-agent-deploy, llm-docker-image-deploy, open-telemetry-ingest-docker-image-deploy, copilot-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, api-reference-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-ingest-docker-image-deploy, probe-docker-image-deploy, haraka-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy, fluent-ingest-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:

View File

@@ -1,58 +0,0 @@
name: E2E Tests
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
# Docker compose needs a lot of space to build images, so we need to free up some space first in the GitHub Actions runner
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: npm run prerun && bash ./Tests/Scripts/enable-billing-env-var.sh
- run: npm run dev
- name: Wait for server to start
run: bash ./Tests/Scripts/status-check.sh http://localhost
- name: Run E2E Tests. Run docker container e2e in docker compose file
run: export $(grep -v '^#' config.env | xargs) && docker compose -f docker-compose.dev.yml up --exit-code-from e2e --abort-on-container-exit e2e || (docker compose -f docker-compose.dev.yml logs e2e && exit 1)
- name: Upload test results
uses: actions/upload-artifact@v4
# Run this on failure
if: failure()
with:
# Name of the artifact to upload.
# Optional. Default is 'artifact'
name: test-results
# A file, directory or wildcard pattern that describes what to upload
# Required.
path: |
./E2E
# Duration after which artifact will expire in days. 0 means using default retention.
# Minimum 1 day.
# Maximum 90 days unless changed from the repository settings page.
# Optional. Defaults to repository settings.
retention-days: 7

22
.vscode/launch.json vendored
View File

@@ -93,7 +93,7 @@
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/Worker",
"localRoot": "${workspaceFolder}/Workflow",
"name": "Workflow: Debug with Docker",
"port": 8735,
"remoteRoot": "/usr/src/app",
@@ -107,7 +107,7 @@
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/Worker",
"localRoot": "${workspaceFolder}/Docs",
"name": "Docs: Debug with Docker",
"port": 8738,
"remoteRoot": "/usr/src/app",
@@ -121,7 +121,7 @@
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/Worker",
"localRoot": "${workspaceFolder}/APIReference",
"name": "API Reference: Debug with Docker",
"port": 8737,
"remoteRoot": "/usr/src/app",
@@ -151,7 +151,7 @@
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/Probe",
"name": "Probe: Debug with Docker",
"port": 9655,
"port": 9229,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
@@ -259,20 +259,6 @@
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/Workers",
"name": "Workers: Debug with Docker",
"port": 9654,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/StatusPage",

View File

@@ -12,14 +12,14 @@
</h2>
<script>
function showPermissions(id){
var permissionsblock = document.getElementById(id+"-permissions");
var viewPermissionsBtn = document.getElementById(id+"-view-permissions");
function showPermissions(id) {
var permissionsblock = document.getElementById(id + "-permissions");
var viewPermissionsBtn = document.getElementById(id + "-view-permissions");
if(permissionsblock.style.display === "none"){
if (permissionsblock.style.display === "none") {
permissionsblock.style.display = "block";
viewPermissionsBtn.innerHTML = "Hide Permissions";
}else{
} else {
permissionsblock.style.display = "none";
viewPermissionsBtn.innerHTML = "View Permissions";
}
@@ -48,11 +48,16 @@
<dt class="sr-only">Description</dt>
<dd class="w-full flex-none text-sm [&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0">
<p>
<%= pageData.columns[Object.keys(pageData.columns)[i]].description -%> <a class="text-gray-500 hover:underline cursor-pointer text-xs" id="<%= Object.keys(pageData.columns)[i] -%>-view-permissions" onclick="showPermissions('<%= Object.keys(pageData.columns)[i] -%>')">View Permissions</a>
<%= pageData.columns[Object.keys(pageData.columns)[i]].description -%> <a
class="text-gray-500 hover:underline cursor-pointer text-xs"
id="<%= Object.keys(pageData.columns)[i] -%>-view-permissions"
onclick="showPermissions('<%= Object.keys(pageData.columns)[i] -%>')">View
Permissions</a>
</p>
</dd>
<dd class="font-mono text-xs" style="display: none;" id="<%= Object.keys(pageData.columns)[i] -%>-permissions">
<dd class="font-mono text-xs" style="display: none;"
id="<%= Object.keys(pageData.columns)[i] -%>-permissions">
<div class="mb-3 mt-3">
<span class="text-gray-700 text-xs">Permissions to Create:&nbsp;</span>
@@ -319,8 +324,7 @@
<div class="[&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0 xl:sticky xl:top-24">
<%- include('../partials/code', {title: "Example Item Request" , requestUrl:
pageData.apiPath+"/3599ee69-43a7-42d7/get-item", code: pageData.itemRequest, requestType: "POST" })
-%>
pageData.apiPath+"/:id/get-item", code: pageData.itemRequest, requestType: "POST" }) -%>
<%- include('../partials/code', {title: "Example Item Response" , code: pageData.itemResponse,
requestType: "" }) -%>
</div>
@@ -469,11 +473,34 @@
</ul>
</div>
<div class="border border-gray-100 bg-gray-50 rounded-md p-4 text-sm mt-10">
<h4 class="font-semibold text-gray-700 ">For clients that do not support PUT requests<h4>
<p class="text-gray-500 text-xs mt-4">
You can also update an object by sending a POST or GET request to these endpoints with the
same
request headers and body.
</p>
<div class="flex items-center gap-x-3 mt-10"><span
class="font-mono text-[0.625rem] font-semibold leading-6 rounded-lg px-1.5 ring-1 ring-inset ring-sky-300 bg-sky-400/10 text-sky-500 ">POST</span><span
class="h-0.5 w-0.5 rounded-full bg-zinc-300 "></span><span
class="font-mono text-xs text-zinc-400">
<%= pageData.apiPath -%>/:id/update-item
</span></div>
<div class="flex items-center gap-x-3 mt-10"><span
class="font-mono text-[0.625rem] font-semibold leading-6 rounded-lg px-1.5 ring-1 ring-inset ring-emerald-300 bg-emerald-400/10 text-emerald-500 ">GET</span><span
class="h-0.5 w-0.5 rounded-full bg-zinc-300 "></span><span
class="font-mono text-xs text-zinc-400">
<%= pageData.apiPath -%>/:id/update-item
</span></div>
</div>
</div>
<div class="[&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0 xl:sticky xl:top-24">
<%- include('../partials/code', {title: "Example Update Request" , requestUrl:
pageData.apiPath+"/3599ee69-43a7-42d7", code: pageData.updateRequest, requestType: "PUT" }) -%>
pageData.apiPath+"/:id", code: pageData.updateRequest, requestType: "PUT" }) -%>
<%- include('../partials/code', {title: "Example Update Response" , code: pageData.updateResponse,
requestType: "" }) -%>
</div>
@@ -498,11 +525,34 @@
<p>This endpoint allows you to delete object by its ID. </p>
<div class="border border-gray-100 bg-gray-50 rounded-md p-4 text-sm mt-10">
<h4 class="font-semibold text-gray-700 ">For clients that do not support DELETE requests<h4>
<p class="text-gray-500 text-xs mt-4">
You can also delete an object by sending a POST or GET request to these endpoints with the
same
request headers and body.
</p>
<div class="flex items-center gap-x-3 mt-10"><span
class="font-mono text-[0.625rem] font-semibold leading-6 rounded-lg px-1.5 ring-1 ring-inset ring-sky-300 bg-sky-400/10 text-sky-500 ">POST</span><span
class="h-0.5 w-0.5 rounded-full bg-zinc-300 "></span><span
class="font-mono text-xs text-zinc-400">
<%= pageData.apiPath -%>/:id/delete-item
</span></div>
<div class="flex items-center gap-x-3 mt-10"><span
class="font-mono text-[0.625rem] font-semibold leading-6 rounded-lg px-1.5 ring-1 ring-inset ring-emerald-300 bg-emerald-400/10 text-emerald-500 ">GET</span><span
class="h-0.5 w-0.5 rounded-full bg-zinc-300 "></span><span
class="font-mono text-xs text-zinc-400">
<%= pageData.apiPath -%>/:id/delete-item
</span></div>
</div>
</div>
<div class="[&amp;>:first-child]:mt-0 [&amp;>:last-child]:mb-0 xl:sticky xl:top-24">
<%- include('../partials/code', {title: "Example Delete Request" , requestUrl:
pageData.apiPath+"/3599ee69-43a7-42d7", code: pageData.deleteRequest, requestType: "DELETE" }) -%>
pageData.apiPath+"/:id", code: pageData.deleteRequest, requestType: "DELETE" }) -%>
<%- include('../partials/code', {title: "Example Delete Response" , code: pageData.deleteResponse,
requestType: "" }) -%>
</div>

View File

@@ -137,7 +137,7 @@ const DashboardProjectPicker: FunctionComponent<ComponentProps> = (
minLength: 6,
},
footerElement: getFooter(),
fieldType: FormFieldSchemaType.RadioButton,
fieldType: FormFieldSchemaType.OptionChooserButton,
radioButtonOptions: SubscriptionPlan.getSubscriptionPlans(
getAllEnvVars(),
).map((plan: SubscriptionPlan): RadioButton => {

View File

@@ -75,7 +75,7 @@ const Projects: FunctionComponent = (): ReactElement => {
minLength: 6,
},
footerElement: getFooter(),
fieldType: FormFieldSchemaType.RadioButton,
fieldType: FormFieldSchemaType.OptionChooserButton,
radioButtonOptions: SubscriptionPlan.getSubscriptionPlans(
getAllEnvVars(),
).map((plan: SubscriptionPlan): RadioButton => {

View File

@@ -512,6 +512,29 @@ import ScheduledMaintenanceFeedService, {
Service as ScheduledMaintenanceFeedServiceType,
} from "Common/Server/Services/ScheduledMaintenanceFeedService";
import SlackAPI from "Common/Server/API/SlackAPI";
import WorkspaceProjectAuthToken from "Common/Models/DatabaseModels/WorkspaceProjectAuthToken";
import WorkspaceProjectAuthTokenService, {
Service as WorkspaceProjectAuthTokenServiceType,
} from "Common/Server/Services/WorkspaceProjectAuthTokenService";
import WorkspaceUserAuthToken from "Common/Models/DatabaseModels/WorkspaceUserAuthToken";
import WorkspaceUserAuthTokenService, {
Service as WorkspaceUserAuthTokenServiceType,
} from "Common/Server/Services/WorkspaceUserAuthTokenService";
import WorkspaceSetting from "Common/Models/DatabaseModels/WorkspaceSetting";
import WorkspaceSettingService, {
Service as WorkspaceSettingServiceType,
} from "Common/Server/Services/WorkspaceSettingService";
import WorkspaceNotificationRule from "Common/Models/DatabaseModels/WorkspaceNotificationRule";
import WorkspaceNotificationRuleService, {
Service as WorkspaceNotificationRuleServiceType,
} from "Common/Server/Services/WorkspaceNotificationRuleService";
const BaseAPIFeatureSet: FeatureSet = {
init: async (): Promise<void> => {
const app: ExpressApplication = Express.getExpressApp();
@@ -534,6 +557,19 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
// notification rule
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
WorkspaceNotificationRule,
WorkspaceNotificationRuleServiceType
>(
WorkspaceNotificationRule,
WorkspaceNotificationRuleService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<MonitorTest, MonitorTestServiceType>(
@@ -542,6 +578,15 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
//service provider setting
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<WorkspaceSetting, WorkspaceSettingServiceType>(
WorkspaceSetting,
WorkspaceSettingService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentFeed, IncidentFeedServiceType>(
@@ -574,6 +619,26 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
WorkspaceProjectAuthToken,
WorkspaceProjectAuthTokenServiceType
>(
WorkspaceProjectAuthToken,
WorkspaceProjectAuthTokenService,
).getRouter(),
);
// user auth token
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<WorkspaceUserAuthToken, WorkspaceUserAuthTokenServiceType>(
WorkspaceUserAuthToken,
WorkspaceUserAuthTokenService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<Alert, AlertServiceType>(Alert, AlertService).getRouter(),
@@ -1368,6 +1433,7 @@ const BaseAPIFeatureSet: FeatureSet = {
`/${APP_NAME.toLocaleLowerCase()}`,
new ResellerPlanAPI().getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new SlackAPI().getRouter());
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new GlobalConfigAPI().getRouter(),

View File

@@ -36,6 +36,7 @@ import ProjectSSO from "Common/Models/DatabaseModels/ProjectSso";
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
import User from "Common/Models/DatabaseModels/User";
import xml2js from "xml2js";
import Name from "Common/Types/Name";
const router: ExpressRouter = Express.getRouter();
@@ -283,6 +284,7 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
let issuerUrl: string = "";
let email: Email | null = null;
let fullName: Name | null = null;
if (!req.params["projectId"]) {
return Response.sendErrorResponse(
@@ -372,6 +374,7 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
issuerUrl = SSOUtil.getIssuer(response);
email = SSOUtil.getEmail(response);
fullName = SSOUtil.getUserFullName(response);
} catch (err: unknown) {
if (err instanceof Exception) {
return Response.sendErrorResponse(req, res, err);
@@ -420,6 +423,7 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
alreadySavedUser = await UserService.createByEmail({
email,
name: fullName || undefined,
isEmailVerified: true,
generateRandomPassword: true,
props: {

View File

@@ -8,6 +8,7 @@ import logger from "Common/Server/Utils/Logger";
import xmlCrypto, { FileKeyInfo } from "xml-crypto";
import xmldom from "xmldom";
import zlib from "zlib";
import Name from "Common/Types/Name";
export default class SSOUtil {
public static createSAMLRequestUrl(data: {
@@ -138,6 +139,88 @@ export default class SSOUtil {
}
}
public static getUserFullName(payload: JSONObject): Name | null {
if (!payload["saml2p:Response"] && !payload["samlp:Response"]) {
return null;
}
payload =
(payload["saml2p:Response"] as JSONObject) ||
(payload["samlp:Response"] as JSONObject) ||
(payload["Response"] as JSONObject);
const samlAssertion: JSONArray =
(payload["saml2:Assertion"] as JSONArray) ||
(payload["saml:Assertion"] as JSONArray) ||
(payload["Assertion"] as JSONArray);
if (!samlAssertion || samlAssertion.length === 0) {
return null;
}
const samlAttributeStatement: JSONArray =
((samlAssertion[0] as JSONObject)[
"saml2:AttributeStatement"
] as JSONArray) ||
((samlAssertion[0] as JSONObject)[
"saml:AttributeStatement"
] as JSONArray) ||
((samlAssertion[0] as JSONObject)["AttributeStatement"] as JSONArray);
if (!samlAttributeStatement || samlAttributeStatement.length === 0) {
return null;
}
const samlAttribute: JSONArray =
((samlAttributeStatement[0] as JSONObject)[
"saml2:Attribute"
] as JSONArray) ||
((samlAttributeStatement[0] as JSONObject)[
"saml:Attribute"
] as JSONArray) ||
((samlAttributeStatement[0] as JSONObject)["Attribute"] as JSONArray);
if (!samlAttribute || samlAttribute.length === 0) {
return null;
}
// get displayName attribute.
// {
// "$": {
// "Name": "http://schemas.microsoft.com/identity/claims/displayname"
// },
// "AttributeValue": [
// "Nawaz Dhandala"
// ]
// },
for (let i: number = 0; i < samlAttribute.length; i++) {
const attribute: JSONObject = samlAttribute[i] as JSONObject;
if (
attribute["$"] &&
(attribute["$"] as JSONObject)["Name"]?.toString()
) {
const name: string | undefined = (attribute["$"] as JSONObject)[
"Name"
]?.toString();
if (
name &&
name === "http://schemas.microsoft.com/identity/claims/displayname" &&
attribute["AttributeValue"] &&
Array.isArray(attribute["AttributeValue"]) &&
attribute["AttributeValue"].length > 0
) {
const fullName: Name = new Name(
attribute["AttributeValue"][0]!.toString() as string,
);
return fullName;
}
}
}
return null;
}
public static getEmail(payload: JSONObject): Email {
if (!payload["saml2p:Response"] && !payload["samlp:Response"]) {
throw new BadRequestException("SAML Response not found.");

View File

@@ -39,7 +39,7 @@
{{> InfoBlock info="No resources have been added to this status page yet."}}
{{/ifCond}}
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}

View File

@@ -10,7 +10,7 @@
{{> DetailBoxField title="" text=announcementDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}

View File

@@ -17,7 +17,7 @@
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}

View File

@@ -14,7 +14,7 @@
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}

View File

@@ -14,7 +14,7 @@
{{> DetailBoxField title="" text=incidentDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}
{{> InfoBlock info=statusPageUrl}}

View File

@@ -21,7 +21,7 @@
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}

View File

@@ -22,7 +22,7 @@
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}

View File

@@ -19,7 +19,7 @@
{{> DetailBoxField title="" text=eventDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat "This is an automated email sent to you because you are subscribed to " statusPageName) }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{> InfoBlock info="You can visit the status page here:"}}

1778
Clickhouse/config.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,7 @@ import {
ManyToOne,
} from "typeorm";
import { TelemetryQuery } from "../../Types/Telemetry/TelemetryQuery";
import { WorkspaceChannel } from "../../Server/Utils/Workspace/WorkspaceBase";
@EnableDocumentation()
@AccessControlColumn("labels")
@@ -1006,4 +1007,51 @@ export default class Alert extends BaseModel {
nullable: true,
})
public telemetryQuery?: TelemetryQuery = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateAlert,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
],
update: [],
})
@Index()
@TableColumn({
isDefaultValueColumn: false,
required: false,
type: TableColumnType.Number,
title: "Alert Number",
description: "Alert Number",
})
@Column({
type: ColumnType.Number,
nullable: true,
})
public alertNumber?: number = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
isDefaultValueColumn: false,
required: false,
type: TableColumnType.JSON,
title: "Post Updates To Workspace Channel Name",
description: "Post Updates To Workspace Channel Name",
})
@Column({
type: ColumnType.JSON,
nullable: true,
})
public postUpdatesToWorkspaceChannels?: Array<WorkspaceChannel> = undefined;
}

View File

@@ -37,6 +37,7 @@ import {
ManyToOne,
} from "typeorm";
import { TelemetryQuery } from "../../Types/Telemetry/TelemetryQuery";
import { WorkspaceChannel } from "../../Server/Utils/Workspace/WorkspaceBase";
@EnableDocumentation()
@AccessControlColumn("labels")
@@ -1120,4 +1121,22 @@ export default class Incident extends BaseModel {
nullable: true,
})
public incidentNumber?: number = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
isDefaultValueColumn: false,
required: false,
type: TableColumnType.JSON,
title: "Post Updates To Workspace Channel Name",
description: "Post Updates To Workspace Channel Name",
})
@Column({
type: ColumnType.JSON,
nullable: true,
})
public postUpdatesToWorkspaceChannels?: Array<WorkspaceChannel> = undefined;
}

View File

@@ -161,10 +161,17 @@ import Dashboard from "./Dashboard";
import MonitorTest from "./MonitorTest";
import ScheduledMaintenanceFeed from "./ScheduledMaintenanceFeed";
import WorkspaceUserAuthToken from "./WorkspaceUserAuthToken";
import WorkspaceProjectAuthToken from "./WorkspaceProjectAuthToken";
import WorkspaceSetting from "./WorkspaceSetting";
import WorkspaceNotificationRule from "./WorkspaceNotificationRule";
const AllModelTypes: Array<{
new (): BaseModel;
}> = [
User,
WorkspaceUserAuthToken,
WorkspaceProjectAuthToken,
Probe,
Project,
EmailVerificationToken,
@@ -343,6 +350,9 @@ const AllModelTypes: Array<{
Dashboard,
MonitorTest,
WorkspaceSetting,
WorkspaceNotificationRule,
];
const modelTypeMap: { [key: string]: { new (): BaseModel } } = {};

View File

@@ -442,6 +442,7 @@ export default class MonitorTest extends BaseModel {
})
public monitorStepProbeResponse?: MonitorStepProbeResponse = undefined;
@Index()
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -35,6 +35,7 @@ import {
ManyToOne,
} from "typeorm";
import Recurring from "../../Types/Events/Recurring";
import { WorkspaceChannel } from "../../Server/Utils/Workspace/WorkspaceBase";
@EnableDocumentation()
@AccessControlColumn("labels")
@@ -950,4 +951,51 @@ export default class ScheduledMaintenance extends BaseModel {
nullable: true,
})
public nextSubscriberNotificationBeforeTheEventAt?: Date = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectScheduledMaintenance,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectScheduledMaintenance,
],
update: [],
})
@Index()
@TableColumn({
isDefaultValueColumn: false,
required: false,
type: TableColumnType.Number,
title: "Scheduled Maintenance Number",
description: "Scheduled Maintenance Number",
})
@Column({
type: ColumnType.Number,
nullable: true,
})
public scheduledMaintenanceNumber?: number = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
isDefaultValueColumn: false,
required: false,
type: TableColumnType.JSON,
title: "Post Updates To Workspace Channel Name",
description: "Post Updates To Workspace Channel Name",
})
@Column({
type: ColumnType.JSON,
nullable: true,
})
public postUpdatesToWorkspaceChannels?: Array<WorkspaceChannel> = undefined;
}

View File

@@ -1973,4 +1973,37 @@ export default class StatusPage extends BaseModel {
default: UptimePrecision.TWO_DECIMAL,
})
public overallUptimePercentPrecision?: UptimePrecision = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectStatusPage,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectStatusPage,
],
})
@TableColumn({
required: false,
type: TableColumnType.VeryLongText,
title: "Subscriber Email Notification Footer Text",
description: "Text to send to subscribers in the footer of the email.",
canReadOnRelationQuery: true,
})
@Column({
nullable: true,
type: ColumnType.VeryLongText,
})
public subscriberEmailNotificationFooterText?: string = undefined;
}

View File

@@ -164,7 +164,12 @@ export default class StatusPageAnnouncement extends BaseModel {
Permission.ProjectMember,
Permission.ReadStatusPageAnnouncement,
],
update: [],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageAnnouncement,
],
})
@TableColumn({
required: false,

View File

@@ -0,0 +1,461 @@
import Project from "./Project";
import User from "./User";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
import BaseNotificationRule from "../../Types/Workspace/NotificationRules/BaseNotificationRule";
import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType";
import Permission from "../../Types/Permission";
@TenantColumn("projectId")
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
],
delete: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.DeleteWorkspaceNotificationRule,
],
update: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.EditWorkspaceNotificationRule,
],
})
@CrudApiEndpoint(new Route("/workspace-notification-rule"))
@Entity({
name: "WorkspaceNotificationRule",
})
@TableMetadata({
tableName: "WorkspaceNotificationRule",
singularName: "Workspace Notification Rule",
pluralName: "Workspace Notification Rules",
icon: IconProp.Logs,
tableDescription: "Notification Rule for Third Party Workspaces",
})
class WorkspaceNotificationRule extends BaseModel {
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Relation to Project Resource in which this object belongs",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Project ID",
description: "ID of your OneUptime Project in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
],
update: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.EditWorkspaceNotificationRule,
],
})
@TableColumn({
title: "Rule Name",
description: "Name of the Notification Rule",
required: true,
unique: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: false,
})
public name?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
],
update: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.EditWorkspaceNotificationRule,
],
})
@TableColumn({
title: "Rule Description",
description: "Description of the Notification Rule",
required: false,
unique: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: true,
})
public description?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
],
update: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.EditWorkspaceNotificationRule,
],
})
@TableColumn({
title: "Workspace Notification Rules",
description: "Notification Rules for the Workspace",
required: true,
unique: false,
type: TableColumnType.JSON,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.JSON,
unique: false,
nullable: false,
})
public notificationRule?: BaseNotificationRule = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
],
update: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.EditWorkspaceNotificationRule,
],
})
@TableColumn({
title: "Workspace Event Type",
description:
"Event Type for the Workspace like Incident Created, Monitor Status Updated, etc.",
required: true,
unique: false,
type: TableColumnType.ShortText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.ShortText,
unique: false,
nullable: false,
})
public eventType?: NotificationRuleEventType = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
],
update: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.EditWorkspaceNotificationRule,
],
})
@TableColumn({
title: "Workspace Type",
description: "Type of Workspace - slack, microsoft teams etc.",
required: true,
unique: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: false,
})
public workspaceType?: WorkspaceType = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
],
update: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.EditWorkspaceNotificationRule,
],
})
@TableColumn({
manyToOneRelationColumn: "createdByUserId",
type: TableColumnType.Entity,
modelType: User,
title: "Created by User",
description:
"Relation to User who created this object (if this object was created by a User)",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "createdByUserId" })
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
],
update: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.EditWorkspaceNotificationRule,
],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Created by User ID",
description:
"User ID who created this object (if this object was created by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public createdByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
],
update: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.EditWorkspaceNotificationRule,
],
})
@TableColumn({
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
// deleted by userId
@ColumnAccessControl({
create: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.CreateWorkspaceNotificationRule,
],
read: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.ReadWorkspaceNotificationRule,
],
update: [
Permission.ProjectAdmin,
Permission.ProjectOwner,
Permission.ProjectMember,
Permission.EditWorkspaceNotificationRule,
],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
}
export default WorkspaceNotificationRule;

View File

@@ -0,0 +1,379 @@
import Project from "./Project";
import User from "./User";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
import Permission from "../../Types/Permission";
export interface MiscData {
[key: string]: string;
}
export interface SlackMiscData extends MiscData {
teamId: string;
teamName: string;
botUserId: string;
}
@TenantColumn("projectId")
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
delete: [Permission.ProjectOwner, Permission.ProjectAdmin],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
})
@CrudApiEndpoint(new Route("/workspace-project-auth-token"))
@Entity({
name: "WorkspaceProjectAuthToken",
})
@TableMetadata({
tableName: "WorkspaceProjectAuthToken",
singularName: "Workspace Project Auth Token",
pluralName: "Workspace Project Auth Tokens",
icon: IconProp.Lock,
tableDescription: "Third Party Auth Token for the Project",
})
class WorkspaceProjectAuthToken extends BaseModel {
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Relation to Project Resource in which this object belongs",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Project ID",
description: "ID of your OneUptime Project in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
read: [],
update: [],
})
@TableColumn({
title: "Auth Token",
required: true,
unique: false,
type: TableColumnType.VeryLongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.VeryLongText,
unique: false,
nullable: false,
})
public authToken?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
update: [],
})
@TableColumn({
title: "Workspace Type",
description: "Type of Workspace - slack, microsoft teams etc.",
required: true,
unique: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: false,
})
public workspaceType?: WorkspaceType = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
})
@TableColumn({
title: "Project ID in Workspace",
required: true,
unique: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: false,
})
public workspaceProjectId?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
})
@TableColumn({
title: "Misc Data",
required: true,
unique: false,
type: TableColumnType.JSON,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.JSON,
unique: false,
nullable: false,
})
public miscData?: MiscData = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
})
@TableColumn({
manyToOneRelationColumn: "createdByUserId",
type: TableColumnType.Entity,
modelType: User,
title: "Created by User",
description:
"Relation to User who created this object (if this object was created by a User)",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "createdByUserId" })
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Created by User ID",
description:
"User ID who created this object (if this object was created by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public createdByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
})
@TableColumn({
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
}
export default WorkspaceProjectAuthToken;

View File

@@ -0,0 +1,230 @@
import Project from "./Project";
import User from "./User";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
export interface Settings {
[key: string]: string;
}
export interface SlackSettings extends Settings {
teamId: string;
teamName: string;
botUserId: string;
}
@TenantColumn("projectId")
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [],
read: [],
delete: [],
update: [],
})
@CrudApiEndpoint(new Route("/workspace-setting"))
@Entity({
name: "WorkspaceSetting",
})
@TableMetadata({
tableName: "WorkspaceSetting",
singularName: "Workspace Setting",
pluralName: "Workspace Settings",
icon: IconProp.Settings,
tableDescription: "Settings for Third Party Workspaces",
})
class WorkspaceSetting extends BaseModel {
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Relation to Project Resource in which this object belongs",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Project ID",
description: "ID of your OneUptime Project in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
title: "Workspace Settings",
description: "Settings for the Workspace",
required: true,
unique: false,
type: TableColumnType.JSON,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.JSON,
unique: false,
nullable: false,
})
public settings?: SlackSettings = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
title: "Workspace Type",
description: "Type of Workspace - slack, microsoft teams etc.",
required: true,
unique: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: false,
})
public workspaceType?: WorkspaceType = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "createdByUserId",
type: TableColumnType.Entity,
modelType: User,
title: "Created by User",
description:
"Relation to User who created this object (if this object was created by a User)",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "createdByUserId" })
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Created by User ID",
description:
"User ID who created this object (if this object was created by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public createdByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
// deleted by userId
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
}
export default WorkspaceSetting;

View File

@@ -0,0 +1,312 @@
import Project from "./Project";
import User from "./User";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
export interface MiscData {
[key: string]: any;
}
export interface SlackMiscData extends MiscData {
userId: string;
}
@TenantColumn("projectId")
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
delete: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@CrudApiEndpoint(new Route("/workspace-user-auth-token"))
@Entity({
name: "WorkspaceUserAuthToken",
})
@TableMetadata({
tableName: "WorkspaceUserAuthToken",
singularName: "Workspace User Auth Token",
pluralName: "Workspace User Auth Tokens",
icon: IconProp.Lock,
tableDescription: "Third Party Auth Token for the User",
})
@CurrentUserCanAccessRecordBy("userId")
class WorkspaceUserAuthToken extends BaseModel {
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Relation to Project Resource in which this object belongs",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Project ID",
description: "ID of your OneUptime Project in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [],
update: [],
})
@TableColumn({
title: "Auth Token",
required: true,
unique: false,
type: TableColumnType.VeryLongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.VeryLongText,
unique: false,
nullable: false,
})
public authToken?: string = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
title: "User ID in Service",
description: "User ID in the Workspace",
required: true,
unique: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: false,
})
public workspaceUserId?: string = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
title: "Workspace Type",
description: "Type of Workspace - slack, microsoft teams etc.",
required: true,
unique: false,
type: TableColumnType.LongText,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
unique: false,
nullable: false,
})
public workspaceType?: WorkspaceType = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
title: "Misc Data",
required: true,
unique: false,
type: TableColumnType.JSON,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.JSON,
unique: false,
nullable: false,
})
public miscData?: MiscData = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "user",
type: TableColumnType.Entity,
modelType: User,
title: "User",
description: "Relation to User who this email belongs to",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "userId" })
public user?: User = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "User ID",
description: "User ID who this email belongs to",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
@Index()
public userId?: ObjectID = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "createdByUserId",
type: TableColumnType.Entity,
modelType: User,
title: "Created by User",
description:
"Relation to User who created this object (if this object was created by a User)",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "createdByUserId" })
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Created by User ID",
description:
"User ID who created this object (if this object was created by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public createdByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
}
export default WorkspaceUserAuthToken;

View File

@@ -131,6 +131,30 @@ export default class BaseAPI<
},
);
router.post(
`${new this.entityType().getCrudApiPath()?.toString()}/:id/update-item`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.updateItem(req, res);
} catch (err) {
next(err);
}
},
);
router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/:id/update-item`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.updateItem(req, res);
} catch (err) {
next(err);
}
},
);
// Delete
router.delete(
`${new this.entityType().getCrudApiPath()?.toString()}/:id`,
@@ -144,6 +168,30 @@ export default class BaseAPI<
},
);
router.post(
`${new this.entityType().getCrudApiPath()?.toString()}/:id/delete-item`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.deleteItem(req, res);
} catch (err) {
next(err);
}
},
);
router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/:id/delete-item`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.deleteItem(req, res);
} catch (err) {
next(err);
}
},
);
this.router = router;
this.service = service;
}

View File

@@ -0,0 +1,368 @@
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
} from "../Utils/Express";
import Response from "../Utils/Response";
import SlackAuthorization from "../Middleware/SlackAuthorization";
import BadRequestException from "../../Types/Exception/BadRequestException";
import logger from "../Utils/Logger";
import { JSONObject } from "../../Types/JSON";
import BadDataException from "../../Types/Exception/BadDataException";
import {
AppApiClientUrl,
DashboardClientUrl,
SlackAppClientId,
SlackAppClientSecret,
} from "../EnvironmentConfig";
import SlackAppManifest from "../Utils/Workspace/Slack/app-manifest.json";
import URL from "../../Types/API/URL";
import HTTPErrorResponse from "../../Types/API/HTTPErrorResponse";
import HTTPResponse from "../../Types/API/HTTPResponse";
import API from "../../Utils/API";
import WorkspaceProjectAuthTokenService from "../Services/WorkspaceProjectAuthTokenService";
import ObjectID from "../../Types/ObjectID";
import WorkspaceUserAuthTokenService from "../Services/WorkspaceUserAuthTokenService";
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
export default class SlackAPI {
public getRouter(): ExpressRouter {
const router: ExpressRouter = Express.getRouter();
router.get(
"/slack/app-manifest",
(req: ExpressRequest, res: ExpressResponse) => {
// return app manifest for slack app
return Response.sendJsonObjectResponse(req, res, SlackAppManifest);
},
);
router.get(
"/slack/auth/:projectId/:userId",
async (req: ExpressRequest, res: ExpressResponse) => {
if (!SlackAppClientId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Slack App Client ID is not set"),
);
}
if (!SlackAppClientSecret) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Slack App Client Secret is not set"),
);
}
const projectId: string | undefined =
req.params["projectId"]?.toString();
const userId: string | undefined = req.params["userId"]?.toString();
if (!projectId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid ProjectID in request"),
);
}
if (!userId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid UserID in request"),
);
}
// if there's an error query param.
const error: string | undefined = req.query["error"]?.toString();
const slackIntegrationPageUrl: URL = URL.fromString(
DashboardClientUrl.toString() +
`/${projectId.toString()}/settings/slack-integration`,
);
if (error) {
return Response.redirect(
req,
res,
slackIntegrationPageUrl.addQueryParam("error", error),
);
}
// slack returns the code on successful auth.
const code: string | undefined = req.query["code"]?.toString();
if (!code) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException("Invalid request"),
);
}
// get access token from slack api.
const redirectUri: URL = URL.fromString(
`${AppApiClientUrl.toString()}/slack/auth/${projectId}/${userId}`,
);
const requestBody: JSONObject = {
code: code,
client_id: SlackAppClientId,
client_secret: SlackAppClientSecret,
redirect_uri: redirectUri.toString(),
};
logger.debug("Slack Auth Request Body: ");
logger.debug(requestBody);
// send the request to slack api to get the access token https://slack.com/api/oauth.v2.access
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.post(
URL.fromString("https://slack.com/api/oauth.v2.access"),
requestBody,
{
"Content-Type": "application/x-www-form-urlencoded",
},
);
if (response instanceof HTTPErrorResponse) {
throw response;
}
const responseBody: JSONObject = response.data;
logger.debug("Slack Auth Request Body: ");
logger.debug(responseBody);
let slackTeamId: string | undefined = undefined;
let slackBotAccessToken: string | undefined = undefined;
let slackUserId: string | undefined = undefined;
let slackTeamName: string | undefined = undefined;
let botUserId: string | undefined = undefined;
let slackUserAccessToken: string | undefined = undefined;
// ReponseBody is in this format.
// {
// "ok": true,
// "access_token": "sample-token",
// "token_type": "bot",
// "scope": "commands,incoming-webhook",
// "bot_user_id": "U0KRQLJ9H",
// "app_id": "A0KRD7HC3",
// "team": {
// "name": "Slack Pickleball Team",
// "id": "T9TK3CUKW"
// },
// "enterprise": {
// "name": "slack-pickleball",
// "id": "E12345678"
// },
// "authed_user": {
// "id": "U1234",
// "scope": "chat:write",
// "access_token": "sample-token",
// "token_type": "user"
// }
// }
if (responseBody["ok"] !== true) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException("Invalid request"),
);
}
if (
responseBody["team"] &&
(responseBody["team"] as JSONObject)["id"]
) {
slackTeamId = (responseBody["team"] as JSONObject)["id"]?.toString();
}
if (responseBody["access_token"]) {
slackBotAccessToken = responseBody["access_token"]?.toString();
}
if (
responseBody["authed_user"] &&
(responseBody["authed_user"] as JSONObject)["id"]
) {
slackUserId = (responseBody["authed_user"] as JSONObject)[
"id"
]?.toString();
}
if (
responseBody["authed_user"] &&
(responseBody["authed_user"] as JSONObject)["access_token"]
) {
slackUserAccessToken = (responseBody["authed_user"] as JSONObject)[
"access_token"
]?.toString();
}
if (
responseBody["team"] &&
(responseBody["team"] as JSONObject)["name"]
) {
slackTeamName = (responseBody["team"] as JSONObject)[
"name"
]?.toString();
}
if (responseBody["bot_user_id"]) {
botUserId = responseBody["bot_user_id"]?.toString();
}
await WorkspaceProjectAuthTokenService.refreshAuthToken({
projectId: new ObjectID(projectId),
workspaceType: WorkspaceType.Slack,
authToken: slackBotAccessToken || "",
workspaceProjectId: slackTeamId || "",
miscData: {
teamId: slackTeamId || "",
teamName: slackTeamName || "",
botUserId: botUserId || "",
},
});
await WorkspaceUserAuthTokenService.refreshAuthToken({
projectId: new ObjectID(projectId),
userId: new ObjectID(userId),
workspaceType: WorkspaceType.Slack,
authToken: slackUserAccessToken || "",
workspaceUserId: slackUserId || "",
miscData: {
userId: slackUserId || "",
},
});
// return back to dashboard after successful auth.
Response.redirect(req, res, slackIntegrationPageUrl);
},
);
router.post(
"/slack/interactive",
SlackAuthorization.isAuthorizedSlackRequest,
(req: ExpressRequest, res: ExpressResponse) => {
return Response.sendJsonObjectResponse(req, res, {
response_action: "clear",
});
},
);
// options load endpoint.
router.post(
"/slack/options-load",
SlackAuthorization.isAuthorizedSlackRequest,
(req: ExpressRequest, res: ExpressResponse) => {
return Response.sendJsonObjectResponse(req, res, {
response_action: "clear",
});
},
);
router.post(
"/slack/command",
SlackAuthorization.isAuthorizedSlackRequest,
(req: ExpressRequest, res: ExpressResponse) => {
return Response.sendJsonObjectResponse(req, res, {
response_action: "clear",
});
},
);
router.post(
"/slack/events",
SlackAuthorization.isAuthorizedSlackRequest,
(req: ExpressRequest, res: ExpressResponse) => {
// respond to slack challenge
const body: any = req.body;
if (body.challenge) {
return Response.sendJsonObjectResponse(req, res, {
challenge: body.challenge,
});
}
// if event is "create-incident" then show the incident create modal with title and description and add a button to submit the form.
if (body.event && body.event.type === "create-incident") {
return Response.sendJsonObjectResponse(req, res, {
type: "modal",
title: {
type: "plain_text",
text: "Create Incident",
},
blocks: [
{
type: "input",
block_id: "title",
element: {
type: "plain_text_input",
action_id: "title",
placeholder: {
type: "plain_text",
text: "Incident Title",
},
},
label: {
type: "plain_text",
text: "Title",
},
},
{
type: "input",
block_id: "description",
element: {
type: "plain_text_input",
action_id: "description",
placeholder: {
type: "plain_text",
text: "Incident Description",
},
},
label: {
type: "plain_text",
text: "Description",
},
},
// button
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: "Submit",
},
style: "primary",
value: "submit",
},
],
},
],
});
}
return Response.sendErrorResponse(
req,
res,
new BadRequestException("Invalid request"),
);
},
);
return router;
}
}

View File

@@ -74,6 +74,8 @@ import StatusPageResource from "Common/Models/DatabaseModels/StatusPageResource"
import StatusPageSSO from "Common/Models/DatabaseModels/StatusPageSso";
import StatusPageSubscriber from "Common/Models/DatabaseModels/StatusPageSubscriber";
import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
import StatusPageResourceUptimeUtil from "../../Utils/StatusPage/ResourceUptime";
import MonitorService from "../Services/MonitorService";
export default class StatusPageAPI extends BaseAPI<
StatusPage,
@@ -517,6 +519,128 @@ export default class StatusPageAPI extends BaseAPI<
},
);
this.router.post(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/overview/:statusPageId/uptime-percent`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
// This reosurce ID can be of a status page resource OR a status page group.
const statusPageResourceId: ObjectID = new ObjectID(
req.params["statusPageResourceId"] as string,
);
const statusPageId: ObjectID = new ObjectID(
req.params["statusPageId"] as string,
);
if(!statusPageId || !statusPageResourceId){
throw new BadDataException("Status Page or Resource not found");
}
// get start and end date from request body.
// if no end date is provided then it will be current date.
// if no start date is provided then it will be 14 days ago from end date.
let startDate: Date = OneUptimeDate.getSomeDaysAgo(14)
let endDate: Date = OneUptimeDate.getCurrentDate();
if(req.body["startDate"]){
startDate = OneUptimeDate.fromString(req.body["startDate"] as string);
}
if(req.body["endDate"]){
endDate = OneUptimeDate.fromString(req.body["endDate"] as string);
}
const monitorStatusTimelines: Array<MonitorStatusTimeline> = [];
// get monitor or group.
// get status page group.
const monitorsInResource: Array<ObjectID> = [];
const statusPageGroup: StatusPageGroup | null = await StatusPageGroupService.findOneBy({
query: {
_id: statusPageResourceId,
statusPageId: statusPageId,
},
select: {
_id: true,
statusPageId: true,
},
props: {
isRoot: true,
},
});
if(statusPageGroup){
// get all monitors in group.
const groupResources: Array<StatusPageResource> = await StatusPageResourceService.findBy({
query: {
statusPageGroupId: statusPageResourceId,
},
select: {
monitorId: true,
monitorGroupId: true,
},
props: {
isRoot: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
});
monitorsInGroup.push(...groupResources.map((resource: StatusPageResource) => {
return resource.monitorId!;
}).filter((id: ObjectID) => {
return Boolean(id);
}));
}
const monitor: Monitor | null = await MonitorService.findOneBy({
query: {
_id: statusPageResourceId,
},
select: {
_id: true,
name: true,
monitorGroupId: true,
},
props: {
isRoot: true,
},
});
const uptimePercent: number | null = null;
StatusPageResourceUptimeUtil.calculateAvgUptimePercentOfStatusPageGroup(
{
statusPageGroup: data.group,
monitorStatusTimelines: monitorStatusTimelines,
precision:
data.group.uptimePercentPrecision ||
UptimePrecision.ONE_DECIMAL,
downtimeMonitorStatuses:
statusPage?.downtimeMonitorStatuses || [],
statusPageResources: statusPageResources,
monitorsInGroup: monitorsInGroup,
},
);
} catch (err) {
next(err);
}
});
this.router.post(
`${new this.entityType()
.getCrudApiPath()

View File

@@ -30,6 +30,14 @@ export default class DatabaseConfig {
return globalConfig.getColumnValue(key);
}
public static async getHomeUrl(): Promise<URL> {
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
return new URL(httpProtocol, host);
}
public static async getHost(): Promise<Hostname> {
return Promise.resolve(new Hostname(process.env["HOST"] || "localhost"));
}

View File

@@ -2,6 +2,7 @@ import {
AccountsRoute,
AdminDashboardRoute,
DashboardRoute,
AppApiRoute,
} from "Common/ServiceRoute";
import BillingConfig from "./BillingConfig";
import Hostname from "Common/Types/API/Hostname";
@@ -259,6 +260,10 @@ export const WorkflowScriptTimeoutInMS: number = process.env[
? parseInt(process.env["WORKFLOW_SCRIPT_TIMEOUT_IN_MS"].toString())
: 5000;
export const WorkflowTimeoutInMs: number = process.env["WORKFLOW_TIMEOUT_IN_MS"]
? parseInt(process.env["WORKFLOW_TIMEOUT_IN_MS"].toString())
: 5000;
export const AllowedActiveMonitorCountInFreePlan: number = process.env[
"ALLOWED_ACTIVE_MONITOR_COUNT_IN_FREE_PLAN"
]
@@ -279,8 +284,19 @@ export const AllowedSubscribersCountInFreePlan: number = process.env[
? parseInt(process.env["ALLOWED_SUBSCRIBERS_COUNT_IN_FREE_PLAN"].toString())
: 100;
export const NotificationWebhookOnCreateUser: string =
process.env["NOTIFICATION_WEBHOOK_ON_CREATED_USER"] || "";
export const NotificationSlackWebhookOnCreateUser: string =
process.env["NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER"] || "";
export const NotificationSlackWebhookOnCreateProject: string =
process.env["NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT"] || "";
// notification delete project
export const NotificationSlackWebhookOnDeleteProject: string =
process.env["NOTIFICATION_SLACK_WEBHOOK_ON_DELETED_PROJECT"] || "";
// notification subscripton update.
export const NotificationSlackWebhookOnSubscriptionUpdate: string =
process.env["NOTIFICATION_SLACK_WEBHOOK_ON_SUBSCRIPTION_UPDATE"] || "";
export const AdminDashboardClientURL: URL = new URL(
HttpProtocol,
@@ -288,6 +304,8 @@ export const AdminDashboardClientURL: URL = new URL(
AdminDashboardRoute,
);
export const AppApiClientUrl: URL = new URL(HttpProtocol, Host, AppApiRoute);
export const DashboardClientUrl: URL = new URL(
HttpProtocol,
Host,
@@ -302,3 +320,10 @@ export const AccountsClientUrl: URL = new URL(
export const DisableTelemetry: boolean =
process.env["DISABLE_TELEMETRY"] === "true";
export const SlackAppClientId: string | null =
process.env["SLACK_APP_CLIENT_ID"] || null;
export const SlackAppClientSecret: string | null =
process.env["SLACK_APP_CLIENT_SECRET"] || null;
export const SlackAppSigningSecret: string | null =
process.env["SLACK_APP_SIGNING_SECRET"] || null;

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1737713529424 implements MigrationInterface {
public name = "MigrationName1737713529424";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPage" ADD "subscriberEmailNotificationFooterText" character varying(100)`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPage" DROP COLUMN "subscriberEmailNotificationFooterText"`,
);
}
}

View File

@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1737715240684 implements MigrationInterface {
public name = "MigrationName1737715240684";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPage" DROP COLUMN "subscriberEmailNotificationFooterText"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPage" ADD "subscriberEmailNotificationFooterText" text`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPage" DROP COLUMN "subscriberEmailNotificationFooterText"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPage" ADD "subscriberEmailNotificationFooterText" character varying(100)`,
);
}
}

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1737997557974 implements MigrationInterface {
public name = "MigrationName1737997557974";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE INDEX "IDX_4d5e62631b2b63aaecb00950ef" ON "MonitorTest" ("isInQueue") `,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "public"."IDX_4d5e62631b2b63aaecb00950ef"`,
);
}
}

View File

@@ -0,0 +1,147 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1739209832500 implements MigrationInterface {
public name = "MigrationName1739209832500";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "WorkspaceUserAuthToken" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "authToken" text NOT NULL, "workspaceUserId" character varying(500) NOT NULL, "workspaceType" character varying(500) NOT NULL, "miscData" jsonb NOT NULL, "userId" uuid, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_ae2f1b46b7e26f58a1f4a56b6ea" PRIMARY KEY ("_id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_bee888f5782b9585e01f13455f" ON "WorkspaceUserAuthToken" ("projectId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_4b7c7d1a8b2259df8c790db094" ON "WorkspaceUserAuthToken" ("userId") `,
);
await queryRunner.query(
`CREATE TABLE "WorkspaceProjectAuthToken" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "authToken" text NOT NULL, "workspaceType" character varying(500) NOT NULL, "workspaceProjectId" character varying(500) NOT NULL, "miscData" jsonb NOT NULL, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_c0caa6a69da614ee74d8c1291da" PRIMARY KEY ("_id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_73f5887268b09c0abccf04ef02" ON "WorkspaceProjectAuthToken" ("projectId") `,
);
await queryRunner.query(
`CREATE TABLE "WorkspaceSetting" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "settings" jsonb NOT NULL, "workspaceType" character varying(500) NOT NULL, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_eb98d42edd6489fbe1cf3f34515" PRIMARY KEY ("_id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_c68f38e2b2b061c40209e85bf2" ON "WorkspaceSetting" ("projectId") `,
);
await queryRunner.query(
`CREATE TABLE "WorkspaceNotificationRule" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(500) NOT NULL, "description" character varying(500), "notificationRule" jsonb NOT NULL, "eventType" character varying NOT NULL, "workspaceType" character varying(500) NOT NULL, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_d1485681c7695ac9841dc52a451" PRIMARY KEY ("_id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_349b022afa9a50a597d6c91ec9" ON "WorkspaceNotificationRule" ("projectId") `,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceUserAuthToken" ADD CONSTRAINT "FK_bee888f5782b9585e01f13455fb" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceUserAuthToken" ADD CONSTRAINT "FK_4b7c7d1a8b2259df8c790db0940" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceUserAuthToken" ADD CONSTRAINT "FK_ec5cbf4536681fe4bea883c98ea" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceUserAuthToken" ADD CONSTRAINT "FK_1b2cb71eaf9e665e4556d1b1263" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceProjectAuthToken" ADD CONSTRAINT "FK_73f5887268b09c0abccf04ef02e" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceProjectAuthToken" ADD CONSTRAINT "FK_8aa5804c7a728039564bf5d967d" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceProjectAuthToken" ADD CONSTRAINT "FK_6287095997a16f1cbdd4fb24b61" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceSetting" ADD CONSTRAINT "FK_c68f38e2b2b061c40209e85bf22" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceSetting" ADD CONSTRAINT "FK_c8fdd61b95bfd0a2ca268b8c602" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceSetting" ADD CONSTRAINT "FK_cb3b7931417a4b4ee05d487b614" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceNotificationRule" ADD CONSTRAINT "FK_349b022afa9a50a597d6c91ec95" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceNotificationRule" ADD CONSTRAINT "FK_55f4e43427fc217ed32cf640a28" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceNotificationRule" ADD CONSTRAINT "FK_65ac673d16286be2dcd5229fe24" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "WorkspaceNotificationRule" DROP CONSTRAINT "FK_65ac673d16286be2dcd5229fe24"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceNotificationRule" DROP CONSTRAINT "FK_55f4e43427fc217ed32cf640a28"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceNotificationRule" DROP CONSTRAINT "FK_349b022afa9a50a597d6c91ec95"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceSetting" DROP CONSTRAINT "FK_cb3b7931417a4b4ee05d487b614"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceSetting" DROP CONSTRAINT "FK_c8fdd61b95bfd0a2ca268b8c602"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceSetting" DROP CONSTRAINT "FK_c68f38e2b2b061c40209e85bf22"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceProjectAuthToken" DROP CONSTRAINT "FK_6287095997a16f1cbdd4fb24b61"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceProjectAuthToken" DROP CONSTRAINT "FK_8aa5804c7a728039564bf5d967d"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceProjectAuthToken" DROP CONSTRAINT "FK_73f5887268b09c0abccf04ef02e"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceUserAuthToken" DROP CONSTRAINT "FK_1b2cb71eaf9e665e4556d1b1263"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceUserAuthToken" DROP CONSTRAINT "FK_ec5cbf4536681fe4bea883c98ea"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceUserAuthToken" DROP CONSTRAINT "FK_4b7c7d1a8b2259df8c790db0940"`,
);
await queryRunner.query(
`ALTER TABLE "WorkspaceUserAuthToken" DROP CONSTRAINT "FK_bee888f5782b9585e01f13455fb"`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_349b022afa9a50a597d6c91ec9"`,
);
await queryRunner.query(`DROP TABLE "WorkspaceNotificationRule"`);
await queryRunner.query(
`DROP INDEX "public"."IDX_c68f38e2b2b061c40209e85bf2"`,
);
await queryRunner.query(`DROP TABLE "WorkspaceSetting"`);
await queryRunner.query(
`DROP INDEX "public"."IDX_73f5887268b09c0abccf04ef02"`,
);
await queryRunner.query(`DROP TABLE "WorkspaceProjectAuthToken"`);
await queryRunner.query(
`DROP INDEX "public"."IDX_4b7c7d1a8b2259df8c790db094"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_bee888f5782b9585e01f13455f"`,
);
await queryRunner.query(`DROP TABLE "WorkspaceUserAuthToken"`);
}
}

View File

@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1739210586538 implements MigrationInterface {
public name = "MigrationName1739210586538";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "Alert" ADD "alertNumber" integer`);
await queryRunner.query(
`CREATE INDEX "IDX_aa91b2228a2b35424a3ae93fdc" ON "Alert" ("alertNumber") `,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "public"."IDX_aa91b2228a2b35424a3ae93fdc"`,
);
await queryRunner.query(`ALTER TABLE "Alert" DROP COLUMN "alertNumber"`);
}
}

View File

@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1739217257089 implements MigrationInterface {
public name = "MigrationName1739217257089";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" ADD "scheduledMaintenanceNumber" integer`,
);
await queryRunner.query(
`CREATE INDEX "IDX_207fe82fd8bdc67bbe1aa0ebf8" ON "ScheduledMaintenance" ("scheduledMaintenanceNumber") `,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "public"."IDX_207fe82fd8bdc67bbe1aa0ebf8"`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" DROP COLUMN "scheduledMaintenanceNumber"`,
);
}
}

View File

@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1739282331053 implements MigrationInterface {
public name = "MigrationName1739282331053";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Incident" ADD "postUpdatesToSlackChannelId" character varying(100)`,
);
await queryRunner.query(
`ALTER TABLE "Alert" ADD "postUpdatesToSlackChannelId" character varying(100)`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" ADD "postUpdatesToSlackChannelId" character varying(100)`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" DROP COLUMN "postUpdatesToSlackChannelId"`,
);
await queryRunner.query(
`ALTER TABLE "Alert" DROP COLUMN "postUpdatesToSlackChannelId"`,
);
await queryRunner.query(
`ALTER TABLE "Incident" DROP COLUMN "postUpdatesToSlackChannelId"`,
);
}
}

View File

@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1739374537088 implements MigrationInterface {
public name = "MigrationName1739374537088";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Incident" RENAME COLUMN "postUpdatesToSlackChannelId" TO "postUpdatesToWorkspaceChannelName"`,
);
await queryRunner.query(
`ALTER TABLE "Alert" RENAME COLUMN "postUpdatesToSlackChannelId" TO "postUpdatesToWorkspaceChannelName"`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" RENAME COLUMN "postUpdatesToSlackChannelId" TO "postUpdatesToWorkspaceChannelName"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" RENAME COLUMN "postUpdatesToWorkspaceChannelName" TO "postUpdatesToSlackChannelId"`,
);
await queryRunner.query(
`ALTER TABLE "Alert" RENAME COLUMN "postUpdatesToWorkspaceChannelName" TO "postUpdatesToSlackChannelId"`,
);
await queryRunner.query(
`ALTER TABLE "Incident" RENAME COLUMN "postUpdatesToWorkspaceChannelName" TO "postUpdatesToSlackChannelId"`,
);
}
}

View File

@@ -0,0 +1,65 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1739569321582 implements MigrationInterface {
public name = "MigrationName1739569321582";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Incident" RENAME COLUMN "postUpdatesToWorkspaceChannelName" TO "postUpdatesToWorkspaceChannels"`,
);
await queryRunner.query(
`ALTER TABLE "Alert" RENAME COLUMN "postUpdatesToWorkspaceChannelName" TO "postUpdatesToWorkspaceChannels"`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" RENAME COLUMN "postUpdatesToWorkspaceChannelName" TO "postUpdatesToWorkspaceChannels"`,
);
await queryRunner.query(
`ALTER TABLE "Incident" DROP COLUMN "postUpdatesToWorkspaceChannels"`,
);
await queryRunner.query(
`ALTER TABLE "Incident" ADD "postUpdatesToWorkspaceChannels" jsonb`,
);
await queryRunner.query(
`ALTER TABLE "Alert" DROP COLUMN "postUpdatesToWorkspaceChannels"`,
);
await queryRunner.query(
`ALTER TABLE "Alert" ADD "postUpdatesToWorkspaceChannels" jsonb`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" DROP COLUMN "postUpdatesToWorkspaceChannels"`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" ADD "postUpdatesToWorkspaceChannels" jsonb`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" DROP COLUMN "postUpdatesToWorkspaceChannels"`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" ADD "postUpdatesToWorkspaceChannels" character varying(100)`,
);
await queryRunner.query(
`ALTER TABLE "Alert" DROP COLUMN "postUpdatesToWorkspaceChannels"`,
);
await queryRunner.query(
`ALTER TABLE "Alert" ADD "postUpdatesToWorkspaceChannels" character varying(100)`,
);
await queryRunner.query(
`ALTER TABLE "Incident" DROP COLUMN "postUpdatesToWorkspaceChannels"`,
);
await queryRunner.query(
`ALTER TABLE "Incident" ADD "postUpdatesToWorkspaceChannels" character varying(100)`,
);
await queryRunner.query(
`ALTER TABLE "ScheduledMaintenance" RENAME COLUMN "postUpdatesToWorkspaceChannels" TO "postUpdatesToWorkspaceChannelName"`,
);
await queryRunner.query(
`ALTER TABLE "Alert" RENAME COLUMN "postUpdatesToWorkspaceChannels" TO "postUpdatesToWorkspaceChannelName"`,
);
await queryRunner.query(
`ALTER TABLE "Incident" RENAME COLUMN "postUpdatesToWorkspaceChannels" TO "postUpdatesToWorkspaceChannelName"`,
);
}
}

View File

@@ -96,6 +96,15 @@ import { MigrationName1736787985322 } from "./1736787985322-MigrationName";
import { MigrationName1736788706141 } from "./1736788706141-MigrationName";
import { MigrationName1736856662868 } from "./1736856662868-MigrationName";
import { MigrationName1737141420441 } from "./1737141420441-MigrationName";
import { MigrationName1737713529424 } from "./1737713529424-MigrationName";
import { MigrationName1737715240684 } from "./1737715240684-MigrationName";
import { MigrationName1737997557974 } from "./1737997557974-MigrationName";
import { MigrationName1739209832500 } from "./1739209832500-MigrationName";
import { MigrationName1739210586538 } from "./1739210586538-MigrationName";
import { MigrationName1739217257089 } from "./1739217257089-MigrationName";
import { MigrationName1739282331053 } from "./1739282331053-MigrationName";
import { MigrationName1739374537088 } from "./1739374537088-MigrationName";
import { MigrationName1739569321582 } from "./1739569321582-MigrationName";
export default [
InitialMigration,
@@ -196,4 +205,13 @@ export default [
MigrationName1736788706141,
MigrationName1736856662868,
MigrationName1737141420441,
MigrationName1737713529424,
MigrationName1737715240684,
MigrationName1737997557974,
MigrationName1739209832500,
MigrationName1739210586538,
MigrationName1739217257089,
MigrationName1739282331053,
MigrationName1739374537088,
MigrationName1739569321582,
];

View File

@@ -0,0 +1,60 @@
import {
ExpressResponse,
NextFunction,
OneUptimeRequest,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BadDataException from "Common/Types/Exception/BadDataException";
import { SlackAppSigningSecret } from "../EnvironmentConfig";
import crypto from "crypto";
import logger from "../Utils/Logger";
export default class SlackAuthorization {
public static async isAuthorizedSlackRequest(
req: OneUptimeRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> {
if (!SlackAppSigningSecret) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"SLACK_APP_SIGNING_SECRET env variable not found.",
),
);
}
// validate slack signing secret
const slackSigningSecret: string = SlackAppSigningSecret.toString();
const slackSignature: string = req.headers["x-slack-signature"] as string;
const timestamp: string = req.headers[
"x-slack-request-timestamp"
] as string;
const requestBody: string = req.body;
logger.debug(`slackSignature: ${slackSignature}`);
logger.debug(`timestamp: ${timestamp}`);
logger.debug(`requestBody: ${requestBody}`);
const baseString: string = `v0:${timestamp}:${requestBody}`;
const signature: string = `v0=${crypto.createHmac("sha256", slackSigningSecret).update(baseString).digest("hex")}`;
// check if the signature is valid
if (
!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(slackSignature),
)
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Slack Signature Verification Failed."),
);
}
next();
}
}

View File

@@ -8,6 +8,8 @@ import {
} from "../../Server/Utils/Express";
import TelemetryIngestionKeyService from "../../Server/Services/TelemetryIngestionKeyService";
import TelemetryIngestionKey from "../../Models/DatabaseModels/TelemetryIngestionKey";
import Response from "../Utils/Response";
import logger from "../Utils/Logger";
export interface TelemetryRequest extends ExpressRequest {
projectId: ObjectID; // Project ID
@@ -17,12 +19,14 @@ export interface TelemetryRequest extends ExpressRequest {
export default class TelemetryIngest {
public static async isAuthorizedServiceMiddleware(
req: ExpressRequest,
_res: ExpressResponse,
res: ExpressResponse,
next: NextFunction,
): Promise<void> {
try {
// check header.
const isOpenTelemetryAPI: boolean = req.path.includes("/otlp/v1");
let oneuptimeToken: string | undefined = req.headers[
"x-oneuptime-token"
] as string | undefined;
@@ -35,6 +39,14 @@ export default class TelemetryIngest {
}
if (!oneuptimeToken) {
logger.error("Missing header: x-oneuptime-token");
if (isOpenTelemetryAPI) {
// then accept the response and return success.
// do not return error because it causes Otel to retry the request.
return Response.sendEmptySuccessResponse(req, res);
}
throw new BadRequestException("Missing header: x-oneuptime-token");
}
@@ -54,6 +66,14 @@ export default class TelemetryIngest {
});
if (!token) {
logger.error("Invalid service token: " + oneuptimeToken);
if (isOpenTelemetryAPI) {
// then accept the response and return success.
// do not return error because it causes Otel to retry the request.
return Response.sendEmptySuccessResponse(req, res);
}
throw new BadRequestException(
"Invalid service token: " + oneuptimeToken,
);
@@ -62,6 +82,16 @@ export default class TelemetryIngest {
projectId = token.projectId as ObjectID;
if (!projectId) {
logger.error(
"Project ID not found for service token: " + oneuptimeToken,
);
if (isOpenTelemetryAPI) {
// then accept the response and return success.
// do not return error because it causes Otel to retry the request.
return Response.sendEmptySuccessResponse(req, res);
}
throw new BadRequestException(
"Project ID not found for service token: " + oneuptimeToken,
);

View File

@@ -41,6 +41,10 @@ import AlertMetricType from "../../Types/Alerts/AlertMetricType";
import AlertFeedService from "./AlertFeedService";
import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed";
import { Gray500, Red500 } from "../../Types/BrandColors";
import Label from "../../Models/DatabaseModels/Label";
import LabelService from "./LabelService";
import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
import AlertSeverityService from "./AlertSeverityService";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -94,6 +98,32 @@ export class Service extends DatabaseService<Model> {
return false;
}
public async getExistingAlertNumberForProject(data: {
projectId: ObjectID;
}): Promise<number> {
// get last alert number.
const lastAlert: Model | null = await this.findOneBy({
query: {
projectId: data.projectId,
},
select: {
alertNumber: true,
},
sort: {
createdAt: SortOrder.Descending,
},
props: {
isRoot: true,
},
});
if (!lastAlert) {
return 0;
}
return lastAlert.alertNumber || 0;
}
public async acknowledgeAlert(
alertId: ObjectID,
acknowledgedByUserId: ObjectID,
@@ -152,9 +182,12 @@ export class Service extends DatabaseService<Model> {
throw new BadDataException("ProjectId required to create alert.");
}
const projectId: ObjectID =
createBy.props.tenantId || createBy.data.projectId!;
const alertState: AlertState | null = await AlertStateService.findOneBy({
query: {
projectId: createBy.props.tenantId || createBy.data.projectId!,
projectId: projectId,
isCreatedState: true,
},
select: {
@@ -173,6 +206,13 @@ export class Service extends DatabaseService<Model> {
createBy.data.currentAlertStateId = alertState.id;
const alertNumberForThisAlert: number =
(await this.getExistingAlertNumberForProject({
projectId: projectId,
})) + 1;
createBy.data.alertNumber = alertNumberForThisAlert;
if (
(createBy.data.createdByUserId ||
createBy.data.createdByUser ||
@@ -235,7 +275,7 @@ export class Service extends DatabaseService<Model> {
projectId: createdItem.projectId!,
alertFeedEventType: AlertFeedEventType.AlertCreated,
displayColor: Red500,
feedInfoInMarkdown: `**Alert Created**:
feedInfoInMarkdown: `**Alert #${createdItem.alertNumber?.toString()} Created**:
**Alert Title**:
@@ -492,74 +532,132 @@ ${createdItem.remediationNotes || "No remediation notes provided."}`,
if (updatedItemIds.length > 0) {
for (const alertId of updatedItemIds) {
let shouldAddAlertFeed: boolean = false;
let feedInfoInMarkdown: string = "**Alert was updated.**";
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
if (onUpdate.updateBy.data.title) {
// add alert feed.
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
await AlertFeedService.createAlertFeed({
alertId: alertId,
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
alertFeedEventType: AlertFeedEventType.AlertUpdated,
displayColor: Gray500,
feedInfoInMarkdown: `**Alert title was updated.** Here's the new title.
feedInfoInMarkdown += `\n\n**Title**:
${onUpdate.updateBy.data.title || "No title provided."}
`,
userId: createdByUserId || undefined,
});
`;
shouldAddAlertFeed = true;
}
if (onUpdate.updateBy.data.rootCause) {
// add alert feed.
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
if (onUpdate.updateBy.data.title) {
// add alert feed.
await AlertFeedService.createAlertFeed({
alertId: alertId,
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
alertFeedEventType: AlertFeedEventType.AlertUpdated,
displayColor: Gray500,
feedInfoInMarkdown: `**Alert root cause was updated.** Here's the new root cause.
feedInfoInMarkdown += `\n\n**Root Cause**:
${onUpdate.updateBy.data.rootCause || "No root cause provided."}
`,
userId: createdByUserId || undefined,
});
`;
shouldAddAlertFeed = true;
}
}
if (onUpdate.updateBy.data.description) {
// add alert feed.
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
await AlertFeedService.createAlertFeed({
alertId: alertId,
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
alertFeedEventType: AlertFeedEventType.AlertUpdated,
displayColor: Gray500,
feedInfoInMarkdown: `**Alert description was updated.** Here's the new description.
${onUpdate.updateBy.data.description || "No description provided."}
`,
userId: createdByUserId || undefined,
});
feedInfoInMarkdown += `\n\n**Alert Description**:
${onUpdate.updateBy.data.description || "No description provided."}
`;
shouldAddAlertFeed = true;
}
if (onUpdate.updateBy.data.remediationNotes) {
// add alert feed.
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
feedInfoInMarkdown += `\n\n**Remediation Notes**:
${onUpdate.updateBy.data.remediationNotes || "No remediation notes provided."}
`;
shouldAddAlertFeed = true;
}
if (
onUpdate.updateBy.data.labels &&
onUpdate.updateBy.data.labels.length > 0 &&
Array.isArray(onUpdate.updateBy.data.labels)
) {
const labelIds: Array<ObjectID> = (
onUpdate.updateBy.data.labels as any
)
.map((label: Label) => {
if (label._id) {
return new ObjectID(label._id?.toString());
}
return null;
})
.filter((labelId: ObjectID | null) => {
return labelId !== null;
});
const labels: Array<Label> = await LabelService.findBy({
query: {
_id: QueryHelper.any(labelIds),
},
select: {
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
props: {
isRoot: true,
},
});
if (labels.length > 0) {
feedInfoInMarkdown += `\n\n**Labels**:
${labels
.map((label: Label) => {
return `- ${label.name}`;
})
.join("\n")}
`;
shouldAddAlertFeed = true;
}
}
if (
onUpdate.updateBy.data.alertSeverity &&
(onUpdate.updateBy.data.alertSeverity as any)._id
) {
const alertSeverity: AlertSeverity | null =
await AlertSeverityService.findOneBy({
query: {
_id: new ObjectID(
(onUpdate.updateBy.data.alertSeverity as any)?._id.toString(),
),
},
select: {
name: true,
},
props: {
isRoot: true,
},
});
if (alertSeverity) {
feedInfoInMarkdown += `\n\n**Alert Severity**:
${alertSeverity.name}
`;
shouldAddAlertFeed = true;
}
}
if (shouldAddAlertFeed) {
await AlertFeedService.createAlertFeed({
alertId: alertId,
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
alertFeedEventType: AlertFeedEventType.AlertUpdated,
displayColor: Gray500,
feedInfoInMarkdown: `**Remediation notes were updated.** Here are the new notes.
${onUpdate.updateBy.data.remediationNotes || "No remediation notes provided."}
`,
feedInfoInMarkdown: feedInfoInMarkdown,
userId: createdByUserId || undefined,
});
}

View File

@@ -29,7 +29,7 @@ import logger from "../Utils/Logger";
import Realtime from "../Utils/Realtime";
import StreamUtil from "../Utils/Stream";
import BaseService from "./BaseService";
import { ExecResult } from "@clickhouse/client";
import { ExecResult, ResponseJSON, ResultSet } from "@clickhouse/client";
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
import { WorkflowRoute } from "Common/ServiceRoute";
import Protocol from "../../Types/API/Protocol";
@@ -148,19 +148,29 @@ export default class AnalyticsDatabaseService<
const countStatement: Statement = this.toCountStatement(countBy);
const dbResult: ExecResult<Stream> = await this.execute(countStatement);
const dbResult: ResultSet<"JSON"> =
await this.executeQuery(countStatement);
const strResult: string = await StreamUtil.convertStreamToText(
dbResult.stream,
);
logger.debug(`${this.model.tableName} Count Statement executed`);
logger.debug(countStatement);
let countPositive: PositiveNumber = new PositiveNumber(strResult || 0);
if (countBy.groupBy && Object.keys(countBy.groupBy).length > 0) {
// this usually happens when group by is used. In this case we count the total number of groups and not rows in those groups.
countPositive = new PositiveNumber(strResult.split("\n").length - 1); // -1 because the last line is empty.
const resultInJSON: ResponseJSON<JSONObject> =
await dbResult.json<JSONObject>();
let countPositive: PositiveNumber = new PositiveNumber(0);
if (
resultInJSON.data &&
resultInJSON.data[0] &&
resultInJSON.data[0]["count()"] &&
typeof resultInJSON.data[0]["count()"] === "string"
) {
countPositive = new PositiveNumber(
resultInJSON.data[0]["count()"] as string,
);
}
logger.debug(`Result: `);
logger.debug(countPositive.toNumber());
countPositive = await this.onCountSuccess(countPositive);
return countPositive;
} catch (error) {
@@ -240,20 +250,18 @@ export default class AnalyticsDatabaseService<
columns: Array<string>;
} = this.toAggregateStatement(aggregateBy);
const dbResult: ExecResult<Stream> = await this.execute(
const dbResult: ResultSet<"JSON"> = await this.executeQuery(
findStatement.statement,
);
const strResult: string = await StreamUtil.convertStreamToText(
dbResult.stream,
);
logger.debug(`${this.model.tableName} Aggregate Statement executed`);
const jsonItems: Array<JSONObject> = this.convertSelectReturnedDataToJson(
strResult,
findStatement.columns,
);
const responseJSON: ResponseJSON<JSONObject> =
await dbResult.json<JSONObject>();
const items: Array<JSONObject> = jsonItems as any;
const items: Array<JSONObject> = responseJSON.data
? responseJSON.data
: [];
const aggregatedItems: Array<AggregatedModel> = [];
@@ -314,16 +322,6 @@ export default class AnalyticsDatabaseService<
}
}
public async executeQuery(query: string): Promise<string> {
const dbResult: ExecResult<Stream> = await this.execute(query);
const strResult: string = await StreamUtil.convertStreamToText(
dbResult.stream,
);
return strResult;
}
private async _findBy(
findBy: FindBy<TBaseModel>,
): Promise<Array<TBaseModel>> {
@@ -379,21 +377,17 @@ export default class AnalyticsDatabaseService<
columns: Array<string>;
} = this.toFindStatement(onBeforeFind);
const dbResult: ExecResult<Stream> = await this.execute(
const dbResult: ResultSet<"JSON"> = await this.executeQuery(
findStatement.statement,
);
logger.debug(`${this.model.tableName} Find Statement executed`);
logger.debug(findStatement.statement);
const strResult: string = await StreamUtil.convertStreamToText(
dbResult.stream,
);
const responseJSON: ResponseJSON<JSONObject> =
await dbResult.json<JSONObject>();
const jsonItems: Array<JSONObject> = this.convertSelectReturnedDataToJson(
strResult,
findStatement.columns,
);
const jsonItems: Array<JSONObject> = responseJSON.data;
let items: Array<TBaseModel> =
AnalyticsBaseModel.fromJSONArray<TBaseModel>(jsonItems, this.modelType);
@@ -411,7 +405,7 @@ export default class AnalyticsDatabaseService<
}
}
private convertSelectReturnedDataToJson(
public convertSelectReturnedDataToJson(
strResult: string,
columns: string[],
): JSONObject[] {
@@ -815,6 +809,24 @@ export default class AnalyticsDatabaseService<
) as ExecResult<Stream>;
}
public async executeQuery(
statement: Statement | string,
): Promise<ResultSet<"JSON">> {
if (!this.databaseClient) {
this.useDefaultDatabase();
}
const query: string = statement instanceof Statement ? statement.query : statement;
const queryParams: Record<string, unknown> | undefined = statement instanceof Statement ? statement.query_params : undefined;
return await this.databaseClient.query({
query: query,
format: "JSON",
query_params: queryParams || undefined as any, // undefined is not specified in the type for query_params, but its ok to pass undefined.
})
}
protected async onUpdateSuccess(
onUpdate: OnUpdate<TBaseModel>,
_updatedItemIds: Array<ObjectID>,

View File

@@ -50,6 +50,17 @@ import Semaphore, {
import IncidentFeedService from "./IncidentFeedService";
import { IncidentFeedEventType } from "../../Models/DatabaseModels/IncidentFeed";
import { Gray500, Red500 } from "../../Types/BrandColors";
import Label from "../../Models/DatabaseModels/Label";
import LabelService from "./LabelService";
import IncidentSeverity from "../../Models/DatabaseModels/IncidentSeverity";
import IncidentSeverityService from "./IncidentSeverityService";
import {
WorkspaceMessageBlock,
WorkspacePayloadMarkdown,
} from "../../Types/Workspace/WorkspaceMessagePayload";
import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService";
import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType";
import { WorkspaceChannel } from "../Utils/Workspace/WorkspaceBase";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -464,6 +475,32 @@ ${createdItem.remediationNotes || "No remediation notes provided."}`,
}
}
// // send message to workspaces - slack, teams, etc.
// const createdChannels: {
// channelsCreated: Array<WorkspaceChannel>;
// } | null = await this.notifyWorkspaceOnIncidentCreate({
// projectId: createdItem.projectId,
// incidentId: createdItem.id!,
// incidentNumber: createdItem.incidentNumber!,
// });
// if (
// createdChannels &&
// createdChannels.channelsCreated &&
// createdChannels.channelsCreated.length > 0
// ) {
// // update incident with these channels.
// await this.updateOneById({
// id: createdItem.id!,
// data: {
// postUpdatesToWorkspaceChannels: createdChannels.channelsCreated,
// },
// props: {
// isRoot: true,
// },
// });
// }
return createdItem;
}
@@ -644,74 +681,134 @@ ${createdItem.remediationNotes || "No remediation notes provided."}`,
if (updatedItemIds.length > 0) {
for (const incidentId of updatedItemIds) {
let shouldAddIncidentFeed: boolean = false;
let feedInfoInMarkdown: string = "**Incident was updated.**";
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
if (onUpdate.updateBy.data.title) {
// add incident feed.
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
await IncidentFeedService.createIncidentFeed({
incidentId: incidentId,
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
incidentFeedEventType: IncidentFeedEventType.IncidentUpdated,
displayColor: Gray500,
feedInfoInMarkdown: `**Incident title was updated.** Here's the new title.
feedInfoInMarkdown += `\n\n**Title**:
${onUpdate.updateBy.data.title || "No title provided."}
`,
userId: createdByUserId || undefined,
});
`;
shouldAddIncidentFeed = true;
}
if (onUpdate.updateBy.data.rootCause) {
// add incident feed.
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
if (onUpdate.updateBy.data.title) {
// add incident feed.
await IncidentFeedService.createIncidentFeed({
incidentId: incidentId,
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
incidentFeedEventType: IncidentFeedEventType.IncidentUpdated,
displayColor: Gray500,
feedInfoInMarkdown: `**Incident root cause was updated.** Here's the new root cause.
feedInfoInMarkdown += `\n\n**Root Cause**:
${onUpdate.updateBy.data.rootCause || "No root cause provided."}
`,
userId: createdByUserId || undefined,
});
`;
shouldAddIncidentFeed = true;
}
}
if (onUpdate.updateBy.data.description) {
// add incident feed.
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
await IncidentFeedService.createIncidentFeed({
incidentId: incidentId,
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
incidentFeedEventType: IncidentFeedEventType.IncidentUpdated,
displayColor: Gray500,
feedInfoInMarkdown: `**Incident description was updated.** Here's the new description.
${onUpdate.updateBy.data.description || "No description provided."}
`,
userId: createdByUserId || undefined,
});
feedInfoInMarkdown += `\n\n**Incident Description**:
${onUpdate.updateBy.data.description || "No description provided."}
`;
shouldAddIncidentFeed = true;
}
if (onUpdate.updateBy.data.remediationNotes) {
// add incident feed.
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
feedInfoInMarkdown += `\n\n**Remediation Notes**:
${onUpdate.updateBy.data.remediationNotes || "No remediation notes provided."}
`;
shouldAddIncidentFeed = true;
}
if (
onUpdate.updateBy.data.labels &&
onUpdate.updateBy.data.labels.length > 0 &&
Array.isArray(onUpdate.updateBy.data.labels)
) {
const labelIds: Array<ObjectID> = (
onUpdate.updateBy.data.labels as any
)
.map((label: Label) => {
if (label._id) {
return new ObjectID(label._id?.toString());
}
return null;
})
.filter((labelId: ObjectID | null) => {
return labelId !== null;
});
const labels: Array<Label> = await LabelService.findBy({
query: {
_id: QueryHelper.any(labelIds),
},
select: {
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
props: {
isRoot: true,
},
});
if (labels.length > 0) {
feedInfoInMarkdown += `\n\n**Labels**:
${labels
.map((label: Label) => {
return `- ${label.name}`;
})
.join("\n")}
`;
shouldAddIncidentFeed = true;
}
}
if (
onUpdate.updateBy.data.incidentSeverity &&
(onUpdate.updateBy.data.incidentSeverity as any)._id
) {
const incidentSeverity: IncidentSeverity | null =
await IncidentSeverityService.findOneBy({
query: {
_id: new ObjectID(
(
onUpdate.updateBy.data.incidentSeverity as any
)?._id.toString(),
),
},
select: {
name: true,
},
props: {
isRoot: true,
},
});
if (incidentSeverity) {
feedInfoInMarkdown += `\n\n**Incident Severity**:
${incidentSeverity.name}
`;
shouldAddIncidentFeed = true;
}
}
if (shouldAddIncidentFeed) {
await IncidentFeedService.createIncidentFeed({
incidentId: incidentId,
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
incidentFeedEventType: IncidentFeedEventType.IncidentUpdated,
displayColor: Gray500,
feedInfoInMarkdown: `**Remediation notes were updated.** Here are the new notes.
${onUpdate.updateBy.data.remediationNotes || "No remediation notes provided."}
`,
feedInfoInMarkdown: feedInfoInMarkdown,
userId: createdByUserId || undefined,
});
}
@@ -1250,5 +1347,131 @@ ${onUpdate.updateBy.data.remediationNotes || "No remediation notes provided."}
logger.error(err);
});
}
public async notifyWorkspaceOnIncidentCreate(data: {
projectId: ObjectID;
incidentId: ObjectID;
incidentNumber: number;
}): Promise<{
channelsCreated: WorkspaceChannel[];
} | null> {
try {
// we will notify the workspace about the incident creation with the bot tokken which is in WorkspaceProjectAuth Table.
return await WorkspaceNotificationRuleService.createInviteAndPostToChannelsBasedOnRules(
{
projectId: data.projectId,
notificationFor: {
incidentId: data.incidentId,
},
notificationRuleEventType: NotificationRuleEventType.Incident,
channelNameSiffix: data.incidentNumber.toString(),
messageBlocks: await this.getWorkspaceMessageBlocksForIncidentCreate({
incidentId: data.incidentId,
}),
},
);
} catch (err) {
// log the error and continue.
logger.error(err);
return null;
}
}
public async getWorkspaceMessageBlocksForIncidentCreate(data: {
incidentId: ObjectID;
}): Promise<Array<WorkspaceMessageBlock>> {
const incident: Model | null = await this.findOneById({
id: data.incidentId,
select: {
projectId: true,
incidentNumber: true,
title: true,
description: true,
incidentSeverity: {
name: true,
},
rootCause: true,
remediationNotes: true,
currentIncidentState: {
name: true,
},
},
props: {
isRoot: true,
},
});
if (!incident) {
throw new BadDataException("Incident not found");
}
const blocks: Array<WorkspaceMessageBlock> = [];
if (incident.incidentNumber) {
const markdownBlock1: WorkspacePayloadMarkdown = {
_type: "WorkspacePayloadMarkdown",
text: `**Incident #${incident.incidentNumber} Created**`,
};
blocks.push(markdownBlock1);
}
if (incident.title) {
const markdownBlock2: WorkspacePayloadMarkdown = {
_type: "WorkspacePayloadMarkdown",
text: `**Incident Title**:
${incident.title}`,
};
blocks.push(markdownBlock2);
}
if (incident.description) {
const markdownBlock3: WorkspacePayloadMarkdown = {
_type: "WorkspacePayloadMarkdown",
text: `**Description**:
${incident.description}`,
};
blocks.push(markdownBlock3);
}
if (incident.incidentSeverity?.name) {
const markdownBlock4: WorkspacePayloadMarkdown = {
_type: "WorkspacePayloadMarkdown",
text: `**Severity**:
${incident.incidentSeverity.name}`,
};
blocks.push(markdownBlock4);
}
if (incident.rootCause) {
const markdownBlock5: WorkspacePayloadMarkdown = {
_type: "WorkspacePayloadMarkdown",
text: `**Root Cause**:
${incident.rootCause}`,
};
blocks.push(markdownBlock5);
}
if (incident.remediationNotes) {
const markdownBlock6: WorkspacePayloadMarkdown = {
_type: "WorkspacePayloadMarkdown",
text: `**Remediation Notes**:
${incident.remediationNotes}`,
};
blocks.push(markdownBlock6);
}
if (incident.currentIncidentState?.name) {
const markdownBlock7: WorkspacePayloadMarkdown = {
_type: "WorkspacePayloadMarkdown",
text: `**Incident State**:
${incident.currentIncidentState.name}`,
};
blocks.push(markdownBlock7);
}
// TODO: Add buttons to Post Private Note, Ack Incident, Resolve Incident. etc.
return blocks as Array<WorkspaceMessageBlock>;
}
}
export default new Service();

View File

@@ -151,6 +151,12 @@ import ScheduledMaintenanceFeedService from "./ScheduledMaintenanceFeedService";
import AlertFeedService from "./AlertFeedService";
import IncidentFeedService from "./IncidentFeedService";
import MonitorTestService from "./MonitorTestService";
import WorkspaceProjectAuthTokenService from "./WorkspaceProjectAuthTokenService";
import WorkspaceUserAuthTokenService from "./WorkspaceUserAuthTokenService";
import WorkspaceSettingService from "./WorkspaceSettingService";
import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService";
const services: Array<BaseService> = [
AcmeCertificateService,
PromoCodeService,
@@ -314,6 +320,12 @@ const services: Array<BaseService> = [
AlertFeedService,
TableViewService,
MonitorTestService,
WorkspaceProjectAuthTokenService,
WorkspaceUserAuthTokenService,
WorkspaceSettingService,
WorkspaceNotificationRuleService,
];
export const AnalyticsServices: Array<

View File

@@ -52,6 +52,7 @@ import NotificationSettingEventType from "../../Types/NotificationSetting/Notifi
import Query from "../Types/Database/Query";
import DeleteBy from "../Types/Database/DeleteBy";
import StatusPageResourceService from "./StatusPageResourceService";
import Label from "../../Models/DatabaseModels/Label";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -643,6 +644,48 @@ export class Service extends DatabaseService<Model> {
}
}
public async getLabelsForMonitors(data: {
monitorIds: Array<ObjectID>;
}): Promise<Array<Label>> {
if (data.monitorIds.length === 0) {
return [];
}
const monitors: Array<Model> = await this.findBy({
query: {
_id: QueryHelper.any(data.monitorIds),
},
select: {
_id: true,
name: true,
labels: true,
},
props: {
isRoot: true,
},
skip: 0,
limit: LIMIT_PER_PROJECT,
});
const labels: Array<Label> = [];
for (const monitor of monitors) {
if (monitor.labels) {
for (const label of monitor.labels) {
const isLabelAlreadyAdded: boolean = labels.some((l: Label) => {
return l.id!.toString() === label.id!.toString();
});
if (!isLabelAlreadyAdded) {
labels.push(label);
}
}
}
}
return labels;
}
public async notifyOwnersWhenNoProbeIsEnabled(data: {
monitorId: ObjectID;
isNoProbesEnabled: boolean;

View File

@@ -1,5 +1,11 @@
import ResellerPlan from "Common/Models/DatabaseModels/ResellerPlan";
import { IsBillingEnabled, getAllEnvVars } from "../EnvironmentConfig";
import {
IsBillingEnabled,
NotificationSlackWebhookOnCreateProject,
NotificationSlackWebhookOnDeleteProject,
NotificationSlackWebhookOnSubscriptionUpdate,
getAllEnvVars,
} from "../EnvironmentConfig";
import AllMeteredPlans from "../Types/Billing/MeteredPlan/AllMeteredPlans";
import CreateBy from "../Types/Database/CreateBy";
import DeleteBy from "../Types/Database/DeleteBy";
@@ -61,6 +67,9 @@ import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
import AlertSeverityService from "./AlertSeverityService";
import AlertState from "../../Models/DatabaseModels/AlertState";
import AlertStateService from "./AlertStateService";
import SlackUtil from "../Utils/Workspace/Slack/Slack";
import URL from "../../Types/API/URL";
import Exception from "../../Types/Exception/Exception";
export interface CurrentPlan {
plan: PlanType | null;
@@ -95,6 +104,8 @@ export class ProjectService extends DatabaseService<Model> {
);
}
logger.debug("Creating project for user " + data.props.userId);
const user: User | null = await UserService.findOneById({
id: data.props.userId,
select: {
@@ -398,6 +409,13 @@ export class ProjectService extends DatabaseService<Model> {
project.paymentProviderSubscriptionSeats +
" completed and project updated.",
);
if (project.id) {
// send slack message on plan change.
await this.sendSubscriptionChangeWebhookSlackNotification(
project.id,
);
}
}
}
}
@@ -405,6 +423,57 @@ export class ProjectService extends DatabaseService<Model> {
return { updateBy, carryForward: [] };
}
private async sendSubscriptionChangeWebhookSlackNotification(
projectId: ObjectID,
): Promise<void> {
if (NotificationSlackWebhookOnSubscriptionUpdate) {
// fetch project again.
const project: Model | null = await this.findOneById({
id: new ObjectID(projectId.toString()),
select: {
name: true,
_id: true,
createdOwnerName: true,
createdOwnerEmail: true,
planName: true,
createdByUserId: true,
paymentProviderSubscriptionStatus: true,
},
props: {
isRoot: true,
},
});
if (!project) {
throw new BadDataException("Project not found");
}
let slackMessage: string = `*Project Plan Changed:*
*Project Name:* ${project.name?.toString() || "N/A"}
*Project ID:* ${project.id?.toString() || "N/A"}
`;
if (project.createdOwnerName && project.createdOwnerEmail) {
slackMessage += `*Project Created By:* ${project?.createdOwnerName?.toString() + " (" + project.createdOwnerEmail.toString() + ")" || "N/A"}
`;
}
if (IsBillingEnabled) {
// which plan?
slackMessage += `*Plan:* ${project.planName?.toString() || "N/A"}
*Subscription Status:* ${project.paymentProviderSubscriptionStatus?.toString() || "N/A"}
`;
}
SlackUtil.sendMessageToChannelViaIncomingWebhook({
url: URL.fromString(NotificationSlackWebhookOnSubscriptionUpdate),
text: slackMessage,
}).catch((error: Exception) => {
logger.error("Error sending slack message: " + error);
});
}
}
private async addDefaultScheduledMaintenanceState(
createdItem: Model,
): Promise<Model> {
@@ -561,6 +630,53 @@ export class ProjectService extends DatabaseService<Model> {
createdItem = await this.addDefaultScheduledMaintenanceState(createdItem);
createdItem = await this.addDefaultAlertState(createdItem);
if (NotificationSlackWebhookOnCreateProject) {
// fetch project again.
const project: Model | null = await this.findOneById({
id: createdItem.id!,
select: {
name: true,
_id: true,
createdOwnerName: true,
createdOwnerEmail: true,
planName: true,
createdByUserId: true,
paymentProviderSubscriptionStatus: true,
},
props: {
isRoot: true,
},
});
if (!project) {
throw new BadDataException("Project not found");
}
let slackMessage: string = `*Project Created:*
*Project Name:* ${project.name?.toString() || "N/A"}
*Project ID:* ${project.id?.toString() || "N/A"}
`;
if (project.createdOwnerName && project.createdOwnerEmail) {
slackMessage += `*Created By:* ${project?.createdOwnerName?.toString() + " (" + project.createdOwnerEmail.toString() + ")" || "N/A"}
`;
if (IsBillingEnabled) {
// which plan?
slackMessage += `*Plan:* ${project.planName?.toString() || "N/A"}
*Subscription Status:* ${project.paymentProviderSubscriptionStatus?.toString() || "N/A"}
`;
}
SlackUtil.sendMessageToChannelViaIncomingWebhook({
url: URL.fromString(NotificationSlackWebhookOnCreateProject),
text: slackMessage,
}).catch((error: Exception) => {
logger.error("Error sending slack message: " + error);
});
}
}
return createdItem;
}
@@ -1007,29 +1123,71 @@ export class ProjectService extends DatabaseService<Model> {
protected override async onBeforeDelete(
deleteBy: DeleteBy<Model>,
): Promise<OnDelete<Model>> {
if (IsBillingEnabled) {
const projects: Array<Model> = await this.findBy({
query: deleteBy.query,
props: deleteBy.props,
limit: LIMIT_MAX,
skip: 0,
select: {
_id: true,
paymentProviderSubscriptionId: true,
paymentProviderMeteredSubscriptionId: true,
const projects: Array<Model> = await this.findBy({
query: deleteBy.query,
props: {
isRoot: true,
},
limit: LIMIT_MAX,
skip: 0,
select: {
_id: true,
paymentProviderSubscriptionId: true,
paymentProviderMeteredSubscriptionId: true,
name: true,
createdByUser: {
name: true,
email: true,
},
});
},
});
return { deleteBy, carryForward: projects };
}
return { deleteBy, carryForward: [] };
return { deleteBy, carryForward: projects };
}
protected override async onDeleteSuccess(
onDelete: OnDelete<Model>,
_itemIdsBeforeDelete: ObjectID[],
): Promise<OnDelete<Model>> {
if (NotificationSlackWebhookOnDeleteProject) {
for (const project of onDelete.carryForward) {
let subscriptionStatus: SubscriptionStatus | null = null;
if (IsBillingEnabled) {
subscriptionStatus = await BillingService.getSubscriptionStatus(
project.paymentProviderSubscriptionId!,
);
}
let slackMessage: string = `*Project Deleted:*
*Project Name:* ${project.name?.toString() || "N/A"}
*Project ID:* ${project._id?.toString() || "N/A"}
`;
if (subscriptionStatus) {
slackMessage += `*Project Subscription Status:* ${subscriptionStatus?.toString() || "N/A"}
`;
}
if (
project.createdByUser &&
project.createdByUser.name &&
project.createdByUser.email
) {
slackMessage += `*Created By:* ${project?.createdByUser.name?.toString() + " (" + project.createdByUser.email.toString() + ")" || "N/A"}
`;
}
SlackUtil.sendMessageToChannelViaIncomingWebhook({
url: URL.fromString(NotificationSlackWebhookOnDeleteProject),
text: slackMessage,
}).catch((err: Error) => {
// log this error but do not throw it. Not important enough to stop the process.
logger.error(err);
});
}
}
// get project id
if (IsBillingEnabled) {
for (const project of onDelete.carryForward) {
@@ -1224,6 +1382,9 @@ export class ProjectService extends DatabaseService<Model> {
isRoot: true,
},
});
// send slack message on plan change.
await this.sendSubscriptionChangeWebhookSlackNotification(projectId);
}
public getActiveProjectStatusQuery(): Query<Model> {

View File

@@ -50,6 +50,8 @@ import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
import ScheduledMaintenanceFeedService from "./ScheduledMaintenanceFeedService";
import { ScheduledMaintenanceFeedEventType } from "../../Models/DatabaseModels/ScheduledMaintenanceFeed";
import { Gray500, Red500 } from "../../Types/BrandColors";
import Label from "../../Models/DatabaseModels/Label";
import LabelService from "./LabelService";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -59,6 +61,32 @@ export class Service extends DatabaseService<Model> {
}
}
public async getExistingScheduledMaintenanceNumberForProject(data: {
projectId: ObjectID;
}): Promise<number> {
// get last scheduledMaintenance number.
const lastScheduledMaintenance: Model | null = await this.findOneBy({
query: {
projectId: data.projectId,
},
select: {
scheduledMaintenanceNumber: true,
},
sort: {
createdAt: SortOrder.Descending,
},
props: {
isRoot: true,
},
});
if (!lastScheduledMaintenance) {
return 0;
}
return lastScheduledMaintenance.scheduledMaintenanceNumber || 0;
}
public async notififySubscribersOnEventScheduled(
scheduledEvents: Array<Model>,
): Promise<void> {
@@ -233,6 +261,8 @@ export class Service extends DatabaseService<Model> {
isPublicStatusPage: statuspage.isPublicStatusPage
? "true"
: "false",
subscriberEmailNotificationFooterText:
statuspage.subscriberEmailNotificationFooterText || "",
resourcesAffected: resourcesAffected,
scheduledAt:
OneUptimeDate.getDateAsFormattedHTMLInMultipleTimezones({
@@ -391,10 +421,13 @@ export class Service extends DatabaseService<Model> {
);
}
const projectId: ObjectID =
createBy.props.tenantId || createBy.data.projectId!;
const scheduledMaintenanceState: ScheduledMaintenanceState | null =
await ScheduledMaintenanceStateService.findOneBy({
query: {
projectId: createBy.props.tenantId,
projectId: projectId,
isScheduledState: true,
},
select: {
@@ -414,6 +447,14 @@ export class Service extends DatabaseService<Model> {
createBy.data.currentScheduledMaintenanceStateId =
scheduledMaintenanceState.id;
const scheduledMaintenanceNumberForThisScheduledMaintenance: number =
(await this.getExistingScheduledMaintenanceNumberForProject({
projectId: projectId,
})) + 1;
createBy.data.scheduledMaintenanceNumber =
scheduledMaintenanceNumberForThisScheduledMaintenance;
// get next notification date.
if (
@@ -451,7 +492,7 @@ export class Service extends DatabaseService<Model> {
scheduledMaintenanceFeedEventType:
ScheduledMaintenanceFeedEventType.ScheduledMaintenanceCreated,
displayColor: Red500,
feedInfoInMarkdown: `**Scheduled Maintenance Created**:
feedInfoInMarkdown: `**Scheduled Maintenance #${createdItem.scheduledMaintenanceNumber?.toString()} Created**:
**Scheduled Maintenance Title**:
@@ -692,40 +733,219 @@ ${createdItem.description || "No description provided."}
if (updatedItemIds.length > 0) {
for (const scheduledMaintenanceId of updatedItemIds) {
let shouldAddScheduledMaintenanceFeed: boolean = false;
let feedInfoInMarkdown: string =
"**Scheduled Maintenance was updated.**";
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
if (onUpdate.updateBy.data.title) {
// add scheduledMaintenance feed.
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeed({
scheduledMaintenanceId: scheduledMaintenanceId,
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
scheduledMaintenanceFeedEventType:
ScheduledMaintenanceFeedEventType.ScheduledMaintenanceUpdated,
displayColor: Gray500,
feedInfoInMarkdown: `**Scheduled Maintenance title was updated.** Here's the new title.
feedInfoInMarkdown += `\n\n**Title**:
${onUpdate.updateBy.data.title || "No title provided."}
`,
userId: createdByUserId || undefined,
});
`;
shouldAddScheduledMaintenanceFeed = true;
}
if (onUpdate.updateBy.data.startsAt) {
// add scheduledMaintenance feed.
feedInfoInMarkdown += `\n\n**Starts At**:
${OneUptimeDate.getDateAsLocalFormattedString(onUpdate.updateBy.data.startsAt as Date) || "No title provided."}
`;
shouldAddScheduledMaintenanceFeed = true;
}
if (onUpdate.updateBy.data.endsAt) {
// add scheduledMaintenance feed.
feedInfoInMarkdown += `\n\n**Ends At**:
${OneUptimeDate.getDateAsLocalFormattedString(onUpdate.updateBy.data.endsAt as Date) || "No title provided."}
`;
shouldAddScheduledMaintenanceFeed = true;
}
if (onUpdate.updateBy.data.description) {
// add scheduledMaintenance feed.
const createdByUserId: ObjectID | undefined | null =
onUpdate.updateBy.props.userId;
feedInfoInMarkdown += `\n\n**Scheduled Maintenance Description**:
${onUpdate.updateBy.data.description || "No description provided."}
`;
shouldAddScheduledMaintenanceFeed = true;
}
if (
onUpdate.updateBy.data.sendSubscriberNotificationsOnBeforeTheEvent &&
Array.isArray(
onUpdate.updateBy.data.sendSubscriberNotificationsOnBeforeTheEvent,
) &&
onUpdate.updateBy.data.sendSubscriberNotificationsOnBeforeTheEvent
.length > 0
) {
feedInfoInMarkdown += `\n\n**Notify Subscribers Before Event Starts**:
${(
onUpdate.updateBy.data
.sendSubscriberNotificationsOnBeforeTheEvent as Array<Recurring>
)
.map((recurring: Recurring) => {
return `- ${(recurring as Recurring).toString()}`;
})
.join("\n")}
`;
shouldAddScheduledMaintenanceFeed = true;
}
if (
onUpdate.updateBy.data.monitors &&
onUpdate.updateBy.data.monitors.length > 0 &&
Array.isArray(onUpdate.updateBy.data.monitors)
) {
const monitorIds: Array<ObjectID> = (
onUpdate.updateBy.data.monitors as any
)
.map((monitor: Label) => {
if (monitor._id) {
return new ObjectID(monitor._id?.toString());
}
return null;
})
.filter((monitorId: ObjectID | null) => {
return monitorId !== null;
});
const monitors: Array<Label> = await MonitorService.findBy({
query: {
_id: QueryHelper.any(monitorIds),
},
select: {
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
props: {
isRoot: true,
},
});
if (monitors.length > 0) {
feedInfoInMarkdown += `\n\n**Resources Affected**:
${monitors
.map((monitor: Monitor) => {
return `- ${monitor.name}`;
})
.join("\n")}
`;
shouldAddScheduledMaintenanceFeed = true;
}
}
if (
onUpdate.updateBy.data.statusPages &&
onUpdate.updateBy.data.statusPages.length > 0 &&
Array.isArray(onUpdate.updateBy.data.statusPages)
) {
const statusPageIds: Array<ObjectID> = (
onUpdate.updateBy.data.statusPages as any
)
.map((statusPage: Label) => {
if (statusPage._id) {
return new ObjectID(statusPage._id?.toString());
}
return null;
})
.filter((statusPageId: ObjectID | null) => {
return statusPageId !== null;
});
const statusPages: Array<Label> = await StatusPageService.findBy({
query: {
_id: QueryHelper.any(statusPageIds),
},
select: {
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
props: {
isRoot: true,
},
});
if (statusPages.length > 0) {
feedInfoInMarkdown += `\n\n**Show on these status pages:**:
${statusPages
.map((statusPage: StatusPage) => {
return `- ${statusPage.name}`;
})
.join("\n")}
`;
shouldAddScheduledMaintenanceFeed = true;
}
}
if (
onUpdate.updateBy.data.labels &&
onUpdate.updateBy.data.labels.length > 0 &&
Array.isArray(onUpdate.updateBy.data.labels)
) {
const labelIds: Array<ObjectID> = (
onUpdate.updateBy.data.labels as any
)
.map((label: Label) => {
if (label._id) {
return new ObjectID(label._id?.toString());
}
return null;
})
.filter((labelId: ObjectID | null) => {
return labelId !== null;
});
const labels: Array<Label> = await LabelService.findBy({
query: {
_id: QueryHelper.any(labelIds),
},
select: {
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
props: {
isRoot: true,
},
});
if (labels.length > 0) {
feedInfoInMarkdown += `\n\n**Labels**:
${labels
.map((label: Label) => {
return `- ${label.name}`;
})
.join("\n")}
`;
shouldAddScheduledMaintenanceFeed = true;
}
}
if (shouldAddScheduledMaintenanceFeed) {
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeed({
scheduledMaintenanceId: scheduledMaintenanceId,
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
scheduledMaintenanceFeedEventType:
ScheduledMaintenanceFeedEventType.ScheduledMaintenanceUpdated,
displayColor: Gray500,
feedInfoInMarkdown: `**Scheduled Maintenance description was updated.** Here's the new description.
${onUpdate.updateBy.data.description || "No description provided."}
`,
feedInfoInMarkdown: feedInfoInMarkdown,
userId: createdByUserId || undefined,
});
}

View File

@@ -148,6 +148,12 @@ export class Service extends DatabaseService<StatusPage> {
createBy.data.defaultBarColor = Green;
}
if (!createBy.data.subscriberEmailNotificationFooterText) {
createBy.data.subscriberEmailNotificationFooterText =
"This is an automated email sent to you because you are subscribed to " +
createBy.data.name;
}
return {
createBy,
carryForward: null,
@@ -469,7 +475,9 @@ export class Service extends DatabaseService<StatusPage> {
},
});
let statusPageURL: string = domain?.fullDomain || "";
let statusPageURL: string = domain?.fullDomain
? `https://${domain.fullDomain}`
: "";
if (!statusPageURL) {
const host: Hostname = await DatabaseConfig.getHost();
@@ -645,6 +653,8 @@ export class Service extends DatabaseService<StatusPage> {
templateType: EmailTemplateType.StatusPageSubscriberReport,
vars: {
statusPageName: statusPageName,
subscriberEmailNotificationFooterText:
statuspage.subscriberEmailNotificationFooterText || "",
statusPageUrl: statusPageURL,
hasResources: report.totalResources > 0 ? "true" : "false",
report: report as any,

View File

@@ -356,6 +356,11 @@ export class Service extends DatabaseService<Model> {
subscriber.subscriberEmail &&
subscriber._id
) {
const unsubscribeUrl: string = this.getUnsubscribeLink(
URL.fromString(statusPageURL),
subscriber.id!,
).toString();
MailService.sendMail(
{
toEmail: subscriber.subscriberEmail,
@@ -373,6 +378,7 @@ export class Service extends DatabaseService<Model> {
? "true"
: "false",
confirmationUrl: confirmSubscriptionLink,
unsubscribeUrl: unsubscribeUrl,
},
subject: "Confirm your subscription to " + statusPageName,
},
@@ -630,6 +636,7 @@ export class Service extends DatabaseService<Model> {
isPublicStatusPage: true,
logoFileId: true,
allowSubscribersToChooseResources: true,
subscriberEmailNotificationFooterText: true,
allowSubscribersToChooseEventTypes: true,
smtpConfig: {
_id: true,

View File

@@ -90,6 +90,7 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
isNewUser = true;
user = await UserService.createByEmail({
email,
name: undefined, // name is not required for now.
props: {
isRoot: true,
},

View File

@@ -1,7 +1,7 @@
import DatabaseConfig from "../DatabaseConfig";
import {
IsBillingEnabled,
NotificationWebhookOnCreateUser,
NotificationSlackWebhookOnCreateUser,
} from "../EnvironmentConfig";
import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
import UpdateBy from "../Types/Database/UpdateBy";
@@ -28,10 +28,11 @@ import Text from "../../Types/Text";
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
import Model from "Common/Models/DatabaseModels/User";
import SlackUtil from "../Utils/Slack";
import SlackUtil from "../Utils/Workspace/Slack/Slack";
import UserTwoFactorAuth from "Common/Models/DatabaseModels/UserTwoFactorAuth";
import UserTwoFactorAuthService from "./UserTwoFactorAuthService";
import BadDataException from "../../Types/Exception/BadDataException";
import Name from "../../Types/Name";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -42,9 +43,9 @@ export class Service extends DatabaseService<Model> {
_onCreate: OnCreate<Model>,
createdItem: Model,
): Promise<Model> {
if (NotificationWebhookOnCreateUser) {
SlackUtil.sendMessageToChannel({
url: URL.fromString(NotificationWebhookOnCreateUser),
if (NotificationSlackWebhookOnCreateUser) {
SlackUtil.sendMessageToChannelViaIncomingWebhook({
url: URL.fromString(NotificationSlackWebhookOnCreateUser),
text: `*New OneUptime User:*
*Email:* ${createdItem.email?.toString() || "N/A"}
*Name:* ${createdItem.name?.toString() || "N/A"}
@@ -292,6 +293,7 @@ export class Service extends DatabaseService<Model> {
public async createByEmail(data: {
email: Email;
name: Name | undefined;
isEmailVerified?: boolean;
generateRandomPassword?: boolean;
props: DatabaseCommonInteractionProps;
@@ -300,6 +302,9 @@ export class Service extends DatabaseService<Model> {
const user: Model = new Model();
user.email = email;
if (data.name) {
user.name = data.name;
}
user.isEmailVerified = data.isEmailVerified || false;
if (data.generateRandomPassword) {

View File

@@ -6,7 +6,7 @@ export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
if (IsBillingEnabled) {
this.hardDeleteItemsOlderThanInDays("createdAt", 3);
this.hardDeleteItemsOlderThanInDays("createdAt", 30);
}
}
}

View File

@@ -0,0 +1,711 @@
import ObjectID from "../../Types/ObjectID";
import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType";
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
import DatabaseService from "./DatabaseService";
import Model from "Common/Models/DatabaseModels/WorkspaceNotificationRule";
import IncidentNotificationRule from "../../Types/Workspace/NotificationRules/NotificationRuleTypes/IncidentNotificationRule";
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
import Incident from "../../Models/DatabaseModels/Incident";
import IncidentService from "./IncidentService";
import { NotificationRuleConditionCheckOn } from "../../Types/Workspace/NotificationRules/NotificationRuleCondition";
import BadDataException from "../../Types/Exception/BadDataException";
import Label from "../../Models/DatabaseModels/Label";
import MonitorService from "./MonitorService";
import Alert from "../../Models/DatabaseModels/Alert";
import AlertService from "./AlertService";
import ScheduledMaintenanceService from "./ScheduledMaintenanceService";
import ScheduledMaintenance from "../../Models/DatabaseModels/ScheduledMaintenance";
import MonitorStatusTimeline from "Common/Models/DatabaseModels/MonitorStatusTimeline";
import MonitorStatusTimelineService from "./MonitorStatusTimelineService";
import { WorkspaceNotificationRuleUtil } from "../../Types/Workspace/NotificationRules/NotificationRuleUtil";
import TeamMemberService from "./TeamMemberService";
import User from "../../Models/DatabaseModels/User";
import BaseNotificationRule from "../../Types/Workspace/NotificationRules/BaseNotificationRule";
import CreateChannelNotificationRule from "../../Types/Workspace/NotificationRules/CreateChannelNotificationRule";
import { WorkspaceChannel } from "../Utils/Workspace/WorkspaceBase";
import WorkspaceUtil from "../Utils/Workspace/Workspace";
import WorkspaceUserAuthToken from "../../Models/DatabaseModels/WorkspaceUserAuthToken";
import WorkspaceUserAuthTokenService from "./WorkspaceUserAuthTokenService";
import WorkspaceMessagePayload, {
WorkspaceMessageBlock,
} from "../../Types/Workspace/WorkspaceMessagePayload";
import WorkspaceProjectAuthToken from "../../Models/DatabaseModels/WorkspaceProjectAuthToken";
import WorkspaceProjectAuthTokenService from "./WorkspaceProjectAuthTokenService";
export interface NotificationFor {
incidentId?: ObjectID | undefined;
alertId?: ObjectID | undefined;
scheduledMaintenanceId?: ObjectID | undefined;
monitorStatusTimelineId?: ObjectID | undefined;
}
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
}
public async createInviteAndPostToChannelsBasedOnRules(data: {
projectId: ObjectID;
notificationRuleEventType: NotificationRuleEventType;
notificationFor: NotificationFor;
channelNameSiffix: string;
messageBlocks: Array<WorkspaceMessageBlock>;
}): Promise<{
channelsCreated: Array<WorkspaceChannel>;
} | null> {
const channelsCreated: Array<WorkspaceChannel> = [];
const projectAuths: Array<WorkspaceProjectAuthToken> =
await WorkspaceProjectAuthTokenService.getProjectAuths({
projectId: data.projectId,
});
if (!projectAuths || projectAuths.length === 0) {
// do nothing.
return null;
}
for (const projectAuth of projectAuths) {
if (!projectAuth.authToken) {
continue;
}
if (!projectAuth.workspaceType) {
continue;
}
const authToken: string = projectAuth.authToken;
const workspaceType: WorkspaceType = projectAuth.workspaceType;
const notificationRules: Array<Model> =
await this.getMatchingNotificationRules({
projectId: data.projectId,
workspaceType: workspaceType,
notificationRuleEventType: data.notificationRuleEventType,
notificationFor: data.notificationFor,
});
if (!notificationRules || notificationRules.length === 0) {
return null;
}
const createdWorkspaceChannels: Array<WorkspaceChannel> =
await this.createChannelsBasedOnRules({
projectOrUserAuthTokenForWorkspasce: authToken,
workspaceType: workspaceType,
notificationRules: notificationRules.map((rule: Model) => {
return rule.notificationRule as CreateChannelNotificationRule;
}),
channelNameSiffix: data.channelNameSiffix,
notificationEventType: data.notificationRuleEventType,
});
await this.inviteUsersAndTeamsToChannelsBasedOnRules({
projectId: data.projectId,
projectOrUserAuthTokenForWorkspasce: authToken,
workspaceType: workspaceType,
notificationRules: notificationRules.map((rule: Model) => {
return rule.notificationRule as CreateChannelNotificationRule;
}),
channelNames: createdWorkspaceChannels.map(
(channel: WorkspaceChannel) => {
return channel.name;
},
),
});
const existingChannelNames: Array<string> =
this.getExistingChannelNamesFromNotificationRules({
notificationRules: notificationRules.map((rule: Model) => {
return rule.notificationRule as BaseNotificationRule;
}),
}) || [];
// add created channel names to existing channel names.
for (const channel of createdWorkspaceChannels) {
if (!existingChannelNames.includes(channel.name)) {
existingChannelNames.push(channel.name);
}
}
await this.postToWorkspaceChannels({
projectOrUserAuthTokenForWorkspasce: authToken,
workspaceType: workspaceType,
workspaceMessagePayload: {
_type: "WorkspaceMessagePayload",
channelNames: existingChannelNames,
messageBlocks: data.messageBlocks,
},
});
channelsCreated.push(...createdWorkspaceChannels);
}
return {
channelsCreated: channelsCreated,
};
}
public async postToWorkspaceChannels(data: {
projectOrUserAuthTokenForWorkspasce: string;
workspaceType: WorkspaceType;
workspaceMessagePayload: WorkspaceMessagePayload;
}): Promise<void> {
await WorkspaceUtil.getWorkspaceTypeUtil(data.workspaceType).sendMessage({
workspaceMessagePayload: data.workspaceMessagePayload,
authToken: data.projectOrUserAuthTokenForWorkspasce,
});
}
public async inviteUsersAndTeamsToChannelsBasedOnRules(data: {
projectId: ObjectID;
projectOrUserAuthTokenForWorkspasce: string;
workspaceType: WorkspaceType;
notificationRules: Array<CreateChannelNotificationRule>;
channelNames: Array<string>;
}): Promise<void> {
const inviteUserIds: Array<ObjectID> =
await this.getUsersIdsToInviteToChannel({
notificationRules: data.notificationRules,
});
const workspaceUserIds: Array<string> = [];
for (const userId of inviteUserIds) {
const workspaceUserId: string | null =
await this.getWorkspaceUserIdFromOneUptimeUserId({
projectId: data.projectId,
workspaceType: data.workspaceType,
oneupitmeUserId: userId,
});
if (workspaceUserId) {
workspaceUserIds.push(workspaceUserId);
}
}
await WorkspaceUtil.getWorkspaceTypeUtil(
data.workspaceType,
).inviteUsersToChannels({
authToken: data.projectOrUserAuthTokenForWorkspasce,
workspaceChannelInvitationPayload: {
channelNames: data.channelNames,
workspaceUserIds: workspaceUserIds,
},
});
}
public async getWorkspaceUserIdFromOneUptimeUserId(data: {
projectId: ObjectID;
workspaceType: WorkspaceType;
oneupitmeUserId: ObjectID;
}): Promise<string | null> {
const userAuth: WorkspaceUserAuthToken | null =
await WorkspaceUserAuthTokenService.findOneBy({
query: {
projectId: data.projectId,
workspaceType: data.workspaceType,
userId: data.oneupitmeUserId,
},
select: {
workspaceUserId: true,
},
props: {
isRoot: true,
},
});
if (!userAuth) {
return null;
}
return userAuth.workspaceUserId?.toString() || null;
}
public async createChannelsBasedOnRules(data: {
projectOrUserAuthTokenForWorkspasce: string;
workspaceType: WorkspaceType;
notificationRules: Array<CreateChannelNotificationRule>;
channelNameSiffix: string;
notificationEventType: NotificationRuleEventType;
}): Promise<Array<WorkspaceChannel>> {
const createdWorkspaceChannels: Array<WorkspaceChannel> = [];
const createdChannelNames: Array<string> = [];
const newChannelNames: Array<string> =
this.getNewChannelNamesFromNotificationRules({
notificationRules: data.notificationRules,
channelNameSiffix: data.channelNameSiffix,
notificationEventType: data.notificationEventType,
});
if (!newChannelNames || newChannelNames.length === 0) {
return [];
}
for (const newChannelName of newChannelNames) {
// if already created then skip it.
if (createdChannelNames.includes(newChannelName)) {
continue;
}
// create channel.
const channel: WorkspaceChannel =
await WorkspaceUtil.getWorkspaceTypeUtil(
data.workspaceType,
).createChannel({
authToken: data.projectOrUserAuthTokenForWorkspasce,
channelName: newChannelName,
});
createdChannelNames.push(channel.name);
createdWorkspaceChannels.push(channel);
}
return createdWorkspaceChannels;
}
public async getUsersIdsToInviteToChannel(data: {
notificationRules: Array<CreateChannelNotificationRule>;
}): Promise<Array<ObjectID>> {
const inviteUserIds: Array<ObjectID> = [];
for (const notificationRule of data.notificationRules) {
const workspaceRules: CreateChannelNotificationRule = notificationRule;
if (workspaceRules.shouldCreateNewChannel) {
if (
workspaceRules.inviteUsersToNewChannel &&
workspaceRules.inviteUsersToNewChannel.length > 0
) {
const userIds: Array<ObjectID> =
workspaceRules.inviteUsersToNewChannel || [];
for (const userId of userIds) {
if (
!inviteUserIds.find((id: ObjectID) => {
return id.toString() === userId.toString();
})
) {
inviteUserIds.push(new ObjectID(userId.toString()));
}
}
}
if (
workspaceRules.inviteTeamsToNewChannel &&
workspaceRules.inviteTeamsToNewChannel.length > 0
) {
let teamIds: Array<ObjectID> =
workspaceRules.inviteTeamsToNewChannel || [];
teamIds = teamIds.map((teamId: ObjectID) => {
return new ObjectID(teamId.toString());
});
const usersInTeam: Array<User> =
await TeamMemberService.getUsersInTeams(teamIds);
for (const user of usersInTeam) {
if (
!inviteUserIds.find((id: ObjectID) => {
return id.toString() === user._id?.toString();
})
) {
const userId: string | undefined = user._id?.toString();
if (userId) {
inviteUserIds.push(new ObjectID(userId));
}
}
}
}
}
}
return inviteUserIds;
}
public getExistingChannelNamesFromNotificationRules(data: {
notificationRules: Array<BaseNotificationRule>;
}): Array<string> {
const channelNames: Array<string> = [];
for (const notificationRule of data.notificationRules) {
const workspaceRules: BaseNotificationRule = notificationRule;
if (workspaceRules.shouldPostToExistingChannel) {
const existingChannelNames: Array<string> =
workspaceRules.existingChannelNames.split(",");
for (const channelName of existingChannelNames) {
if (!channelName) {
// if channel name is empty then skip it.
continue;
}
if (!channelNames.includes(channelName)) {
// if channel name is not already added then add it.
channelNames.push(channelName);
}
}
}
}
return channelNames;
}
public getNewChannelNamesFromNotificationRules(data: {
notificationEventType: NotificationRuleEventType;
notificationRules: Array<CreateChannelNotificationRule>;
channelNameSiffix: string;
}): Array<string> {
const channelNames: Array<string> = [];
for (const notificationRule of data.notificationRules) {
const workspaceRules: CreateChannelNotificationRule = notificationRule;
if (
workspaceRules.shouldCreateNewChannel &&
workspaceRules.newChannelTemplateName
) {
const newChannelName: string =
workspaceRules.newChannelTemplateName ||
`oneuptime-${data.notificationEventType.toLowerCase()}-`;
// add suffix and then check if it is already added or not.
const channelName: string = newChannelName + data.channelNameSiffix;
if (!channelNames.includes(channelName)) {
// if channel name is not already added then add it.
channelNames.push(channelName);
}
}
}
return channelNames;
}
private async getNotificationRules(data: {
projectId: ObjectID;
workspaceType: WorkspaceType;
notificationRuleEventType: NotificationRuleEventType;
}): Promise<Array<Model>> {
return await this.findBy({
query: {
projectId: data.projectId,
workspaceType: data.workspaceType,
eventType: data.notificationRuleEventType,
},
select: {
notificationRule: true,
},
props: {
isRoot: true,
},
skip: 0,
limit: LIMIT_PER_PROJECT,
});
}
private async getValuesBasedOnNotificationFor(data: {
notificationFor: NotificationFor;
}): Promise<{
[key in NotificationRuleConditionCheckOn]:
| string
| Array<string>
| undefined;
}> {
if (data.notificationFor.incidentId) {
const incident: Incident | null = await IncidentService.findOneById({
id: data.notificationFor.incidentId,
select: {
title: true,
description: true,
incidentSeverity: true,
currentIncidentState: true,
labels: true,
monitors: true,
},
props: {
isRoot: true,
},
});
if (!incident) {
throw new BadDataException("Incident ID not found");
}
const monitorLabels: Array<Label> =
await MonitorService.getLabelsForMonitors({
monitorIds:
incident.monitors?.map((monitor: Incident) => {
return monitor.id!;
}) || [],
});
return {
[NotificationRuleConditionCheckOn.MonitorName]: undefined,
[NotificationRuleConditionCheckOn.IncidentTitle]: incident.title || "",
[NotificationRuleConditionCheckOn.IncidentDescription]:
incident.description || "",
[NotificationRuleConditionCheckOn.IncidentSeverity]:
incident.incidentSeverity?._id?.toString() || "",
[NotificationRuleConditionCheckOn.IncidentState]:
incident.currentIncidentState?._id?.toString() || "",
[NotificationRuleConditionCheckOn.MonitorType]: undefined,
[NotificationRuleConditionCheckOn.MonitorStatus]: undefined,
[NotificationRuleConditionCheckOn.AlertTitle]: undefined,
[NotificationRuleConditionCheckOn.AlertDescription]: undefined,
[NotificationRuleConditionCheckOn.AlertSeverity]: undefined,
[NotificationRuleConditionCheckOn.AlertState]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription]:
undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceState]: undefined,
[NotificationRuleConditionCheckOn.IncidentLabels]:
incident.labels?.map((label: Label) => {
return label._id?.toString() || "";
}) || [],
[NotificationRuleConditionCheckOn.AlertLabels]: undefined,
[NotificationRuleConditionCheckOn.MonitorLabels]:
monitorLabels.map((label: Label) => {
return label._id?.toString() || "";
}) || [],
[NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels]:
undefined,
[NotificationRuleConditionCheckOn.Monitors]:
incident.monitors?.map((monitor: Incident) => {
return monitor._id?.toString() || "";
}) || [],
};
}
if (data.notificationFor.alertId) {
const alert: Alert | null = await AlertService.findOneById({
id: data.notificationFor.alertId,
select: {
title: true,
description: true,
alertSeverity: true,
currentAlertState: true,
labels: true,
monitor: true,
},
props: {
isRoot: true,
},
});
if (!alert) {
throw new BadDataException("Alert ID not found");
}
const monitorLabels: Array<Label> =
await MonitorService.getLabelsForMonitors({
monitorIds: alert?.monitor?.id ? [alert?.monitor?.id] : [],
});
return {
[NotificationRuleConditionCheckOn.MonitorName]: undefined,
[NotificationRuleConditionCheckOn.IncidentTitle]: undefined,
[NotificationRuleConditionCheckOn.IncidentDescription]: undefined,
[NotificationRuleConditionCheckOn.IncidentSeverity]: undefined,
[NotificationRuleConditionCheckOn.IncidentState]: undefined,
[NotificationRuleConditionCheckOn.MonitorType]: undefined,
[NotificationRuleConditionCheckOn.MonitorStatus]: undefined,
[NotificationRuleConditionCheckOn.AlertTitle]: alert.title || "",
[NotificationRuleConditionCheckOn.AlertDescription]:
alert.description || "",
[NotificationRuleConditionCheckOn.AlertSeverity]:
alert.alertSeverity?._id?.toString() || "",
[NotificationRuleConditionCheckOn.AlertState]:
alert.currentAlertState?._id?.toString() || "",
[NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription]:
undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceState]: undefined,
[NotificationRuleConditionCheckOn.IncidentLabels]: undefined,
[NotificationRuleConditionCheckOn.AlertLabels]:
alert.labels?.map((label: Label) => {
return label._id?.toString() || "";
}) || [],
[NotificationRuleConditionCheckOn.MonitorLabels]:
monitorLabels.map((label: Label) => {
return label._id?.toString() || "";
}) || [],
[NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels]:
undefined,
[NotificationRuleConditionCheckOn.Monitors]: [
alert.monitor?.id!.toString() || "",
],
};
}
if (data.notificationFor.scheduledMaintenanceId) {
const scheduledMaintenance: ScheduledMaintenance | null =
await ScheduledMaintenanceService.findOneById({
id: data.notificationFor.scheduledMaintenanceId,
select: {
title: true,
description: true,
currentScheduledMaintenanceState: true,
labels: true,
monitors: true,
},
props: {
isRoot: true,
},
});
if (!scheduledMaintenance) {
throw new BadDataException("Scheduled Maintenance ID not found");
}
const monitorLabels: Array<Label> =
await MonitorService.getLabelsForMonitors({
monitorIds:
scheduledMaintenance.monitors?.map(
(monitor: ScheduledMaintenance) => {
return monitor.id!;
},
) || [],
});
return {
[NotificationRuleConditionCheckOn.MonitorName]: undefined,
[NotificationRuleConditionCheckOn.IncidentTitle]: undefined,
[NotificationRuleConditionCheckOn.IncidentDescription]: undefined,
[NotificationRuleConditionCheckOn.IncidentSeverity]: undefined,
[NotificationRuleConditionCheckOn.IncidentState]: undefined,
[NotificationRuleConditionCheckOn.MonitorType]: undefined,
[NotificationRuleConditionCheckOn.MonitorStatus]: undefined,
[NotificationRuleConditionCheckOn.AlertTitle]: undefined,
[NotificationRuleConditionCheckOn.AlertDescription]: undefined,
[NotificationRuleConditionCheckOn.AlertSeverity]: undefined,
[NotificationRuleConditionCheckOn.AlertState]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle]:
scheduledMaintenance.title || "",
[NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription]:
scheduledMaintenance.description || "",
[NotificationRuleConditionCheckOn.ScheduledMaintenanceState]:
scheduledMaintenance.currentScheduledMaintenanceState?._id?.toString() ||
"",
[NotificationRuleConditionCheckOn.IncidentLabels]: undefined,
[NotificationRuleConditionCheckOn.AlertLabels]: undefined,
[NotificationRuleConditionCheckOn.MonitorLabels]:
monitorLabels.map((label: Label) => {
return label._id?.toString() || "";
}) || [],
[NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels]:
scheduledMaintenance.labels?.map((label: Label) => {
return label._id?.toString() || "";
}) || [],
[NotificationRuleConditionCheckOn.Monitors]:
scheduledMaintenance.monitors?.map(
(monitor: ScheduledMaintenance) => {
return monitor._id?.toString() || "";
},
) || [],
};
}
if (data.notificationFor.monitorStatusTimelineId) {
const monitorStatusTimeline: MonitorStatusTimeline | null =
await MonitorStatusTimelineService.findOneById({
id: data.notificationFor.monitorStatusTimelineId,
select: {
monitor: {
name: true,
labels: true,
monitorType: true,
},
monitorStatus: true,
},
props: {
isRoot: true,
},
});
if (!monitorStatusTimeline) {
throw new BadDataException("Monitor Status Timeline ID not found");
}
const monitorLabels: Array<Label> =
monitorStatusTimeline.monitor?.labels || [];
return {
[NotificationRuleConditionCheckOn.MonitorName]:
monitorStatusTimeline.monitor?.name || "",
[NotificationRuleConditionCheckOn.IncidentTitle]: undefined,
[NotificationRuleConditionCheckOn.IncidentDescription]: undefined,
[NotificationRuleConditionCheckOn.IncidentSeverity]: undefined,
[NotificationRuleConditionCheckOn.IncidentState]: undefined,
[NotificationRuleConditionCheckOn.MonitorType]:
monitorStatusTimeline.monitor?.monitorType || undefined,
[NotificationRuleConditionCheckOn.MonitorStatus]:
monitorStatusTimeline.monitorStatus?._id?.toString() || "",
[NotificationRuleConditionCheckOn.AlertTitle]: undefined,
[NotificationRuleConditionCheckOn.AlertDescription]: undefined,
[NotificationRuleConditionCheckOn.AlertSeverity]: undefined,
[NotificationRuleConditionCheckOn.AlertState]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription]:
undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceState]: undefined,
[NotificationRuleConditionCheckOn.IncidentLabels]: undefined,
[NotificationRuleConditionCheckOn.AlertLabels]: undefined,
[NotificationRuleConditionCheckOn.MonitorLabels]:
monitorLabels.map((label: Label) => {
return label._id?.toString() || "";
}) || [],
[NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels]:
undefined,
[NotificationRuleConditionCheckOn.Monitors]: [
monitorStatusTimeline.monitor?._id?.toString() || "",
],
};
}
throw new BadDataException("NotificationFor is not supported");
}
public async getMatchingNotificationRules(data: {
projectId: ObjectID;
workspaceType: WorkspaceType;
notificationRuleEventType: NotificationRuleEventType;
notificationFor: NotificationFor;
}): Promise<Array<Model>> {
const notificationRules: Array<Model> = await this.getNotificationRules({
projectId: data.projectId,
workspaceType: data.workspaceType,
notificationRuleEventType: data.notificationRuleEventType,
});
const values: {
[key in NotificationRuleConditionCheckOn]:
| string
| Array<string>
| undefined;
} = await this.getValuesBasedOnNotificationFor({
notificationFor: data.notificationFor,
});
const matchingNotificationRules: Array<Model> = [];
for (const notificationRule of notificationRules) {
if (
WorkspaceNotificationRuleUtil.isRuleMatching({
notificationRule:
notificationRule.notificationRule as IncidentNotificationRule,
values: values,
})
) {
matchingNotificationRules.push(notificationRule);
}
}
return matchingNotificationRules;
}
}
export default new Service();

View File

@@ -0,0 +1,112 @@
import ObjectID from "../../Types/ObjectID";
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
import DatabaseService from "./DatabaseService";
import Model, {
SlackMiscData,
} from "Common/Models/DatabaseModels/WorkspaceProjectAuthToken";
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
}
public async getProjectAuth(data: {
projectId: ObjectID;
workspaceType: WorkspaceType;
}): Promise<Model | null> {
return await this.findOneBy({
query: {
projectId: data.projectId,
workspaceType: data.workspaceType,
},
select: {
authToken: true,
workspaceProjectId: true,
miscData: true,
},
props: {
isRoot: true,
},
});
}
public async getProjectAuths(data: {
projectId: ObjectID;
}): Promise<Array<Model>> {
return await this.findBy({
query: {
projectId: data.projectId,
},
select: {
authToken: true,
workspaceProjectId: true,
miscData: true,
workspaceType: true,
},
skip: 0,
limit: LIMIT_PER_PROJECT,
props: {
isRoot: true,
},
});
}
public async doesExist(data: {
projectId: ObjectID;
workspaceType: WorkspaceType;
}): Promise<boolean> {
return Boolean(await this.getProjectAuth(data));
}
public async refreshAuthToken(data: {
projectId: ObjectID;
workspaceType: WorkspaceType;
authToken: string;
workspaceProjectId: string;
miscData: SlackMiscData;
}): Promise<void> {
let projectAuth: Model | null = await this.findOneBy({
query: {
projectId: data.projectId,
workspaceType: data.workspaceType,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
});
if (!projectAuth) {
projectAuth = new Model();
projectAuth.projectId = data.projectId;
projectAuth.authToken = data.authToken;
projectAuth.workspaceType = data.workspaceType;
projectAuth.workspaceProjectId = data.workspaceProjectId;
projectAuth.miscData = data.miscData;
await this.create({
data: projectAuth,
props: {
isRoot: true,
},
});
} else {
await this.updateOneById({
id: projectAuth.id!,
data: {
authToken: data.authToken,
workspaceProjectId: data.workspaceProjectId,
miscData: data.miscData,
},
props: {
isRoot: true,
},
});
}
}
}
export default new Service();

View File

@@ -0,0 +1,78 @@
import ObjectID from "../../Types/ObjectID";
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
import DatabaseService from "./DatabaseService";
import Model, {
SlackSettings,
} from "Common/Models/DatabaseModels/WorkspaceSetting";
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
}
public async doesExist(data: {
projectId: ObjectID;
workspaceType: WorkspaceType;
}): Promise<boolean> {
return (
(
await this.countBy({
query: {
projectId: data.projectId,
workspaceType: data.workspaceType,
},
skip: 0,
limit: 1,
props: {
isRoot: true,
},
})
).toNumber() > 0
);
}
public async refreshSetting(data: {
projectId: ObjectID;
workspaceType: WorkspaceType;
settings: SlackSettings;
}): Promise<void> {
let workspaceSetting: Model | null = await this.findOneBy({
query: {
projectId: data.projectId,
workspaceType: data.workspaceType,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
});
if (!workspaceSetting) {
workspaceSetting = new Model();
workspaceSetting.projectId = data.projectId;
workspaceSetting.settings = data.settings;
workspaceSetting.workspaceType = data.workspaceType;
await this.create({
data: workspaceSetting,
props: {
isRoot: true,
},
});
} else {
await this.updateOneById({
id: workspaceSetting.id!,
data: {
settings: data.settings,
},
props: {
isRoot: true,
},
});
}
}
}
export default new Service();

View File

@@ -0,0 +1,89 @@
import ObjectID from "../../Types/ObjectID";
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
import DatabaseService from "./DatabaseService";
import Model, {
SlackMiscData,
} from "Common/Models/DatabaseModels/WorkspaceUserAuthToken";
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
}
public async doesExist(data: {
projectId: ObjectID;
userId: ObjectID;
workspaceType: WorkspaceType;
}): Promise<boolean> {
return (
(
await this.countBy({
query: {
projectId: data.projectId,
userId: data.userId,
workspaceType: data.workspaceType,
},
skip: 0,
limit: 1,
props: {
isRoot: true,
},
})
).toNumber() > 0
);
}
public async refreshAuthToken(data: {
projectId: ObjectID;
userId: ObjectID;
workspaceType: WorkspaceType;
authToken: string;
workspaceUserId: string;
miscData: SlackMiscData;
}): Promise<void> {
let userAuth: Model | null = await this.findOneBy({
query: {
projectId: data.projectId,
userId: data.userId,
workspaceType: data.workspaceType,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
});
if (!userAuth) {
userAuth = new Model();
userAuth.projectId = data.projectId;
userAuth.userId = data.userId;
userAuth.authToken = data.authToken;
userAuth.workspaceType = data.workspaceType;
userAuth.workspaceUserId = data.workspaceUserId;
userAuth.miscData = data.miscData;
await this.create({
data: userAuth,
props: {
isRoot: true,
},
});
} else {
await this.updateOneById({
id: userAuth.id!,
data: {
authToken: data.authToken,
workspaceUserId: data.workspaceUserId,
miscData: data.miscData,
},
props: {
isRoot: true,
},
});
}
}
}
export default new Service();

View File

@@ -8,7 +8,7 @@ import { JSONObject } from "Common/Types/JSON";
import ComponentMetadata, { Port } from "Common/Types/Workflow/Component";
import ComponentID from "Common/Types/Workflow/ComponentID";
import SlackComponents from "Common/Types/Workflow/Components/Slack";
import SlackUtil from "../../../../Utils/Slack";
import SlackUtil from "../../../../Utils/Workspace/Slack/Slack";
export default class SendMessageToChannel extends ComponentCode {
public constructor() {
@@ -69,7 +69,7 @@ export default class SendMessageToChannel extends ComponentCode {
try {
// https://api.slack.com/messaging/webhooks#advanced_message_formatting
apiResult = await SlackUtil.sendMessageToChannel({
apiResult = await SlackUtil.sendMessageToChannelViaIncomingWebhook({
url: args["webhook-url"] as URL,
text: args["text"] as string,
});

View File

@@ -37,7 +37,8 @@ export interface OneUptimeRequest extends express.Request {
userAuthorization?: JSONWebTokenData;
tenantId?: ObjectID;
userGlobalAccessPermission?: UserGlobalAccessPermission;
userTenantAccessPermission?: Dictionary<UserTenantAccessPermission>; // tenantId <-> UserTenantAccessPermission
userTenantAccessPermission?: Dictionary<UserTenantAccessPermission>; // tenantId <-> UserTenantAccessPermission;
rawBody?: string; // raw body of the request before json parsing.
}
export interface OneUptimeResponse extends express.Response {

View File

@@ -16,11 +16,7 @@ import BadDataException from "Common/Types/Exception/BadDataException";
import BasicInfrastructureMetrics from "Common/Types/Infrastructure/BasicMetrics";
import ReturnResult from "Common/Types/IsolatedVM/ReturnResult";
import { JSONObject } from "Common/Types/JSON";
import {
CheckOn,
CriteriaFilter,
FilterCondition,
} from "Common/Types/Monitor/CriteriaFilter";
import { CheckOn, CriteriaFilter } from "Common/Types/Monitor/CriteriaFilter";
import IncomingMonitorRequest from "Common/Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
import MonitorCriteria from "Common/Types/Monitor/MonitorCriteria";
import MonitorCriteriaInstance from "Common/Types/Monitor/MonitorCriteriaInstance";
@@ -57,6 +53,7 @@ import MonitorMetricType from "../../../Types/Monitor/MonitorMetricType";
import TelemetryUtil from "../Telemetry/Telemetry";
import MetricMonitorCriteria from "./Criteria/MetricMonitorCriteria";
import MetricMonitorResponse from "../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
import FilterCondition from "../../../Types/Filter/FilterCondition";
export default class MonitorResourceUtil {
public static async monitorResource(

View File

@@ -22,6 +22,7 @@ import Exception from "Common/Types/Exception/Exception";
import { JSONArray, JSONObject } from "Common/Types/JSON";
import ListData from "Common/Types/ListData";
import PositiveNumber from "Common/Types/PositiveNumber";
import Route from "../../Types/API/Route";
export default class Response {
public static sendEmptySuccessResponse(
@@ -33,6 +34,14 @@ export default class Response {
oneUptimeResponse.status(200).send({} as EmptyResponse);
}
public static sendFileByPath(
_req: ExpressRequest,
res: ExpressResponse,
path: string,
): void {
res.sendFile(path);
}
public static sendCustomResponse(
_req: ExpressRequest,
res: ExpressResponse,
@@ -162,9 +171,9 @@ export default class Response {
public static redirect(
_req: ExpressRequest,
res: ExpressResponse,
url: URL,
to: URL | Route,
): void {
return res.redirect(url.toString());
return res.redirect(to.toString());
}
public static sendJsonArrayResponse(

View File

@@ -1,29 +0,0 @@
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
import { JSONObject } from "Common/Types/JSON";
import API from "Common/Utils/API";
export default class SlackUtil {
public static async sendMessageToChannel(data: {
url: URL;
text: string;
}): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> {
let apiResult: HTTPResponse<JSONObject> | HTTPErrorResponse | null = null;
// https://api.slack.com/messaging/webhooks#advanced_message_formatting
apiResult = await API.post(data.url, {
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `${data.text}`,
},
},
],
});
return apiResult;
}
}

View File

@@ -12,6 +12,7 @@ import Express, {
ExpressStatic,
ExpressUrlEncoded,
NextFunction,
OneUptimeRequest,
RequestHandler,
} from "./Express";
import logger from "./Logger";
@@ -82,7 +83,7 @@ app.set("view engine", "ejs");
* https://stackoverflow.com/questions/19917401/error-request-entity-too-large
*/
app.use((req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
app.use((req: OneUptimeRequest, res: ExpressResponse, next: NextFunction) => {
if (req.headers["content-encoding"] === "gzip") {
const buffers: any = [];

View File

@@ -0,0 +1,3 @@
import WorkspaceBase from "../WorkspaceBase";
export default class MicrosoftTeams extends WorkspaceBase {}

View File

@@ -0,0 +1,325 @@
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
import { JSONObject } from "Common/Types/JSON";
import API from "Common/Utils/API";
import WorkspaceMessagePayload, {
WorkspaceMessagePayloadButton,
WorkspacePayloadHeader,
WorkspacePayloadMarkdown,
} from "../../../../Types/Workspace/WorkspaceMessagePayload";
import logger from "../../Logger";
import Dictionary from "../../../../Types/Dictionary";
import BadRequestException from "../../../../Types/Exception/BadRequestException";
import WorkspaceBase, { WorkspaceChannel } from "../WorkspaceBase";
import WorkspaceType from "../../../../Types/Workspace/WorkspaceType";
export default class SlackUtil extends WorkspaceBase {
public static override async inviteUserToChannel(data: {
authToken: string;
channelName: string;
workspaceUserId: string;
}): Promise<void> {
const channelId: string = (
await this.getWorkspaceChannelFromChannelId({
authToken: data.authToken,
channelId: data.channelName,
})
).id;
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.post(
URL.fromString("https://slack.com/api/conversations.invite"),
{
channel: channelId,
users: data.workspaceUserId,
},
{
Authorization: `Bearer ${data.authToken}`,
},
);
if (response instanceof HTTPErrorResponse) {
throw response;
}
if ((response.jsonData as JSONObject)?.["ok"] !== true) {
throw new BadRequestException("Invalid response");
}
}
public static override async createChannelsIfDoesNotExist(data: {
authToken: string;
channelNames: Array<string>;
}): Promise<Array<WorkspaceChannel>> {
// check existing channels and only create if they dont exist.
const workspaceChannels: Array<WorkspaceChannel> = [];
const existingWorkspaceChannels: Dictionary<WorkspaceChannel> =
await this.getAllWorkspaceChannels({
authToken: data.authToken,
});
for (const channelName of data.channelNames) {
if (existingWorkspaceChannels[channelName]) {
logger.debug(`Channel ${channelName} already exists.`);
workspaceChannels.push(existingWorkspaceChannels[channelName]!);
continue;
}
const channel: WorkspaceChannel = await this.createChannel({
authToken: data.authToken,
channelName: channelName,
});
if (channel) {
workspaceChannels.push(channel);
}
}
return workspaceChannels;
}
public static override async getWorkspaceChannelFromChannelId(data: {
authToken: string;
channelId: string;
}): Promise<WorkspaceChannel> {
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.get<JSONObject>(
URL.fromString("https://slack.com/api/conversations.info"),
{
headers: {
Authorization: `Bearer ${data.authToken}`,
},
params: {
channel: data.channelId,
},
},
);
if (response instanceof HTTPErrorResponse) {
throw response;
}
if (
!((response.jsonData as JSONObject)?.["channel"] as JSONObject)?.["name"]
) {
throw new Error("Invalid response");
}
return {
name: ((response.jsonData as JSONObject)["channel"] as JSONObject)[
"name"
] as string,
id: data.channelId,
workspaceType: WorkspaceType.Slack,
};
}
public static override async getAllWorkspaceChannels(data: {
authToken: string;
}): Promise<Dictionary<WorkspaceChannel>> {
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.get<JSONObject>(
URL.fromString("https://slack.com/api/conversations.list"),
{
headers: {
Authorization: `Bearer ${data.authToken}`,
},
},
);
if (response instanceof HTTPErrorResponse) {
throw response;
}
const channels: Dictionary<WorkspaceChannel> = {};
for (const channel of (response.jsonData as JSONObject)[
"channels"
] as Array<JSONObject>) {
if (!channel["id"] || !channel["name"]) {
continue;
}
channels[channel["name"].toString()] = {
id: channel["id"] as string,
name: channel["name"] as string,
workspaceType: WorkspaceType.Slack,
};
}
return channels;
}
public static override async sendMessage(data: {
workspaceMessagePayload: WorkspaceMessagePayload;
authToken: string; // which auth token should we use to send.
}): Promise<void> {
logger.debug("Notify Slack");
logger.debug(data);
const blocks: Array<JSONObject> = this.getBlocksFromWorkspaceMessagePayload(
data.workspaceMessagePayload,
);
const existingWorkspaceChannels: Dictionary<WorkspaceChannel> =
await this.getAllWorkspaceChannels({
authToken: data.authToken,
});
const channelIdsToPostTo: Array<string> = [];
for (const channelName of data.workspaceMessagePayload.channelNames) {
// get channel ids from existingWorkspaceChannels. IF channel doesn't exist, create it if createChannelsIfItDoesNotExist is true.
let channel: WorkspaceChannel | null = null;
if (existingWorkspaceChannels[channelName]) {
channel = existingWorkspaceChannels[channelName]!;
}
if (channel) {
channelIdsToPostTo.push(channel.id);
} else {
logger.debug(`Channel ${channelName} does not exist.`);
}
}
for (const channelId of channelIdsToPostTo) {
try {
// try catch here to prevent failure of one channel to prevent posting to other channels.
await this.sendPayloadBlocksToChannel({
authToken: data.authToken,
channelId: channelId,
blocks: blocks,
});
} catch (e) {
logger.error(e);
}
}
}
public static override async sendPayloadBlocksToChannel(data: {
authToken: string;
channelId: string;
blocks: Array<JSONObject>;
}): Promise<void> {
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.post(
URL.fromString("https://slack.com/api/chat.postMessage"),
{
channel: data.channelId,
blocks: data.blocks,
},
{
Authorization: `Bearer ${data.authToken}`,
},
);
if (response instanceof HTTPErrorResponse) {
throw response;
}
if ((response.jsonData as JSONObject)?.["ok"] !== true) {
throw new BadRequestException("Invalid response");
}
}
public static override async createChannel(data: {
authToken: string;
channelName: string;
}): Promise<WorkspaceChannel> {
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.post(
URL.fromString("https://slack.com/api/conversations.create"),
{
name: data.channelName,
},
{
Authorization: `Bearer ${data.authToken}`,
},
);
if (response instanceof HTTPErrorResponse) {
throw response;
}
if (
!((response.jsonData as JSONObject)?.["channel"] as JSONObject)?.["id"] ||
!((response.jsonData as JSONObject)?.["channel"] as JSONObject)?.["name"]
) {
throw new Error("Invalid response");
}
return {
id: ((response.jsonData as JSONObject)["channel"] as JSONObject)[
"id"
] as string,
name: ((response.jsonData as JSONObject)["channel"] as JSONObject)[
"name"
] as string,
workspaceType: WorkspaceType.Slack,
};
}
public static override getHeaderBlock(data: {
payloadHeaderBlock: WorkspacePayloadHeader;
}): JSONObject {
return {
type: "header",
text: {
type: "plain_text",
text: data.payloadHeaderBlock.text,
},
};
}
public static override getMarkdownBlock(data: {
payloadMarkdownBlock: WorkspacePayloadMarkdown;
}): JSONObject {
return {
type: "section",
text: {
type: "mrkdwn",
text: data.payloadMarkdownBlock.text,
},
};
}
public static override getButtonBlock(data: {
payloadButtonBlock: WorkspaceMessagePayloadButton;
}): JSONObject {
return {
type: "button",
text: {
type: "plain_text",
text: data.payloadButtonBlock.title,
},
value: data.payloadButtonBlock.title,
action_id: data.payloadButtonBlock.title,
};
}
public static override async sendMessageToChannelViaIncomingWebhook(data: {
url: URL;
text: string;
}): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> {
let apiResult: HTTPResponse<JSONObject> | HTTPErrorResponse | null = null;
// https://api.slack.com/messaging/webhooks#advanced_message_formatting
apiResult = await API.post(data.url, {
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `${data.text}`,
},
},
],
});
return apiResult;
}
}

View File

@@ -0,0 +1,198 @@
{
"display_information": {
"name": "OneUptime",
"description": "The Complete Open-Source Observability Platform",
"background_color": "#000000",
"long_description": "OneUptime is a comprehensive solution for monitoring and managing your online services. Whether you need to check the availability of your website, dashboard, API, or any other online resource, OneUptime can alert your team when downtime happens and keep your customers informed with a status page. OneUptime also helps you handle incidents, set up on-call rotations, run tests, secure your services, analyze logs, track performance, and debug errors."
},
"features": {
"app_home": {
"home_tab_enabled": true,
"messages_tab_enabled": false,
"messages_tab_read_only_enabled": false
},
"bot_user": {
"display_name": "OneUptime",
"always_online": true
},
"shortcuts": [
{
"name": "Create Scheduled Event",
"type": "global",
"callback_id": "create-scheduled-maintenance",
"description": "Create a new scheduled event in OneUptime"
},
{
"name": "Create New Incident",
"type": "global",
"callback_id": "create-incident",
"description": "Creates a new incident in OneUptime"
}
],
"slash_commands": [
{
"command": "/oneuptime",
"url": "https://local.genosyn.com/api/slack/command",
"description": "OneUptime command",
"usage_hint": "incident, scheduled event.",
"should_escape": false
}
]
},
"oauth_config": {
"redirect_urls": [
"https://local.genosyn.com/api/slack/auth"
],
"scopes": {
"user": [
"users:read",
"users:read.email",
"bookmarks:read",
"workflows.templates:write",
"workflows.templates:read",
"users:write",
"users.profile:write",
"users.profile:read",
"usergroups:write",
"usergroups:read",
"team:read",
"team.preferences:read",
"team.billing:read",
"stars:write",
"stars:read",
"search:read",
"remote_files:share",
"reminders:read",
"remote_files:read",
"reminders:write",
"reactions:write",
"reactions:read",
"profile",
"pins:write",
"pins:read",
"openid",
"mpim:write.topic",
"mpim:write",
"mpim:read",
"mpim:history",
"links:write",
"links:read",
"links.embed:write",
"im:write.topic",
"im:write",
"im:read",
"im:history",
"identity.team",
"identity.email",
"identity.basic",
"identity.avatar",
"identify",
"groups:write.topic",
"groups:write.invites",
"groups:write",
"groups:read",
"groups:history",
"files:write",
"files:read",
"emoji:read",
"email",
"dnd:write",
"dnd:read",
"chat:write",
"channels:write.topic",
"channels:write.invites",
"channels:write",
"channels:read",
"channels:history",
"canvases:write",
"canvases:read",
"calls:write",
"calls:read",
"bookmarks:write"
],
"bot": [
"app_mentions:read",
"assistant:write",
"bookmarks:read",
"bookmarks:write",
"calls:write",
"calls:read",
"canvases:read",
"canvases:write",
"channels:history",
"channels:join",
"channels:manage",
"channels:read",
"channels:write.invites",
"channels:write.topic",
"chat:write",
"chat:write.customize",
"chat:write.public",
"commands",
"conversations.connect:manage",
"conversations.connect:read",
"files:read",
"conversations.connect:write",
"dnd:read",
"emoji:read",
"files:write",
"groups:history",
"groups:read",
"groups:write",
"groups:write.invites",
"groups:write.topic",
"im:read",
"im:history",
"incoming-webhook",
"links.embed:write",
"im:write.topic",
"im:write",
"links:read",
"links:write",
"metadata.message:read",
"mpim:history",
"pins:read",
"users:read.email",
"users:read",
"mpim:read",
"mpim:write",
"mpim:write.topic",
"pins:write",
"reactions:write",
"reactions:read",
"reminders:read",
"search:read.files",
"remote_files:write",
"remote_files:share",
"reminders:write",
"remote_files:read",
"search:read.im",
"team.billing:read",
"team.preferences:read",
"search:read.mpim",
"search:read.private",
"search:read.public",
"usergroups:read",
"usergroups:write",
"triggers:write",
"team:read",
"triggers:read",
"users.profile:read",
"users:write",
"workflows.templates:read",
"workflow.steps:execute",
"workflows.templates:write"
]
}
},
"settings": {
"interactivity": {
"is_enabled": true,
"request_url": "https://local.genosyn.com/api/slack/interactive",
"message_menu_options_url": "https://local.genosyn.com/api/slack/options-load"
},
"org_deploy_enabled": true,
"socket_mode_enabled": false,
"token_rotation_enabled": false
}
}

View File

@@ -0,0 +1,67 @@
{
"display_information": {
"name": "OneUptime",
"description": "The Complete Open-Source Observability Platform",
"background_color": "#000000",
"long_description": "OneUptime is a comprehensive solution for monitoring and managing your online services. Whether you need to check the availability of your website, dashboard, API, or any other online resource, OneUptime can alert your team when downtime happens and keep your customers informed with a status page. OneUptime also helps you handle incidents, set up on-call rotations, run tests, secure your services, analyze logs, track performance, and debug errors."
},
"features": {
"app_home": {
"home_tab_enabled": true,
"messages_tab_enabled": false,
"messages_tab_read_only_enabled": false
},
"bot_user": {
"display_name": "OneUptime",
"always_online": true
},
"shortcuts": [
{
"name": "Create Scheduled Event",
"type": "global",
"callback_id": "create-scheduled-maintenance",
"description": "Create a new scheduled event in OneUptime"
},
{
"name": "Create New Incident",
"type": "global",
"callback_id": "create-incident",
"description": "Creates a new incident in OneUptime"
}
],
"slash_commands": [
{
"command": "/oneuptime",
"url": "https://local.genosyn.com/api/slack/command",
"description": "OneUptime command",
"usage_hint": "incident, scheduled event.",
"should_escape": false
}
]
},
"oauth_config": {
"redirect_urls": [
"https://local.genosyn.com/api/slack/auth"
],
"scopes": {
"user": [
"identity.email",
"identity.basic",
"email"
],
"bot": [
"commands"
]
}
},
"settings": {
"interactivity": {
"is_enabled": true,
"request_url": "https://local.genosyn.com/api/slack/interactive",
"message_menu_options_url": "https://local.genosyn.com/api/slack/options-load"
},
"org_deploy_enabled": true,
"socket_mode_enabled": false,
"token_rotation_enabled": false
}
}

View File

@@ -0,0 +1,23 @@
import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
import WorkspaceBase from "./WorkspaceBase";
import SlackWorkspace from "./Slack/Slack";
import MicrosoftTeamsWorkspace from "./MicrosoftTeams/MicrosoftTeams";
import BadDataException from "../../../Types/Exception/BadDataException";
export default class WorkspaceUtil {
public static getWorkspaceTypeUtil(
workspaceType: WorkspaceType,
): typeof WorkspaceBase {
if (workspaceType === WorkspaceType.Slack) {
return SlackWorkspace;
}
if (workspaceType === WorkspaceType.MicrosoftTeams) {
return MicrosoftTeamsWorkspace;
}
throw new BadDataException(
`Workspace type ${workspaceType} is not supported`,
);
}
}

View File

@@ -0,0 +1,169 @@
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
import HTTPResponse from "../../../Types/API/HTTPResponse";
import Dictionary from "../../../Types/Dictionary";
import NotImplementedException from "../../../Types/Exception/NotImplementedException";
import { JSONObject } from "../../../Types/JSON";
import WorkspaceChannelInvitationPayload from "../../../Types/Workspace/WorkspaceChannelInvitationPayload";
import WorkspaceMessagePayload, {
WorkspaceMessagePayloadButton,
WorkspacePayloadButtons,
WorkspacePayloadHeader,
WorkspacePayloadMarkdown,
} from "../../../Types/Workspace/WorkspaceMessagePayload";
import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
import logger from "../Logger";
import URL from "Common/Types/API/URL";
export interface WorkspaceChannel {
id: string;
name: string;
workspaceType: WorkspaceType;
}
export default class WorkspaceBase {
public static async sendPayloadBlocksToChannel(_data: {
authToken: string;
channelId: string;
blocks: Array<JSONObject>;
}): Promise<void> {
throw new NotImplementedException();
}
public static async inviteUsersToChannels(data: {
authToken: string;
workspaceChannelInvitationPayload: WorkspaceChannelInvitationPayload;
}): Promise<void> {
for (const channelName of data.workspaceChannelInvitationPayload
.channelNames) {
await this.inviteUsersToChannel({
authToken: data.authToken,
channelName: channelName,
workspaceUserIds:
data.workspaceChannelInvitationPayload.workspaceUserIds,
});
}
}
public static async inviteUsersToChannel(data: {
authToken: string;
channelName: string;
workspaceUserIds: Array<string>;
}): Promise<void> {
for (const userId of data.workspaceUserIds) {
await this.inviteUserToChannel({
authToken: data.authToken,
channelName: data.channelName,
workspaceUserId: userId,
});
}
}
public static async inviteUserToChannel(_data: {
authToken: string;
channelName: string;
workspaceUserId: string;
}): Promise<void> {
throw new NotImplementedException();
}
public static async createChannelsIfDoesNotExist(_data: {
authToken: string;
channelNames: Array<string>;
}): Promise<Array<WorkspaceChannel>> {
throw new NotImplementedException();
}
public static async getWorkspaceChannelFromChannelId(_data: {
authToken: string;
channelId: string;
}): Promise<WorkspaceChannel> {
throw new NotImplementedException();
}
public static async sendMessage(_data: {
workspaceMessagePayload: WorkspaceMessagePayload;
authToken: string; // which auth token should we use to send.
}): Promise<void> {
throw new NotImplementedException();
}
public static async getAllWorkspaceChannels(_data: {
authToken: string;
}): Promise<Dictionary<WorkspaceChannel>> {
throw new NotImplementedException();
}
public static async createChannel(_data: {
authToken: string;
channelName: string;
}): Promise<WorkspaceChannel> {
throw new NotImplementedException();
}
public static getHeaderBlock(_data: {
payloadHeaderBlock: WorkspacePayloadHeader;
}): JSONObject {
throw new NotImplementedException();
}
public static getMarkdownBlock(_data: {
payloadMarkdownBlock: WorkspacePayloadMarkdown;
}): JSONObject {
throw new NotImplementedException();
}
public static getButtonBlock(_data: {
payloadButtonBlock: WorkspaceMessagePayloadButton;
}): JSONObject {
throw new NotImplementedException();
}
public static getBlocksFromWorkspaceMessagePayload(
data: WorkspaceMessagePayload,
): Array<JSONObject> {
const blocks: Array<JSONObject> = [];
const buttons: Array<JSONObject> = [];
for (const block of data.messageBlocks) {
switch (block._type) {
case "WorkspacePayloadHeader":
blocks.push(
this.getHeaderBlock({
payloadHeaderBlock: block as WorkspacePayloadHeader,
}),
);
break;
case "WorkspacePayloadMarkdown":
blocks.push(
this.getMarkdownBlock({
payloadMarkdownBlock: block as WorkspacePayloadMarkdown,
}),
);
break;
case "WorkspacePayloadButtons":
for (const button of (block as WorkspacePayloadButtons).buttons) {
buttons.push(
this.getButtonBlock({
payloadButtonBlock: button,
}),
);
}
blocks.push({
type: "actions",
elements: buttons,
});
break;
default:
logger.error("Unknown block type: " + block._type);
break;
}
}
return blocks;
}
public static async sendMessageToChannelViaIncomingWebhook(_data: {
url: URL;
text: string;
}): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> {
throw new NotImplementedException();
}
}

View File

@@ -1,3 +1,4 @@
import { Dictionary } from "lodash";
import DatabaseProperty from "../Database/DatabaseProperty";
import BadDataException from "../Exception/BadDataException";
import { JSONObject, ObjectType } from "../JSON";
@@ -98,4 +99,21 @@ export default class Route extends DatabaseProperty {
return null;
}
public addQueryParams(queryParams: Dictionary<string>): Route {
// make sure route ends with "?" if it doesn't have any query params
if (!this.route.includes("?")) {
this.route += "?";
}
for (const key in queryParams) {
this.route += `${key}=${queryParams[key]}&`;
}
//remove last "&" from route
this.route = this.route.substring(0, this.route.length - 1);
return this;
}
}

View File

@@ -26,7 +26,7 @@ export default class Domain extends DatabaseProperty {
"|",
);
const secondTLDs: Array<string> =
"ac|academy|accountant|accountants|actor|adult|aero|ag|agency|ai|airforce|am|amsterdam|apartments|app|archi|army|art|asia|associates|at|attorney|au|auction|auto|autos|baby|band|bar|barcelona|bargains|basketball|bayern|be|beauty|beer|berlin|best|bet|bid|bike|bingo|bio|biz|biz.pl|black|blog|blue|boats|boston|boutique|broker|build|builders|business|buzz|bz|ca|cab|cafe|camera|camp|capital|car|cards|care|careers|cars|casa|cash|casino|catering|cc|center|ceo|ch|charity|chat|cheap|church|city|cl|claims|cleaning|clinic|clothing|cloud|club|cn|co|co.in|co.jp|co.kr|co.nz|co.uk|co.za|coach|codes|coffee|college|com|com.ag|com.au|com.br|com.bz|com.cn|com.co|com.es|com.ky|com.mx|com.pe|com.ph|com.pl|com.ru|com.tw|community|company|computer|condos|construction|consulting|contact|contractors|cooking|cool|country|coupons|courses|credit|creditcard|cricket|cruises|cymru|cz|dance|date|dating|de|deals|degree|delivery|democrat|dental|dentist|design|dev|diamonds|digital|direct|directory|discount|dk|doctor|dog|domains|download|earth|education|email|energy|engineer|engineering|enterprises|equipment|es|estate|eu|events|exchange|expert|exposed|express|fail|faith|family|fan|fans|farm|fashion|film|finance|financial|firm.in|fish|fishing|fit|fitness|flights|florist|fm|football|forsale|foundation|fr|fun|fund|furniture|futbol|fyi|gallery|games|garden|gay|gen.in|gg|gifts|gives|giving|glass|global|gmbh|gold|golf|graphics|gratis|green|gripe|group|gs|guide|guru|hair|haus|health|healthcare|hockey|holdings|holiday|homes|horse|hospital|host|house|idv.tw|immo|immobilien|in|inc|ind.in|industries|info|info.pl|ink|institute|insure|international|investments|io|irish|ist|istanbul|it|jetzt|jewelry|jobs|jp|kaufen|kids|kim|kitchen|kiwi|kr|ky|la|land|lat|law|lawyer|lease|legal|lgbt|life|lighting|limited|limo|live|llc|llp|loan|loans|london|love|ltd|ltda|luxury|maison|makeup|management|market|marketing|mba|me|me.uk|media|melbourne|memorial|men|menu|miami|mobi|moda|moe|money|monster|mortgage|motorcycles|movie|ms|music|mx|nagoya|name|navy|ne.kr|net|net.ag|net.au|net.br|net.bz|net.cn|net.co|net.in|net.ky|net.nz|net.pe|net.ph|net.pl|net.ru|network|news|ninja|nl|no|nom.co|nom.es|nom.pe|nrw|nyc|okinawa|one|onl|online|org|org.ag|org.au|org.cn|org.es|org.in|org.ky|org.nz|org.pe|org.ph|org.pl|org.ru|org.uk|organic|page|paris|partners|parts|party|pe|pet|ph|photography|photos|pictures|pink|pizza|pl|place|plumbing|plus|poker|porn|press|pro|productions|promo|properties|protection|pub|pw|quebec|quest|racing|re.kr|realestate|recipes|red|rehab|reise|reisen|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rich|rip|rocks|rodeo|rugby|run|ryukyu|sale|salon|sarl|school|schule|science|se|security|services|sex|sg|sh|shiksha|shoes|shop|shopping|show|singles|site|ski|skin|soccer|social|software|solar|solutions|space|storage|store|stream|studio|study|style|supplies|supply|support|surf|surgery|sydney|systems|tax|taxi|team|tech|technology|tel|tennis|theater|theatre|tickets|tienda|tips|tires|today|tokyo|tools|tours|town|toys|trade|trading|training|travel|tube|tv|tw|uk|university|uno|us|vacations|vc|vegas|ventures|vet|viajes|video|villas|vin|vip|vision|vodka|vote|voto|voyage|wales|watch|web|webcam|website|wedding|wiki|win|wine|work|works|world|ws|wtf|xxx|xyz|yachts|yoga|yokohama|zone|移动|dev|com|edu|gov|net|mil|org|nom|sch|sbs|caa|res|off|gob|int|tur|ip6|uri|urn|asn|act|nsw|qld|tas|vic|pro|biz|adm|adv|agr|arq|art|ato|bio|bmd|cim|cng|cnt|ecn|eco|emp|eng|esp|etc|eti|far|fnd|fot|fst|g12|ggf|imb|ind|inf|jor|jus|leg|lel|mat|med|mus|not|ntr|odo|ppg|psc|psi|qsl|rec|slg|srv|teo|tmp|trd|vet|zlg|web|ltd|sld|pol|fin|k12|lib|pri|aip|fie|eun|sci|prd|cci|pvt|mod|idv|rel|sex|gen|nic|abr|bas|cal|cam|emr|fvg|laz|lig|lom|mar|mol|pmn|pug|sar|sic|taa|tos|umb|vao|vda|ven|mie|北海道|和歌山|神奈川|鹿児島|ass|rep|tra|per|ngo|soc|grp|plc|its|air|and|bus|can|ddr|jfk|mad|nrw|nyc|ski|spy|tcm|ulm|usa|war|fhs|vgs|dep|eid|fet|fla|flå|gol|hof|hol|sel|vik|cri|iwi|ing|abo|fam|gok|gon|gop|gos|aid|atm|gsm|sos|elk|waw|est|aca|bar|cpa|jur|law|sec|plo|www|bir|cbg|jar|khv|msk|nov|nsk|ptz|rnd|spb|stv|tom|tsk|udm|vrn|cmw|kms|nkz|snz|pub|fhv|red|ens|nat|rns|rnu|bbs|tel|bel|kep|nhs|dni|fed|isa|nsn|gub|e12|tec|орг|обр|упр|alt|nis|jpn|mex|ath|iki|nid|gda|inc|za|ovh|lol|africa".split(
"ac|academy|accountant|accountants|actor|adult|aero|ag|agency|ai|airforce|am|amsterdam|apartments|app|archi|army|art|asia|associates|at|attorney|au|auction|auto|autos|baby|band|bar|barcelona|bargains|basketball|bayern|be|beauty|beer|berlin|best|bet|bid|bike|bingo|bio|biz|biz.pl|black|blog|blue|boats|boston|boutique|broker|build|builders|business|buzz|bz|ca|cab|cafe|camera|camp|capital|car|cards|care|careers|cars|casa|cash|casino|catering|cc|center|ceo|ch|charity|chat|cheap|church|city|cl|claims|cleaning|clinic|clothing|cloud|club|cn|co|co.in|co.jp|co.kr|co.nz|co.uk|co.za|coach|codes|coffee|college|com|com.ag|com.au|com.br|com.bz|com.cn|com.co|com.es|com.ky|com.mx|com.pe|com.ph|com.pl|com.ru|com.tw|community|company|computer|condos|construction|consulting|contact|contractors|cooking|cool|country|coupons|courses|credit|creditcard|cricket|cruises|cymru|cz|dance|date|dating|de|deals|degree|delivery|democrat|dental|dentist|design|dev|diamonds|digital|direct|directory|discount|dk|doctor|dog|domains|download|earth|education|email|energy|engineer|engineering|enterprises|equipment|es|estate|eu|events|exchange|expert|exposed|express|fail|faith|family|fan|fans|farm|fashion|film|finance|financial|firm.in|fish|fishing|fit|fitness|flights|florist|fm|football|forsale|foundation|fr|fun|fund|furniture|futbol|fyi|gallery|games|garden|gay|gen.in|gg|gifts|gives|giving|glass|global|gmbh|gold|golf|graphics|gratis|green|gripe|group|gs|guide|guru|hair|haus|health|healthcare|hockey|holdings|holiday|homes|horse|hospital|host|house|idv.tw|immo|immobilien|in|inc|ind.in|industries|info|info.pl|ink|institute|insure|international|investments|io|irish|ist|istanbul|it|jetzt|jewelry|jobs|jp|kaufen|kids|kim|kitchen|kiwi|kr|ky|la|land|lat|law|lawyer|lease|legal|lgbt|life|lighting|limited|limo|live|llc|llp|loan|loans|london|love|ltd|ltda|luxury|maison|makeup|management|market|marketing|mba|me|me.uk|media|melbourne|memorial|men|menu|miami|mobi|moda|moe|money|monster|mortgage|motorcycles|movie|ms|music|mx|nagoya|name|navy|ne.kr|net|net.ag|net.au|net.br|net.bz|net.cn|net.co|net.in|net.ky|net.nz|net.pe|net.ph|net.pl|net.ru|network|news|ninja|nl|no|nom.co|nom.es|nom.pe|nrw|nyc|okinawa|one|onl|online|org|org.ag|org.au|org.cn|org.es|org.in|org.ky|org.nz|org.pe|org.ph|org.pl|org.ru|org.uk|organic|page|paris|partners|parts|party|pe|pet|ph|photography|photos|pictures|pink|pizza|pl|place|plumbing|plus|poker|porn|press|pro|productions|promo|properties|protection|pub|pw|quebec|quest|racing|re.kr|realestate|recipes|red|rehab|reise|reisen|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rich|rip|rocks|rodeo|rugby|run|ryukyu|sale|salon|sarl|school|schule|science|se|security|services|sex|sg|sh|shiksha|shoes|shop|shopping|show|singles|site|ski|skin|soccer|social|software|solar|solutions|space|storage|store|stream|studio|study|style|supplies|supply|support|surf|surgery|sydney|systems|tax|taxi|team|tech|technology|tel|tennis|theater|theatre|tickets|tienda|tips|tires|today|tokyo|tools|tours|town|toys|trade|trading|training|travel|tube|tv|tw|uk|university|uno|us|vacations|vc|vegas|ventures|vet|viajes|video|villas|vin|vip|vision|vodka|vote|voto|voyage|wales|watch|web|webcam|website|wedding|wiki|win|wine|work|works|world|ws|wtf|xxx|xyz|yachts|yoga|yokohama|zone|移动|dev|com|edu|gov|net|mil|org|nom|sch|sbs|caa|res|off|gob|int|tur|ip6|uri|urn|asn|act|nsw|qld|tas|vic|pro|biz|adm|adv|agr|arq|art|ato|bio|bmd|cim|cng|cnt|ecn|eco|emp|eng|esp|etc|eti|far|fnd|fot|fst|g12|ggf|imb|ind|inf|jor|jus|leg|lel|mat|med|mus|not|ntr|odo|ppg|psc|psi|qsl|rec|slg|srv|teo|tmp|trd|vet|zlg|web|ltd|sld|pol|fin|k12|lib|pri|aip|fie|eun|sci|prd|cci|pvt|mod|idv|rel|sex|gen|nic|abr|bas|cal|cam|emr|fvg|laz|lig|lom|mar|mol|pmn|pug|sar|sic|taa|tos|umb|vao|vda|ven|mie|北海道|和歌山|神奈川|鹿児島|ass|rep|tra|per|ngo|soc|grp|plc|its|air|and|bus|can|ddr|jfk|mad|nrw|nyc|ski|spy|tcm|ulm|usa|war|fhs|vgs|dep|eid|fet|fla|flå|gol|hof|hol|sel|vik|cri|iwi|ing|abo|fam|gok|gon|gop|gos|aid|atm|gsm|sos|elk|waw|est|aca|bar|cpa|jur|law|sec|plo|www|bir|cbg|jar|khv|msk|nov|nsk|ptz|rnd|spb|stv|tom|tsk|udm|vrn|cmw|kms|nkz|snz|pub|fhv|red|ens|nat|rns|rnu|bbs|tel|bel|kep|nhs|dni|fed|isa|nsn|gub|e12|tec|орг|обр|упр|alt|nis|jpn|mex|ath|iki|nid|gda|inc|za|ovh|lol|africa|top".split(
"|",
);

View File

@@ -186,6 +186,10 @@ export default class Recurring extends DatabaseProperty {
return arrayToReturn;
}
public override toString(): string {
return `${this.intervalCount} ${this.intervalType}`;
}
protected static override toDatabase(
value: Recurring | Array<Recurring> | FindOperator<Recurring>,
): JSONObject | Array<JSONObject> | null {

View File

@@ -1,6 +1,6 @@
enum FilterCondition {
And = "And",
Or = "Or",
All = "All",
Any = "Any",
}
export default FilterCondition;

View File

@@ -110,12 +110,38 @@ export enum FilterType {
IsNotExecuting = "Is Not Executing",
}
export enum FilterCondition {
All = "All",
Any = "Any",
}
export class CriteriaFilterUtil {
public static hasValueField(data: {
checkOn: CheckOn;
filterType: FilterType | undefined;
}): boolean {
const { checkOn } = data;
if (checkOn === CheckOn.IsOnline) {
return false;
}
if (
checkOn === CheckOn.IsValidCertificate ||
checkOn === CheckOn.IsSelfSignedCertificate ||
checkOn === CheckOn.IsExpiredCertificate ||
checkOn === CheckOn.IsNotAValidCertificate
) {
return false;
}
if (
FilterType.IsEmpty === data.filterType ||
FilterType.IsNotEmpty === data.filterType ||
FilterType.True === data.filterType ||
FilterType.False === data.filterType
) {
return false;
}
return true;
}
public static getEvaluateOverTimeTypeByCriteriaFilter(
criteriaFilter: CriteriaFilter | undefined,
): Array<EvaluateOverTimeType> {

View File

@@ -1,5 +1,6 @@
import DatabaseProperty from "../Database/DatabaseProperty";
import BadDataException from "../Exception/BadDataException";
import FilterCondition from "../Filter/FilterCondition";
import { JSONObject, ObjectType } from "../JSON";
import JSONFunctions from "../JSONFunctions";
import ObjectID from "../ObjectID";
@@ -8,9 +9,9 @@ import { CriteriaAlert } from "./CriteriaAlert";
import {
CheckOn,
CriteriaFilter,
FilterCondition,
FilterType,
EvaluateOverTimeType,
CriteriaFilterUtil,
} from "./CriteriaFilter";
import { CriteriaIncident } from "./CriteriaIncident";
import MonitorType from "./MonitorType";
@@ -699,19 +700,19 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
monitorType: MonitorType,
): string | null {
if (!value.data) {
return "Monitor Step is required";
return `Monitor Step is required.`;
}
if (value.data.filters.length === 0) {
return "Monitor Criteria filter is required";
return `Filter is required for criteria "${value.data.name}"`;
}
if (!value.data.name) {
return "Monitor Criteria name is required";
return `Name is required for criteria "${value.data.name}"`;
}
if (!value.data.description) {
return "Monitor Criteria description is required";
return `Description is required for criteria "${value.data.name}"`;
}
for (const incident of value.data.incidents) {
@@ -720,21 +721,39 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
}
if (!incident.title) {
return "Monitor Criteria incident title is required";
return `Incident title is required for criteria "${value.data.name}"`;
}
if (!incident.description) {
return "Monitor Criteria incident description is required";
return `Incident description is required for criteria "${value.data.name}"`;
}
if (!incident.incidentSeverityId) {
return "Monitor Criteria incident severity is required";
return `Incident severity is required for criteria "${value.data.name}"`;
}
}
for (const alert of value.data.alerts) {
if (!alert) {
continue;
}
if (!alert.title) {
return `Alert title is required for criteria "${value.data.name}"`;
}
if (!alert.description) {
return `Alert description is required for criteria "${value.data.name}"`;
}
if (!alert.alertSeverityId) {
return `Alert severity is required for criteria "${value.data.name}"`;
}
}
for (const filter of value.data.filters) {
if (!filter.checkOn) {
return "Monitor Criteria filter check on is required";
return `Filter Type is required for criteria "${value.data.name}"`;
}
if (
@@ -742,7 +761,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
filter.checkOn !== CheckOn.IsOnline &&
filter.checkOn !== CheckOn.ResponseTime
) {
return "Ping Monitor cannot have filter criteria: " + filter.checkOn;
return "Ping Monitor cannot have filter type: " + filter.checkOn;
}
if (
@@ -751,6 +770,17 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
) {
return "Disk Path is required for Disk Usage Percent";
}
if (
CriteriaFilterUtil.hasValueField({
checkOn: filter.checkOn,
filterType: filter.filterType,
})
) {
if (!filter.value && filter.value !== 0) {
return `Value is required for criteria "${value.data.name}" on filter type: ${filter.checkOn}`;
}
}
}
return null;

View File

@@ -252,7 +252,7 @@ export default class MonitorStep extends DatabaseProperty {
}
if (
!MonitorCriteria.getValidationError(
MonitorCriteria.getValidationError(
value.data.monitorCriteria,
monitorType,
)

View File

@@ -607,6 +607,11 @@ enum Permission {
DeleteTableView = "DeleteTableView",
EditTableView = "EditTableView",
ReadTableView = "ReadTableView",
CreateWorkspaceNotificationRule = "CreateWorkspaceNotificationRule",
DeleteWorkspaceNotificationRule = "DeleteWorkspaceNotificationRule",
EditWorkspaceNotificationRule = "EditWorkspaceNotificationRule",
ReadWorkspaceNotificationRule = "ReadWorkspaceNotificationRule",
}
export class PermissionHelper {
@@ -1039,6 +1044,35 @@ export class PermissionHelper {
isAccessControlPermission: false,
},
{
permission: Permission.CreateWorkspaceNotificationRule,
title: "Create Workspace Notification Rule",
description: "This permission can create alert states this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.DeleteWorkspaceNotificationRule,
title: "Delete Workspace Notification Rule",
description: "This permission can delete alert states of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.EditWorkspaceNotificationRule,
title: "Edit Workspace Notification Rule",
description: "This permission can edit alert states of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.ReadWorkspaceNotificationRule,
title: "Read Workspace Notification Rule",
description: "This permission can read alert states of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CreateIncidentStateTimeline,
title: "Create Incident State Timeline",

View File

@@ -0,0 +1,12 @@
import FilterCondition from "../../Filter/FilterCondition";
import NotificationRuleCondition from "./NotificationRuleCondition";
export default interface BaseNotificationRule {
_type: string;
// filters for notification rule
filterCondition: FilterCondition; // and OR or. Default is AND
filters: Array<NotificationRuleCondition>; // if this array is empty then it will be considered as all filters are matched.
shouldPostToExistingChannel: boolean;
existingChannelNames: string; // seperate by comma
}

View File

@@ -0,0 +1,14 @@
import ObjectID from "../../ObjectID";
import BaseNotificationRule from "./BaseNotificationRule";
export default interface CreateChannelNotificationRule
extends BaseNotificationRule {
_type: string;
// if filters match then do:
shouldCreateNewChannel: boolean;
inviteTeamsToNewChannel: Array<ObjectID>;
inviteUsersToNewChannel: Array<ObjectID>;
shouldInviteOwnersToNewChannel: boolean;
newChannelTemplateName: string;
}

View File

@@ -0,0 +1,8 @@
enum NotificationRuleEventType {
Incident = "Incident",
MonitorStatus = "Monitor Status",
Alert = "Alert",
ScheduledMaintenance = "Scheduled Maintenance",
}
export default NotificationRuleEventType;

View File

@@ -0,0 +1,358 @@
import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
import AlertState from "../../../Models/DatabaseModels/AlertState";
import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
import IncidentState from "../../../Models/DatabaseModels/IncidentState";
import Label from "../../../Models/DatabaseModels/Label";
import Monitor from "../../../Models/DatabaseModels/Monitor";
import MonitorStatus from "../../../Models/DatabaseModels/MonitorStatus";
import ScheduledMaintenanceState from "../../../Models/DatabaseModels/ScheduledMaintenanceState";
import { DropdownOption } from "../../../UI/Components/Dropdown/Dropdown";
import WorkspaceType from "../WorkspaceType";
import NotificationRuleEventType from "./EventType";
import IncidentNotificationRule from "./NotificationRuleTypes/IncidentNotificationRule";
export enum NotificationRuleConditionCheckOn {
MonitorName = "Monitor Name",
IncidentTitle = "Incident Title",
IncidentDescription = "Incident Description",
IncidentSeverity = "Incident Severity",
IncidentState = "Incident State",
MonitorType = "Monitor Type",
MonitorStatus = "Monitor Status",
AlertTitle = "Alert Title",
AlertDescription = "Alert Description",
AlertSeverity = "Alert Severity",
AlertState = "Alert State",
ScheduledMaintenanceTitle = "Scheduled Maintenance Title",
ScheduledMaintenanceDescription = "Scheduled Maintenance Description",
ScheduledMaintenanceState = "Scheduled Maintenance State",
IncidentLabels = "Incident Labels",
AlertLabels = "Alert Labels",
MonitorLabels = "Monitor Labels",
ScheduledMaintenanceLabels = "Scheduled Maintenance Labels",
Monitors = "Monitors", // like monitor contains in the incident or scheduled event.
}
export enum ConditionType {
EqualTo = "Equal To",
NotEqualTo = "Not Equal To",
GreaterThan = "Greater Than",
LessThan = "Less Than",
GreaterThanOrEqualTo = "Greater Than Or Equal To",
LessThanOrEqualTo = "Less Than Or Equal To",
ContainsAny = "Contains Any",
NotContains = "Not Contains",
StartsWith = "Starts With",
EndsWith = "Ends With",
IsEmpty = "Is Empty",
IsNotEmpty = "Is Not Empty",
True = "True",
False = "False",
ContainsAll = "Contains All",
}
export default interface NotificationRuleCondition {
checkOn: NotificationRuleConditionCheckOn;
conditionType: ConditionType | undefined;
value: string | Array<string> | undefined;
}
export class NotificationRuleConditionUtil {
public static getValidationError(data: {
notificationRule: IncidentNotificationRule;
eventType: NotificationRuleEventType;
workspaceType: WorkspaceType;
}): string | null {
const { notificationRule, eventType, workspaceType } = data;
for (const condition of notificationRule.filters) {
if (!condition.checkOn) {
return "Check On is required";
}
if (!condition.conditionType) {
return `Filter Condition is required for ${condition.checkOn}`;
}
if (!condition.value) {
return `Value is required for ${condition.checkOn}`;
}
if (Array.isArray(condition.value) && condition.value.length === 0) {
return `Value is required for ${condition.checkOn}`;
}
}
if (
eventType === NotificationRuleEventType.Incident ||
eventType === NotificationRuleEventType.Alert ||
eventType === NotificationRuleEventType.ScheduledMaintenance
) {
// either create slack channel or select existing one should be active.
if (
!notificationRule.shouldCreateNewChannel &&
!notificationRule.shouldPostToExistingChannel
) {
return (
"Please select either create slack channel or post to existing " +
workspaceType +
" channel"
);
}
if (notificationRule.shouldPostToExistingChannel) {
if (!notificationRule.existingChannelNames?.trim()) {
return "Existing " + workspaceType + " channel name is required";
}
}
if (notificationRule.shouldCreateNewChannel) {
if (!notificationRule.newChannelTemplateName?.trim()) {
return "New " + workspaceType + " channel name is required";
}
}
}
return null;
}
public static hasValueField(data: {
checkOn: NotificationRuleConditionCheckOn;
conditionType: ConditionType | undefined;
}): boolean {
switch (data.conditionType) {
case ConditionType.IsEmpty:
case ConditionType.IsNotEmpty:
case ConditionType.True:
case ConditionType.False:
return false;
default:
return true;
}
}
public static isDropdownValueField(data: {
checkOn: NotificationRuleConditionCheckOn | undefined;
conditionType: ConditionType | undefined;
}): boolean {
// incident state, alert state, monitor status, scheduled maintenance state, severit, labels, monitors are all dropdowns.
if (!data.checkOn || !data.conditionType) {
return false;
}
switch (data.checkOn) {
case NotificationRuleConditionCheckOn.MonitorType:
case NotificationRuleConditionCheckOn.IncidentState:
case NotificationRuleConditionCheckOn.AlertState:
case NotificationRuleConditionCheckOn.MonitorStatus:
case NotificationRuleConditionCheckOn.ScheduledMaintenanceState:
case NotificationRuleConditionCheckOn.IncidentSeverity:
case NotificationRuleConditionCheckOn.AlertSeverity:
case NotificationRuleConditionCheckOn.MonitorLabels:
case NotificationRuleConditionCheckOn.IncidentLabels:
case NotificationRuleConditionCheckOn.AlertLabels:
case NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels:
case NotificationRuleConditionCheckOn.Monitors:
return true;
default:
return false;
}
}
public static getDropdownOptionsByCheckOn(data: {
alertSeverities: Array<AlertSeverity>;
alertStates: Array<AlertState>;
incidentSeverities: Array<IncidentSeverity>;
monitorStatus: Array<MonitorStatus>;
incidentStates: Array<IncidentState>;
scheduledMaintenanceStates: Array<ScheduledMaintenanceState>;
labels: Array<Label>;
monitors: Array<Monitor>;
checkOn: NotificationRuleConditionCheckOn;
}): Array<DropdownOption> {
if (data.checkOn === NotificationRuleConditionCheckOn.AlertSeverity) {
return data.alertSeverities.map((severity: AlertSeverity) => {
return {
value: severity.id!.toString(),
label: severity.name || "",
};
});
}
if (data.checkOn === NotificationRuleConditionCheckOn.IncidentSeverity) {
return data.incidentSeverities.map((severity: IncidentSeverity) => {
return {
value: severity.id!.toString(),
label: severity.name || "",
};
});
}
if (data.checkOn === NotificationRuleConditionCheckOn.MonitorStatus) {
return data.monitorStatus.map((status: MonitorStatus) => {
return {
value: status.id!.toString(),
label: status.name || "",
};
});
}
if (data.checkOn === NotificationRuleConditionCheckOn.IncidentState) {
return data.incidentStates.map((state: IncidentState) => {
return {
value: state.id!.toString(),
label: state.name || "",
};
});
}
if (
data.checkOn ===
NotificationRuleConditionCheckOn.ScheduledMaintenanceState
) {
return data.scheduledMaintenanceStates.map(
(state: ScheduledMaintenanceState) => {
return {
value: state.id!.toString(),
label: state.name || "",
};
},
);
}
if (
data.checkOn === NotificationRuleConditionCheckOn.MonitorLabels ||
data.checkOn === NotificationRuleConditionCheckOn.IncidentLabels ||
data.checkOn === NotificationRuleConditionCheckOn.AlertLabels ||
data.checkOn ===
NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels
) {
return data.labels.map((label: Label) => {
return {
value: label.id!.toString(),
label: label.name || "",
};
});
}
if (data.checkOn === NotificationRuleConditionCheckOn.Monitors) {
return data.monitors.map((monitor: Monitor) => {
return {
value: monitor.id!.toString(),
label: monitor.name || "",
};
});
}
// alert states
if (data.checkOn === NotificationRuleConditionCheckOn.AlertState) {
return data.alertStates.map((state: AlertState) => {
return {
value: state.id!.toString(),
label: state.name || "",
};
});
}
return [];
}
public static getCheckOnByEventType(
eventType: NotificationRuleEventType,
): Array<NotificationRuleConditionCheckOn> {
switch (eventType) {
case NotificationRuleEventType.Incident:
return [
NotificationRuleConditionCheckOn.IncidentTitle,
NotificationRuleConditionCheckOn.IncidentDescription,
NotificationRuleConditionCheckOn.IncidentSeverity,
NotificationRuleConditionCheckOn.IncidentState,
NotificationRuleConditionCheckOn.IncidentLabels,
NotificationRuleConditionCheckOn.MonitorLabels,
NotificationRuleConditionCheckOn.Monitors,
];
case NotificationRuleEventType.Alert:
return [
NotificationRuleConditionCheckOn.AlertTitle,
NotificationRuleConditionCheckOn.AlertDescription,
NotificationRuleConditionCheckOn.AlertSeverity,
NotificationRuleConditionCheckOn.AlertState,
NotificationRuleConditionCheckOn.AlertLabels,
NotificationRuleConditionCheckOn.MonitorLabels,
NotificationRuleConditionCheckOn.Monitors,
];
case NotificationRuleEventType.MonitorStatus:
return [
NotificationRuleConditionCheckOn.MonitorName,
NotificationRuleConditionCheckOn.MonitorStatus,
NotificationRuleConditionCheckOn.MonitorType,
NotificationRuleConditionCheckOn.Monitors,
NotificationRuleConditionCheckOn.MonitorLabels,
];
case NotificationRuleEventType.ScheduledMaintenance:
return [
NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle,
NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription,
NotificationRuleConditionCheckOn.ScheduledMaintenanceState,
NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels,
NotificationRuleConditionCheckOn.MonitorLabels,
NotificationRuleConditionCheckOn.Monitors,
];
default:
return [];
}
}
public static getConditionTypeByCheckOn(
checkOn: NotificationRuleConditionCheckOn,
): Array<ConditionType> {
switch (checkOn) {
case NotificationRuleConditionCheckOn.MonitorName:
case NotificationRuleConditionCheckOn.IncidentTitle:
case NotificationRuleConditionCheckOn.IncidentDescription:
case NotificationRuleConditionCheckOn.AlertTitle:
case NotificationRuleConditionCheckOn.AlertDescription:
case NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle:
case NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription:
return [
ConditionType.EqualTo,
ConditionType.NotEqualTo,
ConditionType.ContainsAny,
ConditionType.NotContains,
ConditionType.StartsWith,
ConditionType.EndsWith,
];
case NotificationRuleConditionCheckOn.IncidentSeverity:
case NotificationRuleConditionCheckOn.AlertSeverity:
return [ConditionType.ContainsAny, ConditionType.NotContains];
case NotificationRuleConditionCheckOn.IncidentState:
case NotificationRuleConditionCheckOn.AlertState:
case NotificationRuleConditionCheckOn.MonitorStatus:
case NotificationRuleConditionCheckOn.ScheduledMaintenanceState:
return [ConditionType.ContainsAny, ConditionType.NotContains];
case NotificationRuleConditionCheckOn.MonitorType:
return [ConditionType.ContainsAny, ConditionType.NotContains];
case NotificationRuleConditionCheckOn.AlertLabels:
case NotificationRuleConditionCheckOn.IncidentLabels:
case NotificationRuleConditionCheckOn.MonitorLabels:
case NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels:
return [
ConditionType.ContainsAny,
ConditionType.NotContains,
ConditionType.ContainsAll,
];
case NotificationRuleConditionCheckOn.Monitors:
return [
ConditionType.ContainsAny,
ConditionType.NotContains,
ConditionType.ContainsAll,
];
default:
return [];
}
}
}

View File

@@ -0,0 +1,8 @@
import CreateChannelNotificationRule from "../CreateChannelNotificationRule";
export default interface AlertNotificationRule
extends CreateChannelNotificationRule {
_type: "AlertNotificationRule";
shouldAutomaticallyInviteOnCallUsersToNewChannel: boolean;
}

View File

@@ -0,0 +1,8 @@
import CreateChannelNotificationRule from "../CreateChannelNotificationRule";
export default interface IncidentNotificationRule
extends CreateChannelNotificationRule {
_type: "IncidentNotificationRule";
shouldAutomaticallyInviteOnCallUsersToNewChannel: boolean;
}

View File

@@ -0,0 +1,7 @@
import BaseNotificationRule from "../BaseNotificationRule";
// This rule is just used to post to existing channels.
export default interface MonitorStatusNotificationRule
extends BaseNotificationRule {
_type: "MonitorStatusNotificationRule";
}

View File

@@ -0,0 +1,6 @@
import CreateChannelNotificationRule from "../CreateChannelNotificationRule";
export default interface ScheduledMaintenanceNotificationRule
extends CreateChannelNotificationRule {
_type: "ScheduledMaintenanceNotificationRule";
}

View File

@@ -0,0 +1,261 @@
import FilterCondition from "../../Filter/FilterCondition";
import {
ConditionType,
NotificationRuleConditionCheckOn,
} from "./NotificationRuleCondition";
import IncidentNotificationRule from "./NotificationRuleTypes/IncidentNotificationRule";
export class WorkspaceNotificationRuleUtil {
public static isRuleMatching(data: {
notificationRule: IncidentNotificationRule;
values: {
[key in NotificationRuleConditionCheckOn]:
| string
| Array<string>
| undefined;
};
}): boolean {
const notificationRule: IncidentNotificationRule = data.notificationRule;
// no filters means all filters are matched
if (data.notificationRule.filters.length === 0) {
return true;
}
const filterCondition: FilterCondition = notificationRule.filterCondition;
for (const filter of notificationRule.filters) {
const value: string | Array<string> | undefined =
data.values[filter.checkOn];
const condition: ConditionType | undefined = filter.conditionType;
const filterValue: string | Array<string> | undefined = filter.value;
if (!condition) {
continue;
}
const isMatched: boolean = this.didConditionMatch({
value,
condition,
filterValue,
});
if (filterCondition === FilterCondition.All) {
if (!isMatched) {
return false;
}
}
if (filterCondition === FilterCondition.Any) {
if (isMatched) {
return true;
}
}
}
if (filterCondition === FilterCondition.All) {
return true;
}
if (filterCondition === FilterCondition.Any) {
return false;
}
return false;
}
private static didConditionMatch(data: {
value: string | Array<string> | undefined;
condition: ConditionType;
filterValue: string | Array<string> | undefined;
}): boolean {
const value: string | Array<string> | undefined = data.value;
const condition: ConditionType = data.condition;
const filterValue: string | Array<string> | undefined = data.filterValue;
if (value === undefined || filterValue === undefined) {
return false;
}
switch (condition) {
case ConditionType.EqualTo: {
if (Array.isArray(value) && Array.isArray(filterValue)) {
return value.every((val: string) => {
return filterValue.includes(val);
});
}
return value === filterValue;
}
case ConditionType.NotEqualTo: {
if (Array.isArray(value) && Array.isArray(filterValue)) {
return !value.every((val: string) => {
return filterValue.includes(val);
});
}
return value !== filterValue;
}
case ConditionType.GreaterThan: {
if (Array.isArray(value) && Array.isArray(filterValue)) {
return value.every((val: string) => {
return filterValue.every((fVal: string) => {
return val > fVal;
});
});
} else if (value !== undefined && filterValue !== undefined) {
return value > filterValue;
}
return false;
}
case ConditionType.LessThan: {
if (Array.isArray(value) && Array.isArray(filterValue)) {
return value.every((val: string) => {
return filterValue.every((fVal: string) => {
return val < fVal;
});
});
} else if (value !== undefined && filterValue !== undefined) {
return value < filterValue;
}
return false;
}
case ConditionType.GreaterThanOrEqualTo: {
if (Array.isArray(value) && Array.isArray(filterValue)) {
return value.every((val: string) => {
return filterValue.every((fVal: string) => {
return val >= fVal;
});
});
} else if (value !== undefined && filterValue !== undefined) {
return value >= filterValue;
}
return false;
}
case ConditionType.LessThanOrEqualTo: {
if (Array.isArray(value) && Array.isArray(filterValue)) {
return value.every((val: string) => {
return filterValue.every((fVal: string) => {
return val <= fVal;
});
});
} else if (value !== undefined && filterValue !== undefined) {
return value <= filterValue;
}
return false;
}
case ConditionType.ContainsAny: {
if (Array.isArray(value) && Array.isArray(filterValue)) {
return value.some((val: string) => {
return filterValue.includes(val);
});
} else if (
value !== undefined &&
filterValue !== undefined &&
typeof value === "string"
) {
return filterValue.includes(value);
}
return false;
}
case ConditionType.NotContains: {
if (Array.isArray(value) && Array.isArray(filterValue)) {
return !value.some((val: string) => {
return filterValue.includes(val);
});
} else if (
value !== undefined &&
filterValue !== undefined &&
typeof value === "string"
) {
return !filterValue.includes(value);
}
return false;
}
case ConditionType.StartsWith: {
if (Array.isArray(value) && Array.isArray(filterValue)) {
return value.every((val: string) => {
return filterValue.every((fVal: string) => {
return val.startsWith(fVal);
});
});
} else if (
value !== undefined &&
filterValue !== undefined &&
typeof value === "string"
) {
return value.startsWith(filterValue.toString());
}
return false;
}
case ConditionType.EndsWith: {
if (Array.isArray(value) && Array.isArray(filterValue)) {
return value.every((val: string) => {
return filterValue.every((fVal: string) => {
return val.endsWith(fVal);
});
});
} else if (
value !== undefined &&
filterValue !== undefined &&
typeof value === "string"
) {
return value.endsWith(filterValue.toString());
}
return false;
}
case ConditionType.IsEmpty: {
if (Array.isArray(value)) {
return value.length === 0;
}
return value === "" || value === undefined;
}
case ConditionType.IsNotEmpty: {
if (Array.isArray(value)) {
return value.length > 0;
}
return value !== "" && value !== undefined;
}
case ConditionType.True: {
if (Array.isArray(value)) {
return value.every((val: string) => {
return val === "true";
});
}
return value === "true";
}
case ConditionType.False: {
if (Array.isArray(value)) {
return value.every((val: string) => {
return val === "false";
});
}
return value === "false";
}
case ConditionType.ContainsAll: {
if (Array.isArray(value) && Array.isArray(filterValue)) {
return filterValue.every((fVal: string) => {
return value.includes(fVal);
});
}
return false;
}
default: {
return false;
}
}
}
}

View File

@@ -0,0 +1,4 @@
export default interface WorkspaceChannelInvitationPayload {
workspaceUserIds: Array<string>;
channelNames: Array<string>;
}

View File

@@ -0,0 +1,30 @@
export interface WorkspaceMessageBlock {
_type: string;
onlyPostToTheseChannelNames?: Array<string>;
}
export interface WorkspaceMessagePayloadButton {
title: string; // Button title.
onlyPostToTheseChannelNames?: Array<string>; // Channel names to send message to.
}
export interface WorkspacePayloadHeader extends WorkspaceMessageBlock {
_type: "WorkspacePayloadHeader";
text: string;
}
export interface WorkspacePayloadMarkdown extends WorkspaceMessageBlock {
_type: "WorkspacePayloadMarkdown";
text: string;
}
export interface WorkspacePayloadButtons extends WorkspaceMessageBlock {
_type: "WorkspacePayloadButtons";
buttons: Array<WorkspaceMessagePayloadButton>;
}
export default interface WorkspaceMessagePayload {
_type: "WorkspaceMessagePayload";
channelNames: Array<string>; // Channel ids to send message to.
messageBlocks: Array<WorkspaceMessageBlock>; // Message to add to blocks.
}

View File

@@ -0,0 +1,6 @@
enum WorkspaceType {
Slack = "Slack",
MicrosoftTeams = "MicrosoftTeams",
}
export default WorkspaceType;

View File

@@ -32,7 +32,7 @@ const Card: FunctionComponent<ComponentProps> = (
return (
<React.Fragment>
<div className={props.className}>
<div data-testid="card" className={props.className}>
<div className="shadow sm:rounded-md">
<div className="bg-white py-6 px-4 sm:p-6">
<div className="flex justify-between">

View File

@@ -3,9 +3,11 @@ import UiAnalytics from "../../Utils/Analytics";
import Alert, { AlertType } from "../Alerts/Alert";
import Button, { ButtonStyleType } from "../Button/Button";
import ButtonTypes from "../Button/ButtonTypes";
import { DropdownOption, DropdownValue } from "../Dropdown/Dropdown";
import ErrorMessage from "../ErrorMessage/ErrorMessage";
import FormField from "./Fields/FormField";
import FormSummary from "./FormSummary";
import Steps from "./Steps/Steps";
import Field from "./Types/Field";
import Fields from "./Types/Fields";
@@ -48,6 +50,11 @@ export const DefaultValidateFunction: DefaultValidateFunctionType = (
return {};
};
export interface FormSummaryConfig {
enabled: boolean;
defaultStepName?: string | undefined;
}
export interface BaseComponentProps<T> {
submitButtonStyleType?: ButtonStyleType | undefined;
initialValues?: FormValues<T> | undefined;
@@ -73,6 +80,7 @@ export interface BaseComponentProps<T> {
onIsLastFormStep?: undefined | ((isLastFormStep: boolean) => void);
onFormValidationErrorChanged?: ((hasError: boolean) => void) | undefined;
showSubmitButtonOnlyIfSomethingChanged?: boolean | undefined;
summary?: FormSummaryConfig | undefined;
}
export interface ComponentProps<T extends GenericObject>
@@ -104,12 +112,33 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
setIsLoading(props.isLoading);
}, [props.isLoading]);
const getFormSteps: () => Array<FormStep<T>> | undefined = () => {
if (props.summary && props.summary.enabled) {
// add to last step
return [
...(props.steps || [
{
id: props.summary.defaultStepName || "basic",
title: props.summary.defaultStepName || "Basic",
isSummaryStep: false,
},
]),
{
id: "summary",
title: "Summary",
isSummaryStep: true,
},
];
}
return props.steps;
};
const [submitButtonText, setSubmitButtonText] = useState<string>(
props.submitButtonText || "Submit",
);
const [formSteps, setFormSteps] = useState<Array<FormStep<T>> | undefined>(
props.steps,
getFormSteps(),
);
const isInitialValuesSet: MutableRefObject<boolean> = useRef(false);
@@ -175,7 +204,7 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
useEffect(() => {
setFormSteps(
props.steps?.filter((step: FormStep<T>) => {
getFormSteps()?.filter((step: FormStep<T>) => {
if (!step.showIf) {
return true;
}
@@ -240,11 +269,26 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
for (const item of fields) {
// if this field is not the current step.
let shouldSkip: boolean = false;
if (
currentFormStepId &&
item.stepId &&
item.stepId !== currentFormStepId
) {
shouldSkip = true;
}
if (
props.summary?.enabled &&
(!props.steps || props.steps.length === 0)
) {
// if summary is enabled and no steps are provided, then all fields belong to the same step and should not be skipped.
shouldSkip = false;
item.stepId = props.summary.defaultStepName || "basic";
}
if (shouldSkip) {
continue;
}
@@ -536,7 +580,7 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
<div className="flex">
{formSteps && currentFormStepId && (
<div className="w-1/3">
<div style={{ flex: "0 1 auto" }} className="mr-10">
{/* Form Steps */}
<Steps
@@ -551,8 +595,9 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
)}
<div
className={`${
formSteps && currentFormStepId ? "w-2/3 pt-6" : "w-full pt-1"
formSteps && currentFormStepId ? "w-auto pt-6" : "w-full pt-1"
}`}
style={{ flex: "1 1 auto" }}
>
{props.error && (
<div className="mb-3">
@@ -575,6 +620,15 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
return true;
})
.filter((field: Field<T>) => {
const currentValues: FormValues<T> =
refCurrentValue.current;
if (field.showIf && !field.showIf(currentValues)) {
return false;
}
return true;
})
.map((field: Field<T>, i: number) => {
return (
<div key={getFieldName(field)}>
@@ -605,6 +659,16 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
</div>
);
})}
{/* If Summary, show Model detail */}
{currentFormStepId === "summary" && (
<FormSummary
formValues={refCurrentValue.current}
formFields={formFields}
formSteps={formSteps || undefined}
/>
)}
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More