Compare commits

...

830 Commits

Author SHA1 Message Date
Simon Larsen
3658f0349e feat: add react-native-worklets dependency to package.json and package-lock.json 2025-10-12 12:29:29 +01:00
Simon Larsen
02133165d8 chore: update dependencies and configure Tailwind CSS with NativeWind
- Added @expo/metro-runtime, nativewind, prettier-plugin-tailwindcss, react-dom, and tailwindcss to dependencies.
- Updated react-native-safe-area-context to version 5.4.0.
- Added global.css for Tailwind CSS styles.
- Created metro.config.js to integrate NativeWind with Expo.
- Added nativewind-env.d.ts for TypeScript support.
- Created tailwind.config.js to configure Tailwind CSS with NativeWind.
- Updated tsconfig.json to include nativewind-env.d.ts.
2025-10-12 12:28:30 +01:00
Simon Larsen
837b841352 refactor: remove unused ObjectID generation and related state management 2025-10-12 10:29:00 +01:00
Simon Larsen
10e344dad5 feat: add react-native-get-random-values dependency and import in App component 2025-10-12 10:08:23 +01:00
Simon Larsen
848bfb358f chore: add @oneuptime/common dependency and update tsconfig to exclude Common directory 2025-10-12 10:05:18 +01:00
Simon Larsen
2ca1c64532 Implement new feature for user authentication and improve error handling 2025-10-12 09:55:28 +01:00
Simon Larsen
d9a79eafbd fix: clarify file naming conventions in mobile app development prompt 2025-10-12 09:26:31 +01:00
Simon Larsen
a1dc16f4b6 feat: refactor styles into separate Styles.ts file for better organization 2025-10-12 09:25:23 +01:00
Nawaz Dhandala
674613c0d6 feat: add initial mobile app development prompt in MobileAppPrompt.md 2025-10-12 09:20:09 +01:00
Nawaz Dhandala
e4e095798a fix: enhance type definitions and import statements in App.tsx 2025-10-11 12:22:15 +01:00
Nawaz Dhandala
2e07185584 fix: standardize formatting in App.tsx and tsconfig.json 2025-10-11 12:19:33 +01:00
Nawaz Dhandala
144001981a Add initial configuration for mobile app with Expo
- Created package.json for project dependencies and scripts
- Added TypeScript configuration with strict settings and React Native support
2025-10-11 12:16:39 +01:00
Nawaz Dhandala
cd096f66ea fix: update app approval status message to include manual sideloading instructions 2025-10-11 12:15:52 +01:00
Nawaz Dhandala
02cffe9f8b fix: update Microsoft Teams outline image 2025-10-10 20:39:12 +01:00
Nawaz Dhandala
9c52e8966f fix: update Microsoft Teams outline image 2025-10-10 17:38:24 +01:00
Nawaz Dhandala
9257a949fe fix: add Microsoft Teams app client ID and secret placeholders to config example 2025-10-10 16:34:25 +01:00
Nawaz Dhandala
7528ed8e0c fix: update Microsoft Teams app manifest version and improve error handling 2025-10-10 15:55:18 +01:00
Nawaz Dhandala
98b6f3eac3 fix: streamline text formatting in Microsoft Teams integration messages 2025-10-10 09:17:34 +01:00
Nawaz Dhandala
a2561b3ae8 fix: update acknowledge route to acknowledge-page in UserNotificationRuleService 2025-10-10 09:15:55 +01:00
Nawaz Dhandala
368f5c6bbc Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-10-10 09:01:23 +01:00
Nawaz Dhandala
0e2e30b0a9 fix: remove Microsoft identity association endpoint for cleanup 2025-10-10 09:00:42 +01:00
Simon Larsen
2664a24875 fix: update outline image for Microsoft Teams integration 2025-10-09 18:47:58 +01:00
Simon Larsen
40e486669f feat: enhance app description and support information in Microsoft Teams API 2025-10-09 18:36:45 +01:00
Simon Larsen
ae64cbc718 feat: implement welcome adaptive card for OneUptime bot in Microsoft Teams 2025-10-09 18:35:32 +01:00
Simon Larsen
e14f691cc4 feat: add comment for Microsoft identity verification endpoint 2025-10-09 18:21:40 +01:00
Simon Larsen
89c607a530 feat: add endpoint for Microsoft identity association JSON 2025-10-09 18:21:03 +01:00
Simon Larsen
90f4e7418f fix: streamline SCIM group schema definition and ensure proper syntax in migrations 2025-10-09 17:40:44 +01:00
Simon Larsen
79fed2da09 fix: remove deprecated SCIM 1.1 schema references for improved compatibility 2025-10-09 17:34:37 +01:00
Simon Larsen
1dd41c103f Merge branch 'release' of github.com:OneUptime/oneuptime into release 2025-10-09 17:34:18 +01:00
Simon Larsen
988e85affc fix: remove deprecated SCIM schema reference in formatTeamForSCIM function 2025-10-09 17:34:15 +01:00
Nawaz Dhandala
2810516987 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-10-09 17:19:00 +01:00
Nawaz Dhandala
679c1971a2 fix: update WhatsApp setup instructions for token generation and template creation 2025-10-09 17:18:58 +01:00
Nawaz Dhandala
3abdbb7ef9 fix: correct typo in IncidentStateChangedOwnerNotification template ID 2025-10-09 17:12:17 +01:00
Simon Larsen
0d647f5dc1 fix: improve release retrieval by using pagination and handle missing release tags 2025-10-09 13:43:43 +01:00
Nawaz Dhandala
bd1df491a2 Remove MobileApp components, themes, and configuration files
- Deleted package.json, removing all dependencies and scripts for the mobile app.
- Removed FeatureCard, GlowingButton, MetricCard, SafeAreaContainer, and HomeScreen components.
- Eliminated color, layout, and typography theme files.
- Deleted TypeScript configuration file (tsconfig.json).
2025-10-09 11:59:34 +01:00
Nawaz Dhandala
feb872d05c feat: add E2E testing support with docker-compose configuration 2025-10-09 11:39:30 +01:00
Nawaz Dhandala
64b6b99a21 refactor: replace retry action with shell commands for preinstall and E2E tests 2025-10-08 20:10:19 +01:00
Nawaz Dhandala
8046c244b1 feat: add react-dom and react-native-web dependencies to package.json 2025-10-08 18:52:29 +01:00
Nawaz Dhandala
6928316ba0 feat: implement initial structure and components for OneUptime Mobile app 2025-10-08 18:33:50 +01:00
Nawaz Dhandala
36c74642f2 Initialize MobileApp with package.json and tsconfig.json for Expo project setup 2025-10-08 18:14:29 +01:00
Nawaz Dhandala
5a992e99c8 refactor: format publisherDocsUrl for better readability in Microsoft Teams app manifest 2025-10-08 18:06:52 +01:00
Nawaz Dhandala
ff9230f878 refactor: add publisherDocsUrl to Microsoft Teams app manifest 2025-10-08 17:08:45 +01:00
Nawaz Dhandala
9afa861ff2 refactor: remove obsolete migration files and update index 2025-10-08 17:04:59 +01:00
Nawaz Dhandala
6d7486e76d refactor: update developer name from "OneUptime" to "HackerBay Inc" in MicrosoftTeamsAPI 2025-10-08 17:02:59 +01:00
Nawaz Dhandala
8529012b19 refactor: remove "whatsAppText" column from WhatsAppLog table 2025-10-08 16:45:29 +01:00
Nawaz Dhandala
f704bd47a3 refactor: remove unused column "whatsAppText" from WhatsAppLog table 2025-10-08 14:58:24 +01:00
Nawaz Dhandala
239f2fc34e refactor: streamline actionLink resolution in createWhatsAppMessageFromTemplate 2025-10-08 14:35:31 +01:00
Nawaz Dhandala
5d5183b08e refactor: remove action_link from templateVariables in createWhatsAppMessageFromTemplate 2025-10-08 14:29:13 +01:00
Nawaz Dhandala
7cad0fab0f refactor: clean up whitespace and improve code formatting in multiple files 2025-10-08 14:20:41 +01:00
Nawaz Dhandala
98e0d55ee3 feat: update template variable iteration to include parameter names 2025-10-08 14:19:01 +01:00
Nawaz Dhandala
2d8f0d7a58 feat: add recipient_type to WhatsApp message payload for individual recipients 2025-10-08 13:46:55 +01:00
Nawaz Dhandala
643303cd7a feat: enhance error handling to include HTTPErrorResponse support 2025-10-08 13:27:26 +01:00
Nawaz Dhandala
dc692203be feat: add selectMoreFields for delivery channel options in notification settings 2025-10-07 22:40:29 +01:00
Nawaz Dhandala
648c51d007 feat: enhance notification settings with delivery channel options and icons 2025-10-07 22:37:40 +01:00
Nawaz Dhandala
0e5b106333 fix: update icon for resend code button from WhatsApp to SMS 2025-10-07 22:20:24 +01:00
Nawaz Dhandala
40e9ea2ab6 feat: add WhatsApp alert options to notification settings 2025-10-07 22:19:16 +01:00
Nawaz Dhandala
2157e228b9 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-10-07 21:49:36 +01:00
Nawaz Dhandala
5c5534adb8 fix: add type annotation for response in sendWhatsAppMessage method 2025-10-07 21:49:34 +01:00
Nawaz Dhandala
379297cd7e fix: streamline async function signatures in API routes for consistency 2025-10-07 21:49:14 +01:00
Nawaz Dhandala
b3730e9708 fix: add user authorization middleware to /test endpoint 2025-10-07 21:48:31 +01:00
Nawaz Dhandala
2fbc44d5c3 Refactor API endpoints to include error handling middleware
- Updated multiple API routes to use NextFunction for error handling.
- Wrapped asynchronous route handlers in try-catch blocks to catch and pass errors to the next middleware.
- Ensured consistent error responses across various endpoints, improving maintainability and readability.
- Enhanced the structure of the code by reducing nested try-catch blocks and improving flow control.
2025-10-07 21:46:25 +01:00
Nawaz Dhandala
12f05937af fix: enhance error handling in API endpoints by adding NextFunction support 2025-10-07 21:17:59 +01:00
Simon Larsen
5ea440492b Merge branch 'master' of github.com:OneUptime/oneuptime 2025-10-07 21:16:14 +01:00
Simon Larsen
57b851a498 refactor: rename github-release job to draft-github-release and add finalize-github-release job for publishing 2025-10-07 21:15:45 +01:00
Nawaz Dhandala
00f806b077 fix: enhance error handling in WhatsApp message template rendering for missing variables 2025-10-07 21:13:40 +01:00
Nawaz Dhandala
975c20a788 fix: improve error handling in verification endpoints by adding NextFunction support 2025-10-07 20:49:27 +01:00
Nawaz Dhandala
948e2d93c1 fix: handle errors in resend verification code endpoint and add NextFunction support 2025-10-07 20:38:11 +01:00
Nawaz Dhandala
6de3c93745 fix: refactor error handling for WhatsApp verification and resend code modals 2025-10-07 20:31:47 +01:00
Nawaz Dhandala
1f63110561 feat: enhance WhatsApp verification code handling with error logging and improved response management 2025-10-07 20:25:32 +01:00
Nawaz Dhandala
8a94c35450 feat: enforce template key requirement for WhatsApp messages and add debug logging for API requests 2025-10-07 16:59:53 +01:00
Nawaz Dhandala
fdc97284a5 fix: update DEFAULT_META_WHATSAPP_API_VERSION to v23.0 2025-10-07 16:48:22 +01:00
Nawaz Dhandala
792ecfdbdc fix: remove 'From' column from WhatsApp logs table 2025-10-07 16:04:24 +01:00
Nawaz Dhandala
121b01dc08 feat: add WhatsApp notifications toggle to notification settings 2025-10-07 16:01:27 +01:00
Nawaz Dhandala
1aa49071eb feat: add UserWhatsAppAPI for handling WhatsApp verification and resend functionality 2025-10-07 15:57:06 +01:00
Nawaz Dhandala
66bef1284a feat: add documentation for canceling all GitHub actions jobs using GitHub CLI 2025-10-07 15:53:37 +01:00
Nawaz Dhandala
1526f708de fix: enhance error handling and logging for WhatsApp message sending failures 2025-10-07 15:49:34 +01:00
Nawaz Dhandala
53198e4486 fix: improve error logging for WhatsApp message sending failures 2025-10-07 15:39:10 +01:00
Nawaz Dhandala
a66bf2df2a feat: enhance WhatsApp icon SVG with stroke properties and viewBox 2025-10-07 15:31:20 +01:00
Nawaz Dhandala
34422721c3 feat: add WhatsApp logs table to Notification Logs 2025-10-07 15:19:36 +01:00
Nawaz Dhandala
b5008c2363 Merge branch 'whatsapp' 2025-10-07 15:15:48 +01:00
Nawaz Dhandala
1cbab2be08 refactor: remove unused dashboard link and update WhatsApp message template variables 2025-10-07 15:15:25 +01:00
Simon Larsen
1bdcfd71f7 Merge pull request #2026 from OneUptime/whatsapp
Whatsapp
2025-10-07 15:05:58 +01:00
Nawaz Dhandala
792271b146 Merge branch 'whatsapp' of https://github.com/OneUptime/oneuptime into whatsapp 2025-10-07 15:02:44 +01:00
Nawaz Dhandala
3de5fbd35c fix: correct variable assignment for templateKey in test message route 2025-10-07 15:02:19 +01:00
Nawaz Dhandala
6f8dc1ed59 feat: update test message sending functionality to use predefined template and language 2025-10-07 15:00:41 +01:00
Nawaz Dhandala
92309d8fb2 feat: add test notification template and implement test message sending functionality in WhatsApp setup 2025-10-07 14:57:00 +01:00
Simon Larsen
d3070975cb Merge branch 'whatsapp' of github.com:OneUptime/oneuptime into whatsapp 2025-10-07 14:37:26 +01:00
Simon Larsen
69c0da5b17 refactor: remove WhatsApp high-risk cost variable and update related configurations 2025-10-07 14:37:23 +01:00
Nawaz Dhandala
1736690f01 feat: enhance WhatsApp setup markdown with detailed instructions for Business Account ID, App ID, and App Secret 2025-10-07 14:22:43 +01:00
Nawaz Dhandala
78a92905e9 feat: update WhatsApp setup markdown for clarity and improved instructions 2025-10-07 14:21:24 +01:00
Nawaz Dhandala
801c1b4ccb feat: remove header from WhatsApp setup markdown for improved clarity 2025-10-07 14:15:37 +01:00
Nawaz Dhandala
703b1dca51 feat: enhance WhatsApp setup markdown formatting for better structure and clarity 2025-10-07 14:12:55 +01:00
Nawaz Dhandala
e5ef97abd9 feat: improve WhatsApp setup markdown formatting for clarity and structure 2025-10-07 13:58:28 +01:00
Nawaz Dhandala
3053856990 feat: enhance WhatsApp setup markdown formatting for improved readability 2025-10-07 13:45:12 +01:00
Nawaz Dhandala
b8e82e2801 feat: add WhatsApp configuration fields to GlobalConfig migration and update template IDs for type safety 2025-10-07 13:36:21 +01:00
Nawaz Dhandala
2187fe63f0 feat: refactor WhatsApp template IDs and messages for improved type safety and export structure 2025-10-07 13:29:36 +01:00
Nawaz Dhandala
b1f842f9e1 feat: enhance WhatsApp setup documentation with prerequisites and structured steps 2025-10-07 13:25:52 +01:00
Nawaz Dhandala
43e03fb61c feat: update WhatsApp icon SVG path for improved rendering 2025-10-07 13:19:29 +01:00
Nawaz Dhandala
2327ab84c2 feat: add migration for WhatsApp configuration fields in GlobalConfig 2025-10-07 13:16:19 +01:00
Nawaz Dhandala
93034b6018 refactor: update WhatsApp migration and enhance type definitions in WhatsApp settings 2025-10-07 13:10:54 +01:00
Nawaz Dhandala
e45022b5cb feat: add migration for WhatsApp notifications and related database changes 2025-10-07 13:06:47 +01:00
Nawaz Dhandala
c022b70e6d Merge branch 'whatsapp' of https://github.com/OneUptime/oneuptime into whatsapp 2025-10-07 13:06:11 +01:00
Nawaz Dhandala
5615ba2df7 Merge branch 'master' into whatsapp 2025-10-07 13:05:46 +01:00
Nawaz Dhandala
268960bc5b refactor: simplify destructuring of connection settings in getTransporter method 2025-10-07 12:58:19 +01:00
Nawaz Dhandala
508a713ecf refactor: enhance connection handling in TransporterPool for improved email server configuration 2025-10-07 12:57:43 +01:00
Simon Larsen
36e50b591a refactor: update WhatsApp template messages for improved clarity and conciseness 2025-10-07 12:17:04 +01:00
Nawaz Dhandala
05c1f95ba4 fix: enable tool-cache in release workflow for improved build efficiency 2025-10-07 11:52:13 +01:00
Simon Larsen
9d355691ae refactor: rename scheduled_maintenance_number to scheduled_event_number in WhatsApp templates and notification jobs for consistency 2025-10-07 11:32:54 +01:00
Simon Larsen
73c58186b6 refactor: enhance WhatsApp template messages with scheduled maintenance number for better context 2025-10-06 20:23:11 +01:00
Simon Larsen
eb8d3e4dfd refactor: update WhatsApp template messages for clarity and conciseness 2025-10-06 20:17:38 +01:00
Nawaz Dhandala
987f30e5c7 feat: add PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD environment variable to Dockerfiles for improved build performance 2025-10-06 19:45:46 +01:00
Simon Larsen
b7a0dbf81b refactor: replace action_link with corresponding link variables in notification templates 2025-10-06 18:25:59 +01:00
Simon Larsen
7368a0eb7c Refactor notification links in WhatsApp templates and services
- Updated variable names for links in various services to improve consistency:
  - Changed `*_link_on_dashboard` to `*_link` across multiple services including MonitorService, OnCallDutyPolicyEscalationRuleService, and UserNotificationRuleService.

- Modified WhatsApp template messages to reflect the new link variable names, ensuring that the notifications sent to users contain the correct links.

- Adjusted the link variable names in the WhatsAppTemplateUtil to match the updated naming convention.

- Ensured all related notification jobs (e.g., SendCreatedResourceNotification, SendNotePostedNotification) are updated to use the new link variables for better clarity and maintainability.
2025-10-06 18:07:39 +01:00
Nawaz Dhandala
ef0b6f3e14 refactor: replace docker login actions with retry mechanism for improved reliability 2025-10-06 16:36:28 +01:00
Simon Larsen
af8691f61d refactor: remove Meta WhatsApp API version handling and update documentation 2025-10-06 16:31:29 +01:00
Simon Larsen
f7aee2e253 feat: add Meta WhatsApp settings page and configuration options 2025-10-06 16:26:05 +01:00
Nawaz Dhandala
0a1b74d911 fix: update @simplewebauthn/server to version 13.2.2 in package.json and package-lock.json 2025-10-06 16:13:53 +01:00
Nawaz Dhandala
ab48da447d refactor: add test jobs to push-release-tags workflow for enhanced testing coverage 2025-10-06 16:08:43 +01:00
Nawaz Dhandala
1cc0630939 feat: add APP_TAG pinning to versioned release in config.env 2025-10-06 16:06:16 +01:00
Simon Larsen
5391ff4688 Add dashboard links to WhatsApp notifications for various resources
- Added `monitor_link_on_dashboard` to MonitorService notifications.
- Added `on_call_policy_link_on_dashboard` to OnCallDutyPolicyEscalationRule services.
- Added `on_call_schedule_link_on_dashboard` to OnCallDutyPolicyScheduleService.
- Added `probe_link_on_dashboard` to ProbeService notifications.
- Enhanced UserNotificationRuleService to include `alert_link_on_dashboard` and `incident_link_on_dashboard`.
- Updated WhatsApp templates to include links to the OneUptime dashboard for alerts, incidents, monitors, probes, scheduled maintenance, and status pages.
- Added `status_page_link_on_dashboard` to StatusPageOwners notifications.
2025-10-06 15:56:12 +01:00
Nawaz Dhandala
f6d96676fe refactor: update dependencies for test-e2e-release-saas job to include publish-mcp-server 2025-10-06 15:54:50 +01:00
Simon Larsen
cf02842ab1 Refactor WhatsApp message handling across services
- Updated `createWhatsAppMessageFromTemplate` to return a typed `WhatsAppMessagePayload`.
- Added type annotations for `WhatsAppTemplateId` in various services to improve type safety.
- Refactored WhatsApp message creation in `ProbeService`, `UserNotificationRuleService`, `UserNotificationSettingService`, and other worker jobs to ensure consistent typing.
- Enhanced readability and maintainability by using explicit types for event types and WhatsApp message payloads.
- Improved the structure of WhatsApp template ID definitions for better clarity and type inference.
2025-10-06 15:12:29 +01:00
Nawaz Dhandala
b63fcf6b99 refactor: update push-release-tags job dependencies for improved workflow order 2025-10-06 14:54:08 +01:00
Nawaz Dhandala
36069c1b4e refactor: update job dependencies for push-release-tags and github-release in release workflow 2025-10-06 14:48:05 +01:00
Simon Larsen
d2846decce refactor: improve code formatting and readability across multiple files 2025-10-06 14:47:15 +01:00
Simon Larsen
60a8a3f052 feat: include alert and incident identifiers in SMS notifications for better context 2025-10-06 14:45:46 +01:00
Simon Larsen
e2d15dc2e7 Merge branch 'master' of github.com:OneUptime/oneuptime into whatsapp 2025-10-06 14:39:00 +01:00
Simon Larsen
7cdefdeccd feat: add incident and alert numbers to WhatsApp notification templates and related services 2025-10-06 14:37:45 +01:00
Simon Larsen
684b8822af feat: simplify WhatsApp template messages for clarity and user engagement 2025-10-06 14:20:52 +01:00
Nawaz Dhandala
231bc47942 refactor: improve type annotations and formatting in TeamService for clarity 2025-10-06 14:14:11 +01:00
Nawaz Dhandala
965a497be3 feat: implement SCIM mutation checks for team creation and deletion 2025-10-06 14:12:19 +01:00
Nawaz Dhandala
f50a7fb99b refactor: adjust job dependencies and increase timeout for E2E tests in release workflows 2025-10-06 14:08:50 +01:00
Simon Larsen
50a5e75d1a feat: enhance WhatsApp template messages with additional information for user guidance 2025-10-06 14:00:01 +01:00
Simon Larsen
84e838a055 refactor: streamline WhatsApp message creation by removing unused template string imports 2025-10-04 13:49:12 +01:00
Simon Larsen
6d6c78e974 feat: Integrate WhatsApp notifications for various alert and incident events
- Added WhatsApp message creation for alert owner notifications in SendCreatedResourceNotification, SendNotePostedNotification, SendOwnerAddedNotification, and SendStateChangeNotification jobs.
- Implemented WhatsApp message functionality for incident owner notifications in SendCreatedResourceNotification, SendNotePostedNotification, SendOwnerAddedNotification, and SendStateChangeNotification jobs.
- Enhanced monitor owner notifications with WhatsApp messages in SendCreatedResourceNotification, SendOwnerAddedNotification, and SendStatusChangeNotification jobs.
- Included WhatsApp message creation for scheduled maintenance owner notifications in SendCreatedResourceNotification, SendNotePostedNotification, SendOwnerAddedNotification, and SendStateChangeNotification jobs.
- Added WhatsApp message functionality for status page owner notifications in SendAnnouncementCreatedNotification, SendCreatedResourceNotification, and SendOwnerAddedNotification jobs.
- Introduced WhatsAppTemplateUtil to manage WhatsApp message templates and creation logic.
2025-10-04 13:22:56 +01:00
Simon Larsen
778d5b7c6b feat: add UserWhatsAppService and integrate WhatsApp verification code template 2025-10-03 19:25:53 +01:00
Nawaz Dhandala
8051146f41 refactor: enhance type safety and improve variable naming in OnCallDutyScheduleSettings 2025-10-03 19:19:46 +01:00
Simon Larsen
86a359a230 feat: enhance WhatsApp notification handling with template support and error logging 2025-10-03 19:17:10 +01:00
Simon Larsen
c16dac65cc feat: add WhatsApp template IDs and rendering logic for notifications 2025-10-03 19:16:04 +01:00
Simon Larsen
437c9ecdbc feat: add support for WhatsApp notifications in project and user notification settings 2025-10-03 19:04:33 +01:00
Nawaz Dhandala
bf4eec2bdf refactor: improve code formatting and comments for better readability in SCIM and TimePicker components 2025-10-03 19:00:49 +01:00
Nawaz Dhandala
08367f3c7f refactor: update onDuplicateSuccess type to support Promise and implement duplication logic in OnCallDutyScheduleSettings 2025-10-03 18:23:29 +01:00
Simon Larsen
f5d077956a feat: integrate UserWhatsApp into notification settings and management 2025-10-03 18:12:07 +01:00
Simon Larsen
ca74005262 feat: add WhatsApp phone display in NotificationMethodView and create WhatsApp management component 2025-10-03 18:11:10 +01:00
Simon Larsen
52c936935e feat: add UserWhatsApp relation and ID to UserOnCallLogTimeline model 2025-10-03 17:51:34 +01:00
Simon Larsen
2951600ed9 feat: add UserWhatsApp relation and ID to UserNotificationRule model 2025-10-03 17:51:15 +01:00
Simon Larsen
d12c8c778c feat: Add UserWhatsApp and WhatsAppLog models with associated services
- Created UserWhatsApp model to manage WhatsApp numbers linked to users and projects.
- Implemented WhatsAppLog model to log messages sent via WhatsApp, including details like recipient, status, and associated incidents or alerts.
- Developed WhatsAppLogService for managing WhatsApp log entries, including automatic deletion of old logs if billing is enabled.
- Introduced WhatsAppService for sending WhatsApp messages with various contextual options.
2025-10-03 17:28:31 +01:00
Simon Larsen
77d4527a00 feat: add WhatsApp integration with API and configuration support 2025-10-03 17:24:59 +01:00
Nawaz Dhandala
1ef3353155 fix: update modelId retrieval to correctly use parameter for OnCallDutyScheduleSettings 2025-10-03 17:11:04 +01:00
Nawaz Dhandala
2c635c0d1e refactor: add modulePathIgnorePatterns to jest config for build directory 2025-10-03 16:17:45 +01:00
Nawaz Dhandala
44799f4625 Merge branch 'master' of https://github.com/OneUptime/oneuptime into timepkr 2025-10-03 15:39:38 +01:00
Nawaz Dhandala
4bc6e625d2 refactor: adjust spacing for timezone label display in TimePicker component 2025-10-03 15:38:49 +01:00
Nawaz Dhandala
9ff773dd81 refactor: update timezone label display in TimePicker component for improved clarity 2025-10-03 15:37:09 +01:00
Nawaz Dhandala
bf2c2afbb8 refactor: simplify browser locale detection for 12-hour format and enhance timezone label display 2025-10-03 15:01:26 +01:00
Simon Larsen
3efa3b374f feat: add push-release-tags job to create Docker release tags after GitHub release 2025-10-03 14:59:29 +01:00
Nawaz Dhandala
f76b004e2a refactor: implement browser locale detection for 12-hour time format preference 2025-10-03 14:55:47 +01:00
Nawaz Dhandala
cb535bd114 refactor: add type annotations for improved type safety in TimePicker component 2025-10-03 14:45:56 +01:00
Simon Larsen
9b09082d13 feat: add settings page for on-call duty schedule with duplication functionality 2025-10-03 14:37:20 +01:00
Nawaz Dhandala
5ac6e7ba7b refactor: improve code readability in Input and TimePicker components 2025-10-03 14:32:51 +01:00
Nawaz Dhandala
044183dbd7 refactor: enhance TimePicker input size and icon styling for improved usability 2025-10-03 14:27:23 +01:00
Nawaz Dhandala
fb13609c37 refactor: add margin to TimePicker component for improved layout 2025-10-03 14:23:36 +01:00
Nawaz Dhandala
1c602b1ad3 refactor: adjust TimePicker input size and improve clickability indication 2025-10-03 14:18:52 +01:00
Nawaz Dhandala
1affb76085 refactor: update TimePicker component layout and styling for improved usability 2025-10-03 14:16:40 +01:00
Nawaz Dhandala
0e02083dec refactor: implement modal for time selection in TimePicker component 2025-10-03 14:13:51 +01:00
Nawaz Dhandala
a442b02337 refactor: update TimePicker import path to use the correct index file 2025-10-03 14:07:33 +01:00
Simon Larsen
3ab47328f6 refactor: enhance SCIM group schema compatibility by including SCIM 1.1 core schema 2025-10-03 14:01:42 +01:00
Nawaz Dhandala
57ff8c007b refactor: integrate TimePicker component for time selection in FormField and adjust Input spellCheck logic 2025-10-03 13:59:30 +01:00
Nawaz Dhandala
c83d1babcd refactor: update jest configuration to support TypeScript and improve coverage collection 2025-10-03 13:54:50 +01:00
Simon Larsen
1bc96d04fa refactor: enhance artifact naming in release workflows to include job and run attempt 2025-10-03 13:51:52 +01:00
Nawaz Dhandala
e03a9b52ae refactor: remove InputType.TIME usage and implement TimePicker component for time selection 2025-10-03 13:43:53 +01:00
Simon Larsen
a307a68ec1 refactor: implement retry logic for E2E tests in release workflows 2025-10-03 13:36:19 +01:00
Nawaz Dhandala
1032f01278 refactor: enhance query execution logic to handle query parameters more effectively 2025-10-02 19:43:52 +01:00
Nawaz Dhandala
83286aa34d refactor: remove maxmemory-policy from redis config and set to noeviction in values.yaml 2025-10-02 19:31:53 +01:00
Nawaz Dhandala
543757a2b5 refactor: update helm template command to specify chart directory for improved clarity 2025-10-02 16:46:04 +01:00
Nawaz Dhandala
d0bbb12f92 refactor: improve formatting and readability of Microsoft Teams installation instructions 2025-10-02 16:44:40 +01:00
Nawaz Dhandala
066e785a53 refactor: add Microsoft Teams navigation links and integration documentation 2025-10-02 15:15:38 +01:00
Nawaz Dhandala
2f46eadcd4 refactor: add call scheduling feature with inline calendar integration to support page 2025-10-02 15:00:32 +01:00
Nawaz Dhandala
c472555f55 refactor: update installation instructions to include app approval status for Microsoft Teams integration 2025-10-02 14:56:30 +01:00
Nawaz Dhandala
83c9255b98 refactor: enhance Microsoft Teams integration with billing conditionals and installation instructions 2025-10-02 14:47:54 +01:00
Nawaz Dhandala
1748a198c7 refactor: update Microsoft Teams permissions documentation for clarity and conciseness 2025-10-02 14:42:35 +01:00
Nawaz Dhandala
a4841f4b6e refactor: remove ts-ignore and update axios mock type for improved type safety 2025-10-02 13:15:57 +01:00
Nawaz Dhandala
1c750d274e refactor: replace ts-ignore with ts-expect-error for improved type safety in Handlebars helpers and test mocks 2025-10-02 12:18:10 +01:00
Nawaz Dhandala
2dda68c462 refactor: update comment styles for consistency across ProjectAPI, DataPoint, and ComponentCode 2025-10-02 11:57:39 +01:00
Nawaz Dhandala
6d5bc111ba Refactor comments across multiple files to improve clarity and consistency
- Updated comments in Probe/Config.ts to use block comments for proxy configuration.
- Refactored comments in PortMonitor.ts, SyntheticMonitor.ts, and OnlineCheck.ts to block comments for better readability.
- Adjusted comments in ProbeIngest/API/Monitor.ts and ProbeIngest/API/Probe.ts to block comments for clarity.
- Standardized comments in various data migration scripts to block comments for consistency.
- Modified eslint.config.js to enforce multiline comment style as an error.
2025-10-02 11:53:55 +01:00
Nawaz Dhandala
0e1c8df7ab refactor: simplify logging and formatting in Microsoft Teams actions 2025-10-02 11:40:47 +01:00
Nawaz Dhandala
4b55cb3d84 feat: refine incident action handling to improve clarity and flow 2025-10-02 11:36:45 +01:00
Nawaz Dhandala
eb66b57939 feat: prevent duplicate message processing for new incident and scheduled maintenance submissions 2025-10-02 11:33:53 +01:00
Nawaz Dhandala
b53119d249 feat: implement separate handling for new scheduled maintenance creation in Microsoft Teams 2025-10-02 11:32:24 +01:00
Nawaz Dhandala
95c2db5ace feat: streamline incident and scheduled maintenance confirmation by sending messages after hiding form cards 2025-10-02 11:23:41 +01:00
Nawaz Dhandala
8c4bc48c44 feat: add incident and scheduled maintenance links in response messages 2025-10-02 11:18:56 +01:00
Nawaz Dhandala
28f3b98d44 feat: implement new incident and scheduled maintenance submission handling in Microsoft Teams 2025-10-02 11:13:31 +01:00
Nawaz Dhandala
742da27d87 feat: add commands for creating new incidents and scheduled maintenance in Microsoft Teams 2025-10-02 10:55:28 +01:00
Nawaz Dhandala
e2ba7dff8d refactor: update KEDA autoscaling configuration comments for clarity 2025-10-02 09:25:47 +01:00
Simon Larsen
3f6a58a087 Merge pull request #2024 from calvinbui/master
helm: allow removing the auto-generated date label
2025-10-02 09:25:07 +01:00
Calvin Bui
3f2005bd34 fix jinja 2025-10-02 11:38:16 +10:00
Calvin Bui
651ba4247e fix: remove auto-generated date label causing constant updates 2025-10-02 11:36:07 +10:00
Nawaz Dhandala
ba71cf5980 fix: Remove system status command from Microsoft Teams bot responses 2025-10-01 20:57:55 +01:00
Nawaz Dhandala
442a4b935b feat: Enhance incident and maintenance messages with severity icons and dashboard links 2025-10-01 20:52:39 +01:00
Nawaz Dhandala
5caf5e8991 fix: Update command list formatting in bot help message for consistency 2025-10-01 20:50:00 +01:00
Nawaz Dhandala
b5e4545193 fix: Simplify conditional check for TOTP and WebAuthn lists in login function 2025-10-01 20:38:01 +01:00
Nawaz Dhandala
ce4cc70815 refactor: Improve type annotations for error handling and channel name mapping in WorkspaceNotificationRuleService 2025-10-01 20:32:19 +01:00
Nawaz Dhandala
426dda60fe refactor: Enhance two-factor authentication verification logic and improve placeholder formatting in NotificationRuleForm 2025-10-01 20:28:18 +01:00
Nawaz Dhandala
c6be5a9ebf fix: Update placeholder text for existing channel input based on workspace type in NotificationRuleForm 2025-10-01 20:21:33 +01:00
Nawaz Dhandala
eb19b8926b docs: Update setup instructions for Microsoft Teams integration 2025-10-01 18:29:21 +01:00
Nawaz Dhandala
955a3f784d refactor: Remove duplicate code for handling Adaptive Card submits in MicrosoftTeamsUtil 2025-10-01 18:25:11 +01:00
Nawaz Dhandala
756dc88289 refactor: Simplify error message mapping and improve readability in WorkspaceNotificationRuleService 2025-10-01 18:12:08 +01:00
Nawaz Dhandala
c116f9adc1 refactor: Update existing channel handling to use WorkspaceChannel objects and include teamId for Microsoft Teams integration 2025-10-01 18:07:36 +01:00
Nawaz Dhandala
324f29edc3 fix: Improve error messages for missing teamId in Microsoft Teams channel resolution and message sending 2025-10-01 17:56:38 +01:00
Nawaz Dhandala
03064308ef feat: Add teamId to WorkspaceChannel interface and update MicrosoftTeamsUtil to include teamId in channel creation 2025-10-01 14:11:59 +01:00
Nawaz Dhandala
9dd4ac6f6b feat: Add error handling for message sending in Microsoft Teams and Slack integrations 2025-10-01 14:08:29 +01:00
Nawaz Dhandala
8b8f1ba530 fix: Ensure teamId is required for resolving channel names and sending messages in Microsoft Teams 2025-10-01 14:02:47 +01:00
Nawaz Dhandala
31e77d2208 feat: Add optional teamId to WorkspaceMessagePayload for Microsoft Teams integration 2025-10-01 13:53:38 +01:00
Nawaz Dhandala
c6e78a3264 fix: Update Buffer handling to use Uint8Array and ArrayBuffer for improved type safety 2025-10-01 12:51:16 +01:00
Nawaz Dhandala
c474d3b1e5 chore: update eslint-plugin-react to version 7.37.5 2025-09-30 20:30:54 +01:00
Nawaz Dhandala
3afbbfae5a fix: Update schema to allow null values for various string and object properties 2025-09-30 20:12:33 +01:00
Nawaz Dhandala
92c3c619ee refactor: Clean up code formatting and improve readability in Microsoft Teams components 2025-09-30 19:58:52 +01:00
Nawaz Dhandala
b65e28684e fix: Improve KEDA autoscaler condition checks for probes and enhance values.yaml structure 2025-09-30 19:53:51 +01:00
Nawaz Dhandala
dabf1464f2 fix: Update nodeSelector handling in multiple deployment templates for improved clarity and consistency 2025-09-30 19:33:51 +01:00
Nawaz Dhandala
9f618acc31 fix: Refactor security context handling for pods and containers to improve clarity and maintainability 2025-09-30 19:27:32 +01:00
Nawaz Dhandala
3dd4caeae9 feat: Update security context handling for various deployments 2025-09-30 19:23:18 +01:00
Nawaz Dhandala
1345693175 feat: Add pod and container security context support for PostgreSQL, ClickHouse, and Redis 2025-09-30 18:21:54 +01:00
Nawaz Dhandala
b5a422e8aa fix: Change oneuptimeSecret type from object to string and adjust encryptionSecret definition 2025-09-30 18:16:56 +01:00
Nawaz Dhandala
122773b0ba fix: Update comment for oneuptimeSecret to clarify string requirement 2025-09-30 18:16:16 +01:00
Nawaz Dhandala
cfc1d5d820 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-09-30 18:12:14 +01:00
Nawaz Dhandala
18d1df86d5 feat: Add pod and container security context support across deployments
- Updated deployment templates to merge podSecurityContext and containerSecurityContext from values.
- Enhanced the values schema and default values to include podSecurityContext and containerSecurityContext for various services.
- Ensured that security contexts are applied consistently across all relevant templates including accounts, admin-dashboard, api-reference, app, dashboard, docs, fluent-ingest, home, incoming-request-ingest, isolated-vm, nginx, open-telemetry-ingest, otel-collector, probe-ingest, server-monitor-ingest, status-page, test-server, worker, and workflow.
2025-09-30 18:12:11 +01:00
Nawaz Dhandala
4b65366080 feat: Merge nodeSelector values for various deployments in Helm templates 2025-09-30 17:52:19 +01:00
Nawaz Dhandala
74bb6acd62 feat: Add resources and nodeSelector fields to various components in values.yaml and values.schema.json 2025-09-30 17:47:06 +01:00
Simon Larsen
a47b0fe21d Merge pull request #2023 from calvinbui/master
helm: add deployment update strategy
2025-09-30 17:43:30 +01:00
Nawaz Dhandala
349e64e3a2 Merge branch 'yubikey-auth' 2025-09-30 17:14:39 +01:00
Nawaz Dhandala
0f64b49f9c refactor: Clean up code formatting and improve readability in various files 2025-09-30 17:14:23 +01:00
Nawaz Dhandala
1f8d4967ee refactor: Improve logging for email verification process 2025-09-30 17:07:44 +01:00
Nawaz Dhandala
e8b580ab4c refactor: Ensure TOTP and WebAuthn lists are always returned as arrays in fetchTotpAuthList 2025-09-30 17:04:32 +01:00
Nawaz Dhandala
866183d95a refactor: Rename two-factor authentication API endpoints to use TOTP terminology 2025-09-30 17:00:24 +01:00
Nawaz Dhandala
ca64843de0 refactor: Add validation for email and password in login function 2025-09-30 16:50:18 +01:00
Calvin Bui
d7427b9abe update values.schema.json 2025-09-30 23:43:05 +10:00
Calvin Bui
77537999b0 add deployment update strategy 2025-09-30 23:24:17 +10:00
Nawaz Dhandala
263c341792 refactor: Update login function to handle multiple two-factor authentication methods 2025-09-30 13:47:00 +01:00
Nawaz Dhandala
45b1dcbb7e refactor: Update WebAuthn verification API to use initial values and remove unused endpoint 2025-09-30 13:38:20 +01:00
Nawaz Dhandala
43e344dfb1 refactor: Update login function to check for both TOTP and WebAuthn verification 2025-09-30 13:34:20 +01:00
Nawaz Dhandala
e5b2e70ce2 refactor: Rename verifyTwoFactorAuth to verifyTotpAuth for consistency 2025-09-30 13:31:38 +01:00
Nawaz Dhandala
602383a720 refactor: Rename UserTwoFactorAuth table to UserTotpAuth in migration 2025-09-30 13:18:28 +01:00
Nawaz Dhandala
c10e691bf5 refactor: Remove UserTwoFactorAuth model and service in favor of TotpAuth implementation 2025-09-30 13:15:09 +01:00
Nawaz Dhandala
ab7c8c1df9 feat: Add UserWebAuthnAPI routes for authentication verification and update API paths 2025-09-30 13:13:45 +01:00
Nawaz Dhandala
72236b025f refactor: replace TwoFactorAuth with TotpAuth and update related implementations 2025-09-30 13:08:58 +01:00
Nawaz Dhandala
1c5c9ee3ad refactor: update UserWebAuthnAPI and UserWebAuthnService for improved type definitions and clarity 2025-09-30 13:06:46 +01:00
Nawaz Dhandala
352c923e74 refactor: rename UserTwoFactorAuth to UserTotpAuth and update related services, APIs, and models
- Replaced all instances of UserTwoFactorAuth with UserTotpAuth across the codebase.
- Updated API endpoints to reflect the new naming convention.
- Modified database models and services to accommodate the changes.
- Adjusted frontend components to utilize the new UserTotpAuth model.
- Ensured all references, imports, and exports are consistent with the new naming.
2025-09-30 13:02:32 +01:00
Nawaz Dhandala
d3ebbf1c88 feat: Add migration to remove two-factor authentication columns from User table 2025-09-30 12:51:09 +01:00
Simon Larsen
411a455d54 Merge branch 'yubikey-auth' of github.com:OneUptime/oneuptime into yubikey-auth 2025-09-30 12:47:44 +01:00
Nawaz Dhandala
4570d617b2 feat: Implement WebAuthn authentication verification and update login function to support multiple verification methods 2025-09-30 12:46:19 +01:00
Nawaz Dhandala
305094a8b2 feat: Refactor UserWebAuthnAPI and UserWebAuthnService to utilize service methods for registration and authentication options generation 2025-09-30 12:43:13 +01:00
Simon Larsen
b02d1da202 refactor: Simplify master admin status assignment in LoginUtil 2025-09-30 12:29:43 +01:00
Simon Larsen
1407a0fce9 feat: Replace loading spinner and error message with reusable components in LoginPage 2025-09-30 11:58:31 +01:00
Simon Larsen
836fb91cbe refactor: Update terminology to "Security Key-Based Two-Factor Authentication" for clarity and consistency 2025-09-30 11:55:07 +01:00
Simon Larsen
4c819ca906 feat: Move "Add Security Key" button to the right side of the security keys table for improved UI layout 2025-09-30 11:52:57 +01:00
Simon Larsen
3bd7fb7c59 refactor: Update terminology to "Authenticator Based Two Factor Authentication" for clarity and consistency 2025-09-30 11:51:38 +01:00
Simon Larsen
99e7193961 feat: Integrate Base64 encoding for WebAuthn challenge and credential handling in UserWebAuthnAPI and LoginPage 2025-09-30 11:49:23 +01:00
Simon Larsen
db10bba4d3 feat: Implement Base64 utility functions and integrate them into WebAuthn registration process 2025-09-30 11:47:48 +01:00
Simon Larsen
31a425d34d fix: Ensure security keys are always verified by default in UserWebAuthnService 2025-09-30 11:31:54 +01:00
Simon Larsen
7870406295 feat: Integrate WebAuthn verification in two-factor authentication checks 2025-09-30 11:31:40 +01:00
Simon Larsen
77de0a1116 feat: Upgrade Node.js base image to version 24.9-alpine3.21 in multiple Dockerfiles and remove debugging flag from nodemon configurations 2025-09-30 11:27:03 +01:00
Simon Larsen
f2da31a6f9 feat: Update package-lock.json to add new dependencies for testing and TypeScript 2025-09-29 21:40:34 +01:00
Simon Larsen
d36577069f feat: Add UserWebAuthn migration for WebAuthn credential management 2025-09-29 20:54:24 +01:00
Simon Larsen
4cdf959a01 fix: Update channel name comparison logic to use displayName for accurate matching 2025-09-29 20:21:12 +01:00
Simon Larsen
62adb3fd76 fix: Ensure team selection is required for Microsoft Teams integration in notification rules 2025-09-29 20:13:56 +01:00
Simon Larsen
5bc6e67c21 refactor: Enhance button styles and update team item display in TeamsAvailableModal 2025-09-29 20:07:13 +01:00
Simon Larsen
d7e86a56e6 refactor: Remove search functionality and simplify team list display in TeamsAvailableModal 2025-09-29 19:56:19 +01:00
Simon Larsen
c2b569c13a feat: Implement TeamsAvailableModal for improved team selection in Microsoft Teams integration 2025-09-29 19:41:06 +01:00
Simon Larsen
6d6555396a fix: Correct API URL handling in Microsoft Teams integration 2025-09-29 19:19:42 +01:00
Simon Larsen
13018c8169 refactor: Update API URL handling for Microsoft Teams integration 2025-09-29 19:14:53 +01:00
Simon Larsen
82f462785b feat: Add modal to display and refresh Microsoft Teams list in integration 2025-09-29 19:08:44 +01:00
Simon Larsen
9f21725949 refactor: Simplify teams retrieval by removing redundant checks and updating refresh logic 2025-09-29 17:37:19 +01:00
Nawaz Dhandala
82bee44933 Refactor Microsoft Teams integration code for improved type safety and clarity
- Added type annotations for various variables and constants in Incident, Monitor, ScheduledMaintenance, and MicrosoftTeams actions.
- Enhanced type definitions for JSON-related imports and improved handling of JSON values.
- Updated methods to ensure consistent use of types across the Microsoft Teams integration.
- Refactored message formatting in incident and monitor details to explicitly define message types.
- Improved readability and maintainability of the code by restructuring some conditional statements and variable declarations.
- Ensured proper handling of project authentication and access tokens with clearer logging and error handling.
2025-09-29 17:22:05 +01:00
Nawaz Dhandala
b67c702d4d Refactor Microsoft Teams integration components and improve code readability
- Removed unnecessary blank lines in Workspace.ts and MicrosoftTeamsIntegration.tsx.
- Reformatted function parameters for better readability in WorkspaceType.ts.
- Simplified imports in MicrosoftTeamsIntegration.tsx and NotificationRuleForm.tsx.
- Enhanced conditional checks for better clarity in NotificationRuleForm.tsx and NotificationRuleViewElement.tsx.
- Improved formatting and consistency in MicrosoftTeamsIntegrationDocumentation.tsx.
- Updated state management and conditional rendering in WorkspaceNotificationRulesTable.tsx.
- Ensured consistent use of line breaks and indentation across various components for improved maintainability.
2025-09-29 16:59:33 +01:00
Simon Larsen
e5017908ae Merge pull request #2006 from OneUptime/v4-ms-teams
feat: Add Microsoft Teams integration with configuration and action t…
2025-09-29 16:58:25 +01:00
Simon Larsen
eff87e1705 feat: Implement method to retrieve user's joined teams in Microsoft Teams integration 2025-09-29 16:50:27 +01:00
Simon Larsen
9fabdbbb2b feat: Add method to refresh teams list for a user in Microsoft Teams integration 2025-09-29 16:43:48 +01:00
Simon Larsen
56908674bb feat: Add endpoint to refresh teams list in Microsoft Teams API 2025-09-29 16:43:02 +01:00
Simon Larsen
9fe3fb041e feat: Add refresh teams functionality and loading state in Microsoft Teams integration 2025-09-29 16:35:19 +01:00
Simon Larsen
46d3bf527b feat: Update team selection titles and labels in NotificationRuleForm for clarity 2025-09-29 16:28:30 +01:00
Simon Larsen
06e11aa8d7 feat: Update card title for admin consent status in Microsoft Teams integration 2025-09-29 16:23:58 +01:00
Simon Larsen
acd9a87694 feat: Update card title for manual app installation prompt in Microsoft Teams 2025-09-29 16:21:42 +01:00
Simon Larsen
9a4f799145 feat: Remove channelCache from MicrosoftTeamsMiscData interface 2025-09-29 15:29:45 +01:00
Simon Larsen
edd067f79f feat: Add tenantId handling in Microsoft Teams request and auth token retrieval 2025-09-29 14:45:20 +01:00
Simon Larsen
36b6b8423a feat: Simplify Microsoft Teams data loading by removing unnecessary try-catch block 2025-09-29 14:41:01 +01:00
Simon Larsen
81c558dc21 feat: Refactor Microsoft Teams integration to use MicrosoftTeamsTeam type for improved type safety 2025-09-29 14:38:18 +01:00
Simon Larsen
a23d585ec6 feat: Update teamId handling in Microsoft Teams channel existence check 2025-09-29 14:32:27 +01:00
Simon Larsen
c193bd71d7 feat: Replace SlackMiscData with WorkspaceMiscData in refreshAuthToken method 2025-09-29 14:27:36 +01:00
Simon Larsen
b1791602c8 feat: Define WorkspaceMiscData type to unify Slack and Microsoft Teams miscellaneous data structures 2025-09-29 14:26:40 +01:00
Simon Larsen
e98059e4ee feat: Add optional teamId parameter to workspace channel methods for improved Microsoft Teams integration 2025-09-29 14:26:32 +01:00
Simon Larsen
371ba8f414 feat: Enhance Microsoft Teams integration by adding teamId support and fetching available teams 2025-09-29 14:20:19 +01:00
Simon Larsen
a271ba2cd9 feat: Add MicrosoftTeamsTeam interface and availableTeams property to MicrosoftTeamsMiscData 2025-09-29 14:10:26 +01:00
Simon Larsen
a337065c4f feat: Remove unused imports from MicrosoftTeamsIntegration component 2025-09-29 13:42:47 +01:00
Simon Larsen
22479a6f9e feat: Add teamId support for notification channels and update related interfaces 2025-09-29 13:41:56 +01:00
Simon Larsen
29051c3010 feat: Simplify Microsoft Teams integration by removing team selection logic and updating connection messages 2025-09-29 13:40:36 +01:00
Simon Larsen
aaa29c87ca feat: Rename microsoftTeams prop to microsoftTeamsTeams for clarity 2025-09-29 13:38:46 +01:00
Simon Larsen
0385dc7ac8 feat: Add Microsoft Teams team selection for existing and new channel creation in notification rules 2025-09-29 13:38:31 +01:00
Simon Larsen
d55e354a07 feat: Update instructions for building and compiling with proper formatting and clarity 2025-09-29 13:35:08 +01:00
Nawaz Dhandala
20716b7c7e feat: Refactor WebAuthn integration in login and user management components for improved readability and maintainability 2025-09-29 13:07:48 +01:00
Nawaz Dhandala
86143d1585 feat: Add WebAuthn support to the login page for enhanced two-factor authentication 2025-09-29 13:01:57 +01:00
Nawaz Dhandala
214915528b feat: Implement UserWebAuthn model and API for WebAuthn credential management 2025-09-29 13:00:52 +01:00
Nawaz Dhandala
9b24ff50ec feat: Enhance two-factor authentication by integrating WebAuthn support and updating fetch function to return webAuthnList 2025-09-29 12:55:33 +01:00
Nawaz Dhandala
9caeb34f63 feat: Add WebAuthn support for two-factor authentication
- Introduced new API paths for generating and verifying WebAuthn authentication options.
- Integrated UserWebAuthnAPI into the BaseAPI feature set.
- Added UserWebAuthn model to the database models.
- Implemented UserWebAuthnService for handling WebAuthn-related database operations.
- Updated the User Profile page to include functionality for managing WebAuthn security keys.
- Added UI components for registering and displaying WebAuthn security keys.
- Included necessary dependencies for WebAuthn functionality in package.json and package-lock.json.
2025-09-29 12:54:44 +01:00
Simon Larsen
368f33db24 feat: Enhance bot message handling by checking for direct messages and mentions 2025-09-29 11:40:26 +01:00
Simon Larsen
5d7a18cbe2 feat: Integrate getWorkspaceTypeDisplayName for improved workspace type display in notification rules 2025-09-29 11:21:25 +01:00
Simon Larsen
19f663d0fd feat: Simplify form card hiding by using deleteActivity method in Microsoft Teams actions 2025-09-29 11:16:49 +01:00
Simon Larsen
d857024fbe feat: Update activity text to ensure proper form card hiding in Microsoft Teams actions 2025-09-29 10:51:45 +01:00
Simon Larsen
027d766e03 feat: Hide form card after adding notes and executing actions in Microsoft Teams alert, incident, and scheduled maintenance actions 2025-09-29 10:49:19 +01:00
Simon Larsen
e7599c2202 feat: Add null handling for on-call policy cards in MicrosoftTeamsAlertActions and MicrosoftTeamsIncidentActions 2025-09-29 10:42:12 +01:00
Simon Larsen
703b525310 feat: Enhance Microsoft Teams actions with new incident and alert functionalities, including viewing and submitting notes, executing on-call policies, and changing states 2025-09-29 09:52:27 +01:00
Simon Larsen
534882bc17 feat: Add new incident note actions for viewing and submitting notes in MicrosoftTeamsIncidentActionType 2025-09-27 13:12:05 +01:00
Simon Larsen
bdb2170663 refactor: Consolidate incident action handling into MicrosoftTeamsIncidentActions class 2025-09-27 13:04:24 +01:00
Simon Larsen
dffb11b304 fix: Correct casing for incident action type values in MicrosoftTeamsIncidentActionType enum 2025-09-27 12:44:51 +01:00
Simon Larsen
4e5386fccf fix: Normalize incident action type values and improve logging in MicrosoftTeamsUtil 2025-09-27 12:09:01 +01:00
Simon Larsen
6c11fbf850 fix: Remove fallback text for accessibility in adaptive card message handling 2025-09-27 11:50:08 +01:00
Simon Larsen
f713eb7546 fix: Improve error handling for missing AAD Object ID in Teams bot invoke activity 2025-09-27 11:22:24 +01:00
Simon Larsen
0d1f07b5ae feat: Add method to extract action type and value from Teams Adaptive Card submits 2025-09-27 11:15:38 +01:00
Simon Larsen
ffcaaf213f fix: Simplify action type mapping in MicrosoftTeamsUtil and improve logging 2025-09-27 11:08:25 +01:00
Simon Larsen
b05a0619fb feat: Enhance MicrosoftTeamsUtil to support adaptive card actions and improve message handling 2025-09-27 10:59:20 +01:00
Simon Larsen
cbd4d26189 feat: Add helm template command to generate Kubernetes manifests in release workflow 2025-09-27 10:30:06 +01:00
Simon Larsen
1f24a79a8a fix: Update JSON schema to include commonConfiguration and allow additional properties for cert-manager 2025-09-27 10:24:03 +01:00
Simon Larsen
94227a103d feat: Add comprehensive JSON schema for OneUptime Helm chart configuration, defining properties for various components including global settings, database configurations, and service parameters. 2025-09-27 10:20:22 +01:00
Simon Larsen
cd1bf5befe fix: Enhance markdown handling in MicrosoftTeamsUtil to clean up links and bold markers for better rendering 2025-09-26 18:02:22 +01:00
Simon Larsen
e50e75b009 fix: Replace LIMIT_PER_PROJECT with a fixed limit of 10 for query results in MicrosoftTeams utility 2025-09-26 17:50:13 +01:00
Simon Larsen
9656fbdae4 feat: Enhance Microsoft Teams bot with new commands for active incidents, scheduled maintenance, ongoing maintenance, and active alerts 2025-09-26 17:45:24 +01:00
Simon Larsen
1004251175 fix: Update Microsoft Teams app type to single-tenant and adjust bot adapter initialization for tenant-specific handling 2025-09-26 15:08:53 +01:00
Simon Larsen
5c70aea851 fix: Update import for ConfigurationBotFrameworkAuthenticationOptions and type definition for authConfig in MicrosoftTeamsUtil 2025-09-26 15:02:51 +01:00
Simon Larsen
25d5cc2a47 refactor: Remove HomeClientUrl export from EnvironmentConfig for code clarity 2025-09-26 14:38:40 +01:00
Simon Larsen
b6802bf949 Merge branch 'master' into v4-ms-teams 2025-09-26 14:23:46 +01:00
Simon Larsen
5af14af52a feat: Add EnableWorkflow decorator to OnCallDutyPolicy models for enhanced access control 2025-09-26 14:20:47 +01:00
Nawaz Dhandala
fffe84526e refactor: Clean up logging statements in ScheduledMaintenanceService.ts for improved clarity 2025-09-25 19:46:39 +01:00
Simon Larsen
e776186070 feat: Enhance logging for scheduled maintenance notifications with detailed debug information 2025-09-25 19:45:51 +01:00
Nawaz Dhandala
0f4f974d04 refactor: Remove unnecessary blank lines in SCIM.ts and TeamMemberService.ts for improved code clarity 2025-09-25 19:35:41 +01:00
Simon Larsen
22ecea6381 Merge branch 'master' of github.com:OneUptime/oneuptime 2025-09-25 19:32:22 +01:00
Simon Larsen
d9ad2e3036 refactor: Remove ignoreHooks property from team member operations and adjust SCIM checks for root users 2025-09-25 19:32:19 +01:00
Nawaz Dhandala
42e60213c6 refactor: Improve code formatting and readability in SCIM.ts 2025-09-25 19:22:27 +01:00
Simon Larsen
6817a0759a feat: Add enablePushGroups option to SCIM configuration for enhanced group management 2025-09-25 19:21:49 +01:00
Nawaz Dhandala
963e2ffc6d refactor: Update SCIM check function signature and type annotations in TeamView and Users components 2025-09-25 16:36:25 +01:00
Nawaz Dhandala
5c2c50ecc6 refactor: Improve code formatting and readability in ScheduledMaintenanceService, TeamMemberService, TeamView, and Users components 2025-09-25 16:32:38 +01:00
Simon Larsen
1bb4e77075 Merge branch 'scim-push' 2025-09-25 16:26:30 +01:00
Simon Larsen
1f22c231d6 feat: Update SCIM error modal title for clarity on user management 2025-09-25 15:21:00 +01:00
Simon Larsen
4b3baa9ad3 feat: Add SCIM error modal to prevent user invitations when SCIM is enabled 2025-09-25 15:14:07 +01:00
Simon Larsen
503728345f feat: Implement SCIM checks in TeamView and Teams to restrict member invitations and deletions when SCIM is enabled 2025-09-25 15:11:19 +01:00
Nawaz Dhandala
958b6bf72c fix: Improve notification settings handling for scheduled maintenance updates 2025-09-25 14:40:27 +01:00
Simon Larsen
6bb5ec12f5 feat: Implement SCIM checks in TeamMemberService to prevent member invitations and deletions when SCIM is enabled 2025-09-25 14:23:30 +01:00
Simon Larsen
30b2b63b9b feat: Enhance user creation process in SCIM by integrating auto-provisioning and team assignment logic 2025-09-25 13:56:37 +01:00
Simon Larsen
6c37290f49 feat: Refactor user existence checks in SCIM team operations for improved clarity and efficiency 2025-09-25 13:26:30 +01:00
Nawaz Dhandala
b19302f8b9 feat: Refactor SCIM member handling to use SCIMMember type for improved type safety 2025-09-25 13:03:06 +01:00
Nawaz Dhandala
acd0629791 chore: update eslint and typescript-eslint dependencies to latest versions 2025-09-25 12:54:45 +01:00
Nawaz Dhandala
3fa1aebceb chore: update eslint and eslint-plugin-unused-imports dependencies
- upgraded eslint-plugin-unused-imports from 3.2.0 to 4.1.0
- downgraded eslint from 9.36.0 to 8.57.1
- updated typescript-eslint packages to use scoped package names
2025-09-25 12:51:54 +01:00
Simon Larsen
1d6a645241 chore: update dependencies for eslint and typescript
- upgraded eslint from ^8.57.0 to ^9.36.0
- upgraded typescript from ^5.8.3 to ^5.9.2
- upgraded typescript-eslint from ^8.33.1 to ^8.44.1
2025-09-25 12:47:23 +01:00
Simon Larsen
922e30f162 feat: Add MigrationName1758798730753 to schema migrations 2025-09-25 12:13:02 +01:00
Simon Larsen
73beb80056 feat: Add enablePushGroups column to ProjectSCIM for SCIM provisioning 2025-09-25 12:12:48 +01:00
Simon Larsen
72fd74862c feat: Add conditional display for Teams based on enablePushGroups setting in SCIM configuration 2025-09-25 12:02:42 +01:00
Simon Larsen
13d403ecf8 feat: Add enablePushGroups option to SCIM configuration for group provisioning 2025-09-25 12:02:29 +01:00
Simon Larsen
30bca619dd feat: Add enablePushGroups configuration to ProjectSCIM for SCIM provisioning 2025-09-25 11:59:08 +01:00
Simon Larsen
f969adba9c fix: Update Okta integration instructions for SCIM application setup 2025-09-25 11:46:58 +01:00
Simon Larsen
b42abc6e42 Merge branch 'master' into scim-push 2025-09-25 11:26:10 +01:00
Simon Larsen
7ec183f9e9 fix: Add Cert-Manager configuration instructions for Let's Encrypt in README 2025-09-25 11:18:16 +01:00
Simon Larsen
d33c739372 fix: Correct secretName assignment logic for TLS hosts in ingress template 2025-09-24 21:55:07 +01:00
Simon Larsen
d8e12daec5 fix: Update ingress and cluster-issuer templates for ACME challenge handling 2025-09-24 21:26:55 +01:00
Simon Larsen
2a54cfc527 fix: Add ACME certificate profile to ClusterIssuer configuration 2025-09-24 20:51:15 +01:00
Simon Larsen
c0241a2e20 refactor: Update Cert-Manager integration in README and templates for clarity and consistency 2025-09-24 11:32:45 +01:00
Simon Larsen
e32d7cb368 fix: Add note to enable cert-manager before Let's Encrypt configuration 2025-09-24 11:28:16 +01:00
Simon Larsen
4dec1290e8 refactor: Update cert-manager configuration to use new naming convention for Let's Encrypt settings 2025-09-24 11:21:41 +01:00
Simon Larsen
04f6493a6d Refactor code structure for improved readability and maintainability 2025-09-24 11:19:15 +01:00
Simon Larsen
f113e84aa5 fix: Remove cert-manager CRDs installation option from values.yaml 2025-09-24 11:09:45 +01:00
Simon Larsen
57f764b92a refactor: Update cert-manager configuration to use index for improved clarity 2025-09-24 11:03:03 +01:00
Simon Larsen
c4c7d10d16 Add ClusterIssuer configuration for cert-manager with Let's Encrypt support
- Introduced a new template for ClusterIssuer in the Helm chart.
- Configured ACME server and email for Let's Encrypt.
- Set up HTTP01 solver with ingress class from values.
2025-09-24 10:47:34 +01:00
Simon Larsen
00ba94f372 feat: Include status page ID in SEO response for improved tracking 2025-09-24 10:35:52 +01:00
Simon Larsen
f85f41ffa9 fix: Ensure non-null assertion for repeatableJobs when adding job 2025-09-23 21:45:51 +01:00
Simon Larsen
21dfcdfa63 Merge pull request #2017 from OneUptime/queue-readd
feat: Implement repeatable job handling on queue reconnection; add lo…
2025-09-23 21:13:12 +01:00
Nawaz Dhandala
c818decfc8 refactor: Enhance type definitions and error handling in reconnect listener setup 2025-09-23 21:04:55 +01:00
Simon Larsen
1386fef470 refactor: Extract reconnect listener setup into a separate method for improved readability and error handling 2025-09-23 21:01:25 +01:00
Simon Larsen
4c0cbc17a2 refactor: Update queue event listener to use async IIFE for improved readability and reliability 2025-09-23 20:53:38 +01:00
Simon Larsen
3c42447b41 Merge branch 'master' into queue-readd 2025-09-23 20:44:26 +01:00
Simon Larsen
e31f616dc1 Merge pull request #2016 from OneUptime/sp-rss
Sp rss
2025-09-23 20:39:23 +01:00
Nawaz Dhandala
cfbb65f7ae refactor: Replace inline type definitions with RSSItem type for improved readability and consistency 2025-09-23 20:31:54 +01:00
Nawaz Dhandala
52c42dae1e refactor: Enhance type annotations for getStatusPageData and handleRSS functions; streamline code structure 2025-09-23 20:30:21 +01:00
Simon Larsen
d838d377a0 feat: Add HomeClientUrl to EnvironmentConfig and update RSS feed URL generation to use HttpProtocol 2025-09-23 20:22:56 +01:00
Simon Larsen
3a6f8b4c95 feat: Add atom link and guid to RSS feed XML generation 2025-09-23 20:17:37 +01:00
Simon Larsen
1039bd9f0b feat: Implement RSS feed handling and status page data retrieval logic 2025-09-23 20:16:20 +01:00
Nawaz Dhandala
5f84c7195c feat: Implement repeatable job handling on queue reconnection; add logging for job re-addition 2025-09-23 20:13:54 +01:00
Nawaz Dhandala
9597f66ab1 refactor: Improve type annotations for TeamComplianceStatusTable component; remove unnecessary line in SendStateChangeNotification 2025-09-23 19:37:16 +01:00
Simon Larsen
1562f8ee6a fix: Update @oneuptime/common and axios versions in package-lock.json; modify compile command to include npm update 2025-09-23 19:34:01 +01:00
Simon Larsen
11b0477cd6 fix: Add validation for projectId and scheduledMaintenanceId in getDashboardUrl method; include projectId in scheduled maintenance selection 2025-09-23 19:30:41 +01:00
Simon Larsen
174694e040 fix: Update axios version in package-lock.json and clean up unused imports in TeamComplianceStatusTable and TeamView 2025-09-23 19:24:12 +01:00
Simon Larsen
f5664116b9 Merge branch 'release' of github.com:OneUptime/oneuptime into release 2025-09-23 18:14:41 +01:00
Simon Larsen
0eb502b77a Merge branch 'master' into release 2025-09-23 18:14:20 +01:00
Nawaz Dhandala
1460521dc0 fix: Add type annotation for complianceStatusTableRef in TeamView 2025-09-23 18:14:03 +01:00
Nawaz Dhandala
e73c8bca16 refactor: Improve code readability by formatting user name assignment and useImperativeHandle in TeamComplianceStatusTable and TeamView 2025-09-23 18:10:43 +01:00
Simon Larsen
a0fef8df3d feat: Update notification method labels for clarity in TeamView 2025-09-23 17:16:43 +01:00
Simon Larsen
1821377dfa feat: Add refresh functionality to TeamComplianceStatusTable and integrate with TeamView 2025-09-23 17:13:22 +01:00
Simon Larsen
ac2c501058 feat: Refactor TeamComplianceStatusTable rendering and integrate Card component for better UI structure 2025-09-23 17:04:28 +01:00
Simon Larsen
3ce3d1ee65 feat: Improve loading and empty state handling in TeamComplianceStatusTable 2025-09-23 17:00:52 +01:00
Simon Larsen
ad17e49177 feat: Enhance user name retrieval in compliance status to include email as fallback 2025-09-23 16:58:50 +01:00
Simon Larsen
18389a0c31 feat: Update ComplianceRuleType enum values for consistency and clarity 2025-09-23 13:19:49 +01:00
Nawaz Dhandala
9d04975759 feat: Update migration for ruleType column in TeamComplianceSetting and improve notification rule labels in TeamView 2025-09-23 13:14:13 +01:00
Simon Larsen
3bb9b3d78b feat: Replace migration for ruleType column in TeamComplianceSetting and update OnCallDutyPolicyScheduleLayer defaults 2025-09-23 13:13:35 +01:00
Simon Larsen
ddfae282e6 Merge branch 'team-compliacne-rules' 2025-09-23 13:09:56 +01:00
Simon Larsen
02663bb33a feat: Change ruleType column type to LongText for improved compliance rule descriptions 2025-09-23 13:08:35 +01:00
Simon Larsen
6cb1a49128 feat: Update notification rule labels in TeamView for clarity 2025-09-23 13:07:53 +01:00
Nawaz Dhandala
4bcddf860c feat: Enhance TeamComplianceAPI and TeamComplianceService to include user compliance statuses and improve type safety 2025-09-23 13:05:34 +01:00
Nawaz Dhandala
d8d4593d38 feat: Refactor code for improved readability and maintainability across multiple files 2025-09-23 12:57:28 +01:00
Simon Larsen
f76381525f Merge pull request #2015 from OneUptime/team-compliacne-rules
Team compliance rules
2025-09-23 12:55:47 +01:00
Simon Larsen
91abb2318b feat: Add migration for unique index on teamId and ruleType in TeamComplianceSetting and update OnCallDutyPolicyScheduleLayer defaults 2025-09-23 12:55:14 +01:00
Simon Larsen
3a9e695336 feat: Add unique index on teamId and ruleType in TeamComplianceSetting and implement validation in TeamComplianceSettingService 2025-09-23 12:54:25 +01:00
Simon Larsen
e270c5d70a feat: Enhance compliance checks by aggregating missing notification rules for incident and alert severities 2025-09-23 12:45:27 +01:00
Simon Larsen
b40e88e8ec feat: Add userProfilePictureId to UserComplianceStatus and update rendering in TeamComplianceStatusTable 2025-09-23 12:38:26 +01:00
Simon Larsen
2173ed288a feat: Refactor TeamComplianceStatusTable to use LocalTable component for improved rendering 2025-09-23 12:28:44 +01:00
Simon Larsen
82669c8f23 feat: Update API endpoint for fetching team compliance status and add common headers 2025-09-23 12:25:51 +01:00
Simon Larsen
b093a730ab feat: Add migration for TeamComplianceSetting table and related constraints 2025-09-23 12:15:20 +01:00
Simon Larsen
8700068468 feat: Enhance compliance checks by integrating incident and alert severity notification rules 2025-09-23 12:08:47 +01:00
Simon Larsen
13e322944b feat: Implement TeamComplianceService for managing team compliance status and user notifications 2025-09-23 12:02:11 +01:00
Simon Larsen
62aecc6e9f feat: Add Team Compliance Settings and Status tables to TeamView 2025-09-23 12:00:13 +01:00
Nawaz Dhandala
61eca28545 Merge branch 'release' of https://github.com/OneUptime/oneuptime into release 2025-09-23 11:56:14 +01:00
Nawaz Dhandala
c289027efc fix: Implement retry mechanism for npm prerun command in workflow files to enhance reliability 2025-09-23 11:55:46 +01:00
Simon Larsen
ecdae56ffa feat: Add TeamComplianceStatusTable component for displaying team compliance status 2025-09-23 11:49:40 +01:00
Nawaz Dhandala
005536633c fix: Implement retry mechanism for Dockerfile generation to enhance reliability 2025-09-23 11:48:52 +01:00
Simon Larsen
d1ec1d6936 feat: Implement TeamComplianceAPI for retrieving team compliance status 2025-09-23 11:48:07 +01:00
Simon Larsen
6f68629f29 feat: Add TeamComplianceAPI to BaseAPIFeatureSet for compliance management 2025-09-23 11:46:18 +01:00
Simon Larsen
8af4bece10 feat: Add TeamComplianceSetting model and service with compliance rule types 2025-09-23 11:25:16 +01:00
Simon Larsen
18a0d6ab26 Merge branch 'master' into v4-ms-teams 2025-09-22 12:58:52 +01:00
Simon Larsen
6d73bb8a12 Merge branch 'release' of github.com:OneUptime/oneuptime into release 2025-09-22 12:56:31 +01:00
Simon Larsen
85b0f47be1 feat: Add Code of Conduct page and link in legal section 2025-09-22 12:55:41 +01:00
Simon Larsen
cc3596fc8e fix: Enhance adaptive card sending via Bot Framework with improved error handling and accessibility features 2025-09-22 11:26:02 +01:00
Simon Larsen
654367dbd8 fix: Refactor bot user ID handling and ensure fallback for undefined user ID in message sending 2025-09-22 10:44:08 +01:00
Simon Larsen
e5ccdc1a56 fix: Enhance access token expiration handling in Microsoft Teams utility 2025-09-22 10:33:27 +01:00
Simon Larsen
84389212f6 fix: Update Microsoft Teams app version to 1.1.0 in API manifest 2025-09-21 19:52:10 +01:00
Simon Larsen
5b01c202eb fix: Update Microsoft Teams API permissions for channel message and creation handling 2025-09-21 19:47:42 +01:00
Simon Larsen
153ee4fc20 fix: Update Microsoft Teams API permissions to include ChannelMessage.Send and additional channel-related permissions 2025-09-21 16:56:57 +01:00
Simon Larsen
03e176794e fix: Update Microsoft Teams app version to 2.1.0 and enhance permissions for message team members 2025-09-21 16:08:46 +01:00
Simon Larsen
9783f4897c fix: Correct indentation for Group.Read.All permission in Microsoft Teams integration documentation 2025-09-21 16:03:39 +01:00
Simon Larsen
928f6457bc fix: Update Microsoft Teams API to use Teams API for fetching available teams and add documentation for Group.Read.All permission 2025-09-21 16:00:39 +01:00
Simon Larsen
b628bd3ad1 fix: Enhance Microsoft Teams integration by updating project connection logic and handling admin consent state 2025-09-21 15:46:29 +01:00
Simon Larsen
dba0d69f63 fix: Update admin consent installation instructions for clarity in Microsoft Teams integration 2025-09-21 15:34:12 +01:00
Simon Larsen
64c203259a fix: Move admin consent card rendering to the bottom for improved UI flow in Microsoft Teams integration 2025-09-21 15:32:06 +01:00
Simon Larsen
25ec5c8df0 fix: Update admin consent button titles for clarity in Microsoft Teams integration 2025-09-21 15:31:46 +01:00
Simon Larsen
22270c62f4 fix: Remove admin consent reminder UI when consent is completed in Microsoft Teams integration 2025-09-21 15:31:14 +01:00
Simon Larsen
bb7176252c fix: Ensure loading state is updated correctly in error handling of Microsoft Teams integration 2025-09-21 15:30:53 +01:00
Simon Larsen
97f62b1458 fix: Remove unnecessary return statement in error handling of Microsoft Teams integration 2025-09-21 15:28:13 +01:00
Simon Larsen
5eb333ccfc feat: Update admin consent handling in Microsoft Teams integration 2025-09-21 15:15:33 +01:00
Simon Larsen
bd05afb0a7 feat: Implement admin consent handling and UI updates in Microsoft Teams integration 2025-09-21 14:57:53 +01:00
Simon Larsen
96e6780e7b feat: Enhance project auth token handling in Microsoft Teams integration 2025-09-21 14:54:06 +01:00
Simon Larsen
5e8ed144ae feat: Update title for manual app installation in Microsoft Teams integration 2025-09-21 14:39:27 +01:00
Simon Larsen
588ff245ec feat: Add admin consent callback URL to Microsoft Teams integration documentation 2025-09-21 14:38:17 +01:00
Simon Larsen
02cef807ea feat: Add scope parameter to admin consent URL for Microsoft Teams integration 2025-09-21 14:36:23 +01:00
Simon Larsen
041ffdfbe0 Merge pull request #2014 from OneUptime/master
Release
2025-09-21 14:31:24 +01:00
Simon Larsen
e2c362a5b3 feat: Add admin consent flow for Microsoft Teams integration with callback handling 2025-09-21 13:13:37 +01:00
Simon Larsen
000ff4ad45 refactor: Update API post call to use object syntax for improved clarity 2025-09-21 12:52:54 +01:00
Simon Larsen
064e16cc6f refactor: Improve API call syntax and remove unnecessary comments for better readability 2025-09-21 12:37:10 +01:00
Simon Larsen
fbc6b8fa48 fix: Correct typos and improve clarity in Microsoft Teams integration documentation 2025-09-20 10:05:01 +01:00
Simon Larsen
4e0935873d Merge branch 'master' into v4-ms-teams 2025-09-20 09:50:20 +01:00
Nawaz Dhandala
35a40a431e fix: Correct balance display logic to ensure accurate message for negative balances 2025-09-20 09:24:02 +01:00
Simon Larsen
d24c245b4a refactor: Update API post calls to use structured object syntax for improved readability 2025-09-20 09:23:11 +01:00
Simon Larsen
99f59f2f1e refactor: Update API call to use object destructuring for contributors request 2025-09-20 09:20:11 +01:00
Simon Larsen
c558eb578f refactor: Update API call syntax to use object destructuring for improved readability 2025-09-19 23:01:16 +01:00
Simon Larsen
97380a5410 Merge branch 'master' into v4-ms-teams 2025-09-19 22:58:45 +01:00
Simon Larsen
462f40680e fix: Update balance display logic to show message for negative balances 2025-09-19 22:58:16 +01:00
Simon Larsen
8a11dbe35b fix: Change sort order of createdAt to descending in StatusPageAPI 2025-09-19 22:47:34 +01:00
Nawaz Dhandala
d2c1467a07 refactor: Simplify API call syntax and improve code readability across multiple files 2025-09-19 22:37:25 +01:00
Simon Larsen
ff57061190 Refactor API calls to use new request structure
- Updated API.post and API.get calls across multiple components to use the new object structure for requests, including specifying `url`, `data`, and `headers` explicitly.
- This change improves code readability and consistency in how API requests are made throughout the application.
2025-09-19 22:36:05 +01:00
Simon Larsen
63f1034f4a refactor: Update API calls to use object destructuring for parameters in multiple components 2025-09-19 22:19:50 +01:00
Simon Larsen
87ddec9e6c refactor: Update API method calls to use object destructuring for parameters 2025-09-19 22:15:02 +01:00
Simon Larsen
b2ea52e549 refactor: Simplify API method signatures by consolidating parameters into a single options object 2025-09-19 22:01:25 +01:00
Simon Larsen
92bb753cbf fix: Update API call in Billing settings to use undefined for data parameter 2025-09-19 21:51:36 +01:00
Simon Larsen
7428e75643 refactor: Remove inheritance from BaseAPI and restructure BillingAPI class 2025-09-19 21:45:29 +01:00
Simon Larsen
0a2ed040f0 feat: Add discountPercent field to Project model and corresponding migration 2025-09-19 21:33:48 +01:00
Simon Larsen
ded0aba399 feat: Implement customer balance retrieval in BillingAPI and update Billing settings page to display balance 2025-09-19 21:25:52 +01:00
Simon Larsen
0658248535 refactor: Remove dark mode styles from BarChart and LineChart components for consistency 2025-09-19 21:10:41 +01:00
Simon Larsen
6cf2d842ad Merge branch 'master' of github.com:OneUptime/oneuptime 2025-09-19 21:02:46 +01:00
Simon Larsen
4a6bf95ef3 refactor: Simplify styles in LogsViewer and LegendItem components for cleaner UI 2025-09-19 21:02:41 +01:00
Nawaz Dhandala
af7835fc8a refactor: Improve code formatting and readability in various service and utility files 2025-09-19 20:58:32 +01:00
Nawaz Dhandala
5e7ed2be73 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-09-19 20:57:56 +01:00
Nawaz Dhandala
11894d0ba5 Refactor API calls to use unified request structure
- Updated MicrosoftTeams and Slack classes to use the new API.post structure with url and data properties.
- Refactored API utility methods to accept a single options object for HTTP requests, improving consistency across the codebase.
- Modified various service and utility classes (e.g., CopilotPullRequestService, CodeRepositoryUtil, and ApiMonitor) to align with the new API request format.
- Enhanced error handling and logging for API responses.
- Updated tests to accommodate changes in API method signatures.
2025-09-19 20:57:54 +01:00
Simon Larsen
8bb92bbeb3 docs: Clarify server restart instructions in Microsoft Teams integration documentation 2025-09-19 15:58:58 +01:00
Simon Larsen
a50af21ce0 docs: Revise Microsoft Teams integration documentation for app manifest upload steps and server restart instructions 2025-09-19 15:57:55 +01:00
Simon Larsen
df29ed2b78 docs: Update Microsoft Teams integration documentation to clarify app registration details and configuration steps 2025-09-19 15:44:39 +01:00
Simon Larsen
58cba788fc docs: Update Microsoft Teams integration documentation to correct bot service creation steps 2025-09-19 15:37:02 +01:00
Simon Larsen
760044f603 docs: Add clarification on secret value usage in Microsoft Teams integration documentation 2025-09-19 15:36:08 +01:00
Simon Larsen
fb099a1c3e refactor: Remove redundant notes on Microsoft Teams permissions for clarity 2025-09-19 15:35:41 +01:00
Simon Larsen
32692bc939 docs: Enhance Microsoft Teams integration documentation with detailed permissions information 2025-09-19 15:35:21 +01:00
Simon Larsen
c514b9cb74 refactor: Update Microsoft Teams integration documentation for clarity and accuracy 2025-09-19 15:17:12 +01:00
Simon Larsen
5d44943958 fix: Update Microsoft Teams app registration details for clarity and accuracy 2025-09-19 15:15:44 +01:00
Simon Larsen
df4138e5d3 Merge branch 'master' into v4-ms-teams 2025-09-19 14:56:04 +01:00
Simon Larsen
51777709f3 Revert "refactor: Enhance email template styles and structure for improved readability and aesthetics"
This reverts commit 43b6fdf45d.
2025-09-19 13:47:49 +01:00
Simon Larsen
f7416bb0a6 feat: Add alertId and incidentId to notification payloads for enhanced tracking 2025-09-19 12:46:53 +01:00
Simon Larsen
43b6fdf45d refactor: Enhance email template styles and structure for improved readability and aesthetics 2025-09-19 12:35:44 +01:00
Simon Larsen
818995a6ae feat: enhance bot authentication configuration for multi-tenant Microsoft Teams apps 2025-09-18 21:02:48 +01:00
Simon Larsen
38fa1ba9f8 refactor: streamline message handling by utilizing TurnContext for sending messages in Microsoft Teams 2025-09-18 20:51:48 +01:00
Simon Larsen
b862ac113f feat: Refactor Bot Framework activity handling to use botbuilder SDK's adapter for improved processing 2025-09-18 20:40:57 +01:00
Simon Larsen
a2526a5a99 refactor: change logging level from info to debug for bot message operations 2025-09-18 20:22:18 +01:00
Simon Larsen
4124e7e9f7 chore: add botbuilder dependency to Common package.json 2025-09-18 20:16:51 +01:00
Simon Larsen
05d3e73b83 docs: Update Microsoft Teams integration documentation for bot messaging endpoint configuration 2025-09-18 20:10:21 +01:00
Simon Larsen
907f0149d8 Merge branch 'master' into v4-ms-teams 2025-09-18 16:56:57 +01:00
Simon Larsen
54f4c63a51 feat: Add dynamic loading of Mermaid.js for rendering diagrams in blog posts 2025-09-18 16:56:27 +01:00
Simon Larsen
80388adb59 refactor: Implement JWT validation for Bot Framework authentication in MicrosoftTeamsAPI 2025-09-17 21:23:32 +01:00
Simon Larsen
ca0792a7f7 refactor: Enhance Bot Framework authentication validation and access token retrieval logging 2025-09-17 21:15:40 +01:00
Simon Larsen
7c1e42cff9 refactor: Add validation for Bot Framework service URLs and implement access token retrieval method 2025-09-17 21:09:08 +01:00
Simon Larsen
e9904c0e74 refactor: Move Bot Framework activity handlers to MicrosoftTeamsUtil for better organization and maintainability 2025-09-17 20:58:00 +01:00
Simon Larsen
dd9d296e62 refactor: Enhance logging and add test endpoint for Bot Framework integration in MicrosoftTeamsAPI 2025-09-17 20:54:50 +01:00
Simon Larsen
c0005618fe refactor: Implement Bot Framework messaging endpoint and activity handlers in MicrosoftTeamsAPI 2025-09-17 20:47:11 +01:00
Simon Larsen
feb503b448 refactor: Update image path handling and fix URL formatting in MicrosoftTeamsIntegration 2025-09-17 20:32:29 +01:00
Simon Larsen
d2c411c9d6 refactor: Simplify icon path handling by removing fallback checks in MicrosoftTeamsAPI 2025-09-17 20:26:59 +01:00
Simon Larsen
562e930ca0 refactor: Replace fs with LocalFile utility for icon file handling in MicrosoftTeamsAPI 2025-09-17 20:26:13 +01:00
Simon Larsen
e97b2904e8 refactor: Simplify icon handling logic by removing fallback checks for Microsoft Teams icons 2025-09-17 16:47:01 +01:00
Simon Larsen
fd07d70575 refactor: Update Teams app manifest version and improve icon handling logic 2025-09-17 16:44:35 +01:00
Simon Larsen
3e6c8b3f68 refactor: Remove fallback for MicrosoftTeamsAppClientId and add packageName to manifest 2025-09-17 16:23:26 +01:00
Simon Larsen
ad772c4a5b refactor: Remove fallback for MicrosoftTeamsAppClientId in webApplicationInfo 2025-09-17 16:22:10 +01:00
Simon Larsen
5396c0c9cf refactor: Specify type for archive variable in MicrosoftTeamsAPI 2025-09-17 16:13:56 +01:00
Simon Larsen
c6e6ae5be0 refactor: Update Microsoft Teams app manifest version and improve icon directory handling 2025-09-17 16:13:32 +01:00
Simon Larsen
82397fec5a refactor: Update Teams app manifest versioning logic and add client ID to docker-compose 2025-09-17 16:04:32 +01:00
Simon Larsen
6301e24c02 refactor: Add Microsoft Teams app client ID and secret to configuration files 2025-09-17 15:37:03 +01:00
Simon Larsen
23b3a4d9dd refactor: Add Microsoft Teams app configuration to environment and docker-compose files 2025-09-17 15:34:50 +01:00
Simon Larsen
e6c158b2b5 refactor: Use AppVersion for Teams app manifest versioning 2025-09-17 15:30:43 +01:00
Simon Larsen
5a4f2e0744 refactor: Add validation for Microsoft Teams App Client ID in getTeamsAppManifest method 2025-09-17 15:29:34 +01:00
Simon Larsen
545335a74d refactor: Update nodemon.json files to ignore node_modules and public directories 2025-09-17 14:50:58 +01:00
Simon Larsen
5ab49052fe Merge branch 'master' into v4-ms-teams 2025-09-17 14:22:25 +01:00
Simon Larsen
2985b7675d refactor: Remove unused Button import and clean up API call parameters in SlackChannelCacheModal component 2025-09-17 14:01:18 +01:00
Simon Larsen
49cd04c5d5 refactor: Simplify channel cache management by replacing rows with a dictionary component in SlackChannelCacheModal 2025-09-17 13:54:56 +01:00
Simon Larsen
2a23d1a962 refactor: Update API call to include common headers and remove unnecessary modal properties in SlackChannelCacheModal component 2025-09-17 13:48:56 +01:00
Nawaz Dhandala
13f19adc13 refactor: Enhance type annotations and improve code clarity in SlackAPI, CopyTextButton, LogItem, LogsViewer, and SlackChannelCacheModal components 2025-09-17 13:20:05 +01:00
Nawaz Dhandala
3e79bbf55f refactor: Improve code formatting and readability across multiple components 2025-09-17 13:10:30 +01:00
Nawaz Dhandala
b400e89a3b Merge branch 'release' 2025-09-17 13:09:59 +01:00
Nawaz Dhandala
fc8362e7e5 feat: Implement SlackChannelCacheModal for viewing and editing cached Slack channels 2025-09-17 13:09:29 +01:00
Nawaz Dhandala
8b209e82d5 feat: Add endpoint to fetch and cache all Slack channels for the current tenant's project 2025-09-17 13:08:09 +01:00
Simon Larsen
6fb4fdf698 Merge pull request #2009 from OneUptime/better-log-ui
refactor: Enhance LogsViewer with improved scroll handling and UI upd…
2025-09-17 13:04:07 +01:00
Simon Larsen
9551e64b16 refactor: Replace SVG icons with Icon component for consistency in CopyTextButton and LogItem components 2025-09-17 12:58:36 +01:00
Simon Larsen
232f938d2c refactor: Update log message styling for improved readability in LogItem component 2025-09-17 12:51:25 +01:00
Simon Larsen
0dc24b36e6 refactor: Consolidate copy button logic for log message display in LogItem component 2025-09-17 12:46:38 +01:00
Simon Larsen
a77eaf214f refactor: Remove wrapLines toggle and associated UI elements for a cleaner toolbar in LogsViewer component 2025-09-17 12:44:45 +01:00
Simon Larsen
5ab08fbfcb refactor: Simplify click handling for collapse button in LogItem component 2025-09-17 12:34:16 +01:00
Simon Larsen
a73c2eb32c refactor: Update CopyTextButton integration in LogItem component with improved props and styling 2025-09-17 12:31:19 +01:00
Nawaz Dhandala
8f9e6d5bec refactor: Increase maxPages limit for improved pagination in SlackUtil 2025-09-17 12:30:34 +01:00
Nawaz Dhandala
790e26d608 refactor: Increase limit for faster searches in requestBody 2025-09-17 12:30:22 +01:00
Simon Larsen
79b96bcce8 refactor: Simplify severity badge handling and improve log body rendering in LogItem component 2025-09-17 12:22:27 +01:00
Simon Larsen
592a7f893e refactor: Enhance LogItem and LogsViewer components with improved styling and functionality 2025-09-17 12:18:48 +01:00
Simon Larsen
7408f9c204 Merge pull request #2008 from OneUptime/snyk-fix-649148701c5ddf7371d98302b082212e
[Snyk] Security upgrade axios from 1.7.7 to 1.12.0
2025-09-17 11:47:38 +01:00
Nawaz Dhandala
307de42434 feat: Add generateGroupsListResponse function for SCIM group list responses 2025-09-17 11:28:41 +01:00
Nawaz Dhandala
d1ae7f67c4 refactor: Adjust indentation for @CaptureSpan() annotation in updateChannelCache method 2025-09-17 11:07:30 +01:00
Simon Larsen
b4616885b2 refactor: Implement bulk channel cache updates and improve cache handling in SlackUtil 2025-09-17 11:05:50 +01:00
Nawaz Dhandala
7fb7f3719f refactor: Update lastUpdated timestamp format and clean up code style in SlackUtil 2025-09-17 10:56:59 +01:00
Nawaz Dhandala
d1d1d1935d refactor: Enhance LogsViewer with improved scroll handling and UI updates 2025-09-17 10:55:05 +01:00
Simon Larsen
5b54f66821 refactor: Add projectId parameter to getAllWorkspaceChannels and implement bulk cache update 2025-09-17 10:54:04 +01:00
Simon Larsen
e99a954387 refactor: Add projectId parameter to getAllWorkspaceChannels method 2025-09-17 10:50:40 +01:00
Simon Larsen
93fd9ce3cc refactor: Limit API pagination to maxPages and adjust channel search limit 2025-09-17 10:46:26 +01:00
Simon Larsen
3a3e510b11 refactor: Exclude archived channels in Slack API requests 2025-09-17 10:43:44 +01:00
Simon Larsen
cbaf1edf89 refactor: Increase maxPages limit for API search in SlackUtil 2025-09-17 10:41:59 +01:00
snyk-bot
94b21b5fc3 fix: Common/package.json & Common/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-AXIOS-12613773
2025-09-17 01:44:58 +00:00
Simon Larsen
f60cc4c199 fix: Correct configurationUrl formatting in MicrosoftTeamsAPI 2025-09-16 21:37:24 +01:00
Simon Larsen
81b1f120ee refactor: Remove manifest state and props from MicrosoftTeamsIntegration and MicrosoftTeamsIntegrationDocumentation components 2025-09-16 21:30:49 +01:00
Simon Larsen
48a0036023 feat: Add manual app installation instructions and download option for app manifest in MicrosoftTeamsIntegration 2025-09-16 21:25:34 +01:00
Simon Larsen
9925a6a5a5 feat: Update MicrosoftTeamsAPI and EnvironmentConfig to improve app manifest handling and add HomeClientUrl 2025-09-16 21:17:51 +01:00
Simon Larsen
e5ca287dcb feat: Update MicrosoftTeamsAPI to read pre-resized icons from the filesystem instead of using placeholder images 2025-09-16 21:12:51 +01:00
Simon Larsen
1a5962c430 feat: Refactor token validation in MicrosoftTeamsUtil to enhance error handling and refresh logic 2025-09-16 21:05:55 +01:00
Simon Larsen
ba5a664fca feat: Update MicrosoftTeamsAPI to disable notification-only mode and extract tenant ID from JWT for improved token validation 2025-09-16 21:05:43 +01:00
Simon Larsen
f7d05c642f feat: Refactor MicrosoftTeams integration in WorkspaceUtil to use MicrosoftTeamsUtil and include projectId in user name retrieval 2025-09-16 20:52:20 +01:00
Simon Larsen
877810219c feat: Enhance MicrosoftTeamsUtil with detailed debug logging for access token management and message sending 2025-09-16 20:48:56 +01:00
Simon Larsen
fed8c9a261 feat: Add detailed debug logging for message sending in MicrosoftTeamsUtil 2025-09-16 20:43:25 +01:00
Simon Larsen
896d36b16b feat: Add detailed logging for channel retrieval and message posting in MicrosoftTeamsUtil 2025-09-16 20:41:21 +01:00
Simon Larsen
a9c6d565f4 feat: Implement access token refresh mechanism in MicrosoftTeamsUtil for improved token management 2025-09-16 20:39:33 +01:00
Simon Larsen
ae58a33456 refactor: Improve debug logging format for channel search in SlackUtil 2025-09-16 18:17:34 +01:00
Simon Larsen
d689d7a12a feat: Enhance MicrosoftTeamsUtil with valid access token retrieval and update interfaces for additional data fields 2025-09-16 16:44:24 +01:00
Simon Larsen
f28ada8994 fix: Add undefined parameter to API calls in MicrosoftTeamsUtil for consistency 2025-09-16 16:33:34 +01:00
Nawaz Dhandala
4d9de1d326 refactor: Add debug logging for channel search in SlackUtil 2025-09-16 14:42:28 +01:00
Simon Larsen
405b28ee91 feat: Pass workspaceType to query in WorkspaceNotificationRuleTable component 2025-09-16 14:33:11 +01:00
Simon Larsen
245aeac4a9 refactor: Remove Coming Soon component and update Microsoft Teams connection messages for consistency 2025-09-16 14:16:56 +01:00
Nawaz Dhandala
6b8f8db991 feat: add archiver package and update Microsoft Teams integration components
- Added `archiver` package to `package.json` and `package-lock.json`.
- Refactored `MicrosoftTeamsIntegration.tsx` for improved readability and consistency.
- Updated error handling and state management in `MicrosoftTeamsIntegration`.
- Enhanced documentation component for Microsoft Teams integration.
- Cleaned up formatting and structure in `MicrosoftTeamsIntegrationDocumentation.tsx`.
- Adjusted settings page to utilize the updated Microsoft Teams integration component.
2025-09-16 14:08:40 +01:00
Simon Larsen
6c06c6682a refactor: Update team selection UI layout and button styles for improved alignment 2025-09-16 14:02:01 +01:00
Simon Larsen
f48da38ae1 feat: Replace button elements with reusable Button component for team selection UI 2025-09-16 13:55:47 +01:00
Simon Larsen
29de1dfbb2 refactor: Remove team search functionality and update team selection UI 2025-09-16 13:53:59 +01:00
Simon Larsen
9063bd145f feat: Replace team selection buttons with radio buttons for improved UX 2025-09-16 13:50:55 +01:00
Simon Larsen
436a393cb1 refactor: Enhance team selection UI with improved styling and search functionality 2025-09-16 13:46:53 +01:00
Simon Larsen
a7f6e264f5 feat: Implement team selection UI for Microsoft Teams integration with search functionality 2025-09-16 13:30:10 +01:00
Simon Larsen
777c2a36bf refactor: Remove optional serviceCatalogId from MicrosoftTeamsMiscData interface 2025-09-16 12:23:53 +01:00
Simon Larsen
c005448103 feat: Update Microsoft Teams API scopes to include User.Read permission 2025-09-16 12:05:03 +01:00
Simon Larsen
54f7be2c62 feat: Update Microsoft Teams OAuth flow to use static redirect URI with state parameter for projectId and userId 2025-09-16 11:53:26 +01:00
Simon Larsen
1f52b91bc5 refactor: Simplify Microsoft Teams integration page by removing unnecessary components and structure 2025-09-16 11:45:14 +01:00
Simon Larsen
d54bafd5d1 Merge branch 'master' of github.com:OneUptime/oneuptime into v4-ms-teams 2025-09-16 11:20:37 +01:00
Simon Larsen
7728e84f34 Merge pull request #2007 from OneUptime/span-fix
refactor: Enhance root span detection logic in TraceExplorer component
2025-09-16 11:15:37 +01:00
Simon Larsen
a5808eac5e refactor: Enhance root span detection logic in TraceExplorer component 2025-09-16 11:15:13 +01:00
Simon Larsen
5fdb54a8d9 feat: Implement Microsoft Teams API with endpoints for app manifest, OAuth, and webhooks 2025-09-16 10:47:09 +01:00
Simon Larsen
f02a4de88d feat: Refactor channel ID handling to include team ID and improve channel info retrieval from Microsoft Graph API 2025-09-16 10:38:18 +01:00
Simon Larsen
49b2999d4f feat: Enhance Microsoft Teams integration with new messaging and channel management features 2025-09-16 10:33:59 +01:00
Simon Larsen
60c1caa11f feat: Add Microsoft Teams integration component with authentication handling 2025-09-16 10:26:40 +01:00
Simon Larsen
3eb21895d8 feat: Implement Microsoft Teams authentication actions and request handling 2025-09-16 10:26:13 +01:00
Simon Larsen
2d9527e94d feat: Add Microsoft Teams integration documentation component 2025-09-16 10:10:55 +01:00
Simon Larsen
9f26e6f75d feat: Update Microsoft Teams incident action types to use specific incident action constants 2025-09-16 09:59:25 +01:00
Simon Larsen
7890698f7d feat: Refactor scheduled maintenance action handling to use constants for action types 2025-09-16 09:58:55 +01:00
Simon Larsen
e738db0b24 feat: Add Microsoft Teams action handlers for alerts, incidents, monitors, and on-call duties; update scheduled maintenance action types 2025-09-16 09:48:25 +01:00
Simon Larsen
67524ee869 feat: Implement Microsoft Teams scheduled maintenance actions and update alert action types 2025-09-16 09:34:49 +01:00
Simon Larsen
0e5c0d1509 feat: Add Microsoft Teams integration with configuration and action types 2025-09-16 09:32:43 +01:00
Simon Larsen
cdff9338d9 refactor: Simplify stats initialization and update logic in TraceExplorer component 2025-09-15 18:50:46 +01:00
Simon Larsen
074926ee41 Merge pull request #2005 from OneUptime/on-call-loop
On call loop
2025-09-15 18:35:29 +01:00
Nawaz Dhandala
b0ef6e23a3 refactor: Improve readability and structure in LayerUtil's getEvents method 2025-09-15 18:35:08 +01:00
Simon Larsen
1ab6bc5af9 test: Enhance overnight window test to validate distinct segments across midnight 2025-09-15 18:18:22 +01:00
Simon Larsen
04dba20871 feat: Implement weekly restrictions handling in LayerUtil and add corresponding tests 2025-09-15 18:15:13 +01:00
Simon Larsen
1cd5c927f6 feat: Add comprehensive tests for LayerUtil including daily restrictions and rotation handoff scenarios 2025-09-15 18:10:35 +01:00
Simon Larsen
6513222c3b feat: Add support for overnight event windows in getEventsByDailyRestriction method 2025-09-15 18:09:07 +01:00
Simon Larsen
498abd5251 fix: Update error handling to exclude status 400 for network-related errors 2025-09-15 17:21:48 +01:00
Nawaz Dhandala
fc46a81eb8 refactor: Improve code readability and consistency in various components 2025-09-15 12:41:12 +01:00
Simon Larsen
c3fb6e9f32 Merge branch 'master' of github.com:OneUptime/oneuptime 2025-09-15 12:35:58 +01:00
Simon Larsen
3aba42b965 feat: Enhance TraceExplorer UI with trace ID display and copy functionality 2025-09-15 12:22:51 +01:00
Simon Larsen
5651a43b22 feat: Add service filtering and statistics display in TraceExplorer 2025-09-15 12:19:29 +01:00
Simon Larsen
929792ef6a feat: Enhance TraceExplorer with error filtering and summary stats display 2025-09-15 11:54:20 +01:00
Simon Larsen
e13cdc4523 Merge pull request #2004 from OneUptime/span-improve
Span improve
2025-09-15 11:50:47 +01:00
Simon Larsen
8dc0535dac feat: Enhance JSONTable grouping logic to always override existing keys with grouped array representation 2025-09-15 11:50:21 +01:00
Simon Larsen
dc4541739a feat: Implement grouping for primitive arrays in JSONTable component 2025-09-15 11:41:07 +01:00
Simon Larsen
a4a3da7e2e feat: Add UI improvement task with Tailwind CSS guidelines for modern design 2025-09-15 11:30:54 +01:00
Simon Larsen
21432e1416 feat: Add default sorting to TelemetryServiceTable by name in ascending order 2025-09-15 11:23:57 +01:00
Simon Larsen
2f568f1319 feat: Add JSONTable component for improved display of JSON attributes in SpanViewer 2025-09-15 11:17:04 +01:00
Simon Larsen
50bdd592b4 refactor: Clean up telemetry span attributes by removing unnecessary properties 2025-09-15 10:50:02 +01:00
Simon Larsen
3938637b84 feat: Enhance MarkdownViewer styling and improve preformatted code handling 2025-09-10 20:08:30 +01:00
Simon Larsen
3ed9e21271 Merge branch 'master' of github.com:OneUptime/oneuptime 2025-09-10 19:09:57 +01:00
Simon Larsen
63e1266e2b feat: Improve MarkdownViewer styling with enhanced Tailwind CSS classes for better readability and aesthetics 2025-09-10 19:09:53 +01:00
Nawaz Dhandala
a552812711 feat: Add projectId support to SlackUtil message sending for incident and scheduled maintenance actions 2025-09-10 18:21:30 +01:00
Nawaz Dhandala
ad07ab75fe feat: Add elkjs dependency for enhanced functionality 2025-09-10 18:13:59 +01:00
Nawaz Dhandala
c8deffebb0 refactor: Improve code readability by standardizing formatting and spacing in SlackUtil methods 2025-09-10 17:12:51 +01:00
Simon Larsen
67a3ea5109 feat: Enhance SlackUtil with projectId support and caching for channel operations 2025-09-10 17:11:04 +01:00
Simon Larsen
6728cc0458 feat: Add projectId parameter to channel-related methods for improved context handling 2025-09-10 17:08:02 +01:00
Simon Larsen
f84ab2474f feat: Optimize channel existence checks by introducing getWorkspaceChannelByName method and streamline channel name normalization 2025-09-10 16:47:36 +01:00
Simon Larsen
5c8ce04eed feat: Enhance pagination handling by supporting 'skip' and 'limit' parameters from both query and body 2025-09-09 16:56:55 +01:00
Nawaz Dhandala
3064aa0364 feat: Improve code formatting and descriptions in announcement-related components and migrations for better readability 2025-09-09 14:36:32 +01:00
Simon Larsen
9625f1381c feat: Add migration for AnnouncementMonitor and AnnouncementTemplateMonitor tables with foreign key constraints 2025-09-09 14:12:33 +01:00
Simon Larsen
6ecd3ad166 Merge branch 'master' into announcement-monitor 2025-09-09 14:04:36 +01:00
Simon Larsen
8e54cac86e feat: Add createEditModalWidth prop with large size to multiple template views for consistent modal presentation 2025-09-09 14:01:46 +01:00
Nawaz Dhandala
cc52bb76d1 feat: Enhance incident state handling by adding type definitions, improving error handling, and updating default state display 2025-09-09 13:54:52 +01:00
Nawaz Dhandala
4c037f54f4 feat: Refactor incident state migration and update related components for improved clarity and functionality 2025-09-09 13:49:34 +01:00
Simon Larsen
b869628d4a feat: Implement fetch for initial incident state and update form values 2025-09-09 13:48:11 +01:00
Simon Larsen
0fbeb503ad feat: Update incident state field title and description for clarity 2025-09-09 13:06:29 +01:00
Simon Larsen
a302e4dc6c feat: Implement automatic selection of the first incident state and update related references 2025-09-09 12:58:07 +01:00
Nawaz Dhandala
00c8783137 feat: Add monitor selection to status page announcements and templates, enhancing resource notification capabilities 2025-09-09 12:56:42 +01:00
Simon Larsen
11211f4a62 feat: Update initial incident state description to reflect default behavior 2025-09-09 12:53:30 +01:00
Simon Larsen
d29750d66e feat: Add initialIncidentState field to IncidentTemplates for incident creation 2025-09-09 12:47:15 +01:00
Simon Larsen
7dc590dab4 feat: Add initialIncidentStateId migration and update index references 2025-09-09 12:37:43 +01:00
Simon Larsen
1d0ed64c1a feat: Rename currentIncidentState to initialIncidentState and update related references in IncidentTemplate, IncidentService, and IncidentTemplatesView 2025-09-09 12:26:48 +01:00
Simon Larsen
0cf3884be4 Merge branch 'master' into select-incident-state 2025-09-09 12:21:12 +01:00
Simon Larsen
165f5608e6 feat: Add step to free disk space in GitHub Actions runner for improved image build efficiency 2025-09-09 12:18:07 +01:00
Nawaz Dhandala
f2b8cfbffb Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-09-09 12:12:57 +01:00
Nawaz Dhandala
6084e15f20 refactor: Enhance type annotations in MarkdownEditor and tests for improved type safety 2025-09-09 12:12:55 +01:00
Simon Larsen
b1db4187de Learn more about Markdown syntax. 2025-09-09 12:12:09 +01:00
Nawaz Dhandala
20ce8a8c74 refactor: Clean up MarkdownEditor and FormField code for improved readability and consistency 2025-09-09 11:45:00 +01:00
Simon Larsen
39200249d1 feat: Update spell check handling in MarkdownEditor and tests for improved functionality 2025-09-09 11:38:58 +01:00
Simon Larsen
27533125e4 feat: Add createEditModalWidth prop to multiple components for consistent modal sizing 2025-09-09 11:23:20 +01:00
Simon Larsen
99dd421329 feat: Add createEditModalWidth prop to IncidentDelete for consistent modal sizing 2025-09-09 11:17:11 +01:00
Simon Larsen
4184894f27 feat: Refactor MarkdownEditor toolbar layout for improved organization and readability 2025-09-09 11:12:40 +01:00
Simon Larsen
a7a00dc0fa feat: Add dataTestId prop to MarkdownEditor and FormField for improved testing 2025-09-09 10:58:06 +01:00
Simon Larsen
9340f69789 feat: Add additional toolbar buttons and formatting options in MarkdownEditor 2025-09-09 10:35:10 +01:00
Simon Larsen
ba33bc0c23 feat: Enhance MarkdownEditor with improved heading handling and toolbar buttons 2025-09-09 10:27:41 +01:00
Simon Larsen
b8cac60c6e feat: Enhance Markdown preview with improved code block handling and styling 2025-09-09 09:59:32 +01:00
Simon Larsen
3a5d5253d0 feat: Enhance MarkdownEditor with toolbar buttons and preview functionality 2025-09-09 09:53:44 +01:00
Simon Larsen
64010b0348 feat: Add initial incident state selection to incident templates and creation forms 2025-09-09 08:35:46 +01:00
Simon Larsen
84ca2ff311 fix: Remove redundant APP_VERSION build argument in Docker image deployment steps 2025-09-08 22:08:00 +01:00
Simon Larsen
c0becebadc feat: Update date formatting to user-friendly display in getMonitorStatusTimelineForStatusPage method 2025-09-08 21:58:53 +01:00
Nawaz Dhandala
6ef99fd890 refactor: Specify types for format and testDate in OneUptimeDate class methods 2025-09-08 21:52:31 +01:00
Nawaz Dhandala
a55f2f7842 refactor: Improve code readability by formatting function arguments and return values in date handling methods 2025-09-08 21:51:53 +01:00
Simon Larsen
0aae7877c7 feat: Update date formatting to user-friendly display in various components 2025-09-08 21:49:19 +01:00
Simon Larsen
8d6cc37f7a feat: Update date formatting to user-friendly display across various components 2025-09-08 21:41:57 +01:00
Simon Larsen
1a0f7eb1e7 feat: Enhance date formatting to user-friendly display in Scheduled Maintenance components 2025-09-08 21:36:47 +01:00
Simon Larsen
6ed65ed3ef fix: Change tag type from semver to raw for Docker image deployments 2025-09-08 20:43:09 +01:00
Simon Larsen
2ac342e26a feat: Add billing_enabled variable to Nginx configuration 2025-09-08 20:29:28 +01:00
Simon Larsen
fe80d6b1ff fix: Remove unnecessary markdown syntax from upgrading guide 2025-09-08 18:44:43 +01:00
Simon Larsen
a68254be6d fix: Clarify reason for discontinuing Bitnami charts in upgrading guide 2025-09-08 18:43:14 +01:00
Simon Larsen
49a9e355fe feat: Add upgrading guide and navigation link to documentation 2025-09-08 18:41:40 +01:00
Simon Larsen
7091e35393 Update GitHub Actions workflow to read version prefix from VERSION_PREFIX file and adjust versioning scheme
- Added a new job 'read-version' to read the major and minor version from VERSION_PREFIX file.
- Updated dependent jobs to use the version read from 'read-version' instead of hardcoded version.
- Changed versioning format in multiple jobs to reflect the new versioning scheme based on the content of VERSION_PREFIX.
- Created VERSION_PREFIX file with initial version set to 8.0.
2025-09-07 15:17:47 +01:00
Simon Larsen
34cc8a43ab Merge pull request #1995 from OneUptime/bitnami-mgr-postgres
Bitnami mgr postgres
2025-09-07 13:21:41 +01:00
Simon Larsen
75333ef36c feat: Add pod security context configuration for ClickHouse and Redis StatefulSets 2025-09-07 13:03:09 +01:00
Simon Larsen
d4b3f1b60b feat: Add primary pod security context configuration for PostgreSQL 2025-09-07 12:59:27 +01:00
Simon Larsen
318d20a5a5 feat: Update PostgreSQL StatefulSet to use primary nodeSelector, affinity, tolerations, and resources 2025-09-07 12:56:29 +01:00
Simon Larsen
44b9c33e5c feat: Add primary ConfigMaps for PostgreSQL configuration and pg_hba settings 2025-09-07 12:45:32 +01:00
Simon Larsen
317a17cbab feat: Rename PostgreSQL ConfigMaps to include 'primary' in their names for clarity 2025-09-07 12:40:23 +01:00
Simon Larsen
6d2cb53760 feat: Update PostgreSQL configuration to use primary settings for ConfigMaps 2025-09-07 12:37:30 +01:00
Simon Larsen
7ddc4be319 feat: Add pg_hba.conf configuration and corresponding ConfigMap for PostgreSQL 2025-09-07 12:32:47 +01:00
Simon Larsen
604776551b feat: Add PostgreSQL configuration checksum and update container args 2025-09-07 12:25:41 +01:00
Simon Larsen
26b085030d refactor: Remove initContainers from PostgreSQL StatefulSet and enable default configuration settings 2025-09-07 12:17:13 +01:00
Simon Larsen
e1046d2424 Merge branch 'master' into bitnami-mgr-postgres 2025-09-07 11:08:46 +01:00
Simon Larsen
cf2a7b9dfa feat: Enhance diagnostics collection in KinD setup script 2025-09-07 11:05:34 +01:00
Simon Larsen
55f4c0b65d docs: Add SQL query to check used and free space in Postgres 2025-09-06 20:38:34 +01:00
Simon Larsen
5100fbda52 docs: Add SQL query to check used and free space in Clickhouse 2025-09-06 20:35:17 +01:00
Simon Larsen
9e36188975 fix: Update image registry and repository in ci-values.yaml 2025-09-06 17:47:47 +01:00
Simon Larsen
26c2d41dfa Merge branch 'master' into bitnami-mgr-postgres 2025-09-06 17:45:00 +01:00
Simon Larsen
a511a433b1 refactor: Remove security context and default profiles from ClickHouse configuration 2025-09-06 11:27:50 +01:00
Simon Larsen
cc581e91b5 Merge pull request #1994 from OneUptime/bitnami-mgr
Bitnami mgr
2025-09-06 11:09:13 +01:00
Simon Larsen
3fd95fe8aa Merge branch 'master' into bitnami-mgr 2025-09-06 11:09:01 +01:00
Simon Larsen
6f2455c265 fix: Set ClickHouse resourcesPreset to "none" to override default value 2025-09-05 21:26:00 +01:00
Simon Larsen
de5b32a609 refactor: Remove default resourcesPreset for ClickHouse configuration 2025-09-05 21:17:13 +01:00
Simon Larsen
155b0d90f1 refactor: Transition from MicroK8s to KinD for Kubernetes cluster setup in CI scripts 2025-09-05 21:15:58 +01:00
Simon Larsen
3da5e12a0d feat: Add auto-generated password option for ClickHouse configuration 2025-09-05 21:14:58 +01:00
Simon Larsen
8accdc6bd4 Merge branch 'master' of github.com:OneUptime/oneuptime 2025-09-05 20:56:35 +01:00
Simon Larsen
22a10702ac feat: Add comprehensive architecture diagram and explanation for self-hosted setup 2025-09-05 20:56:32 +01:00
Nawaz Dhandala
a013c86fae refactor: Standardize wait duration for pod readiness checks 2025-09-05 16:24:25 +01:00
Nawaz Dhandala
361626d21f refactor: Enhance pod readiness check with improved diagnostics and error handling 2025-09-05 16:23:01 +01:00
Simon Larsen
6615ac63d7 refactor: Simplify legal page structure and enhance navigation with Bootstrap styling 2025-09-05 14:50:22 +01:00
Simon Larsen
655611b28d refactor: Revamp support page layout and content for improved user experience 2025-09-05 14:45:55 +01:00
Simon Larsen
9bd45ecd14 Merge branch 'master' into bitnami-mgr-postgres 2025-09-05 14:01:46 +01:00
Simon Larsen
53c9babb83 refactor: Replace commented wait logic with active polling for pod readiness 2025-09-05 14:01:00 +01:00
Simon Larsen
ccc0b0142b feat: Remove default profiles configuration from ClickHouse settings in values.yaml 2025-09-05 13:51:07 +01:00
Simon Larsen
4a2f7f68cb feat: Update PostgreSQL password field to use 'postgresPassword' in values.yaml and secrets.yaml 2025-09-05 12:48:10 +01:00
Simon Larsen
de994e10de feat: Fix PostgreSQL username to be fixed as "postgres" in values.yaml 2025-09-05 12:21:43 +01:00
Simon Larsen
2ff22ca079 feat: Enhance security context handling for ClickHouse, PostgreSQL, and Redis StatefulSets 2025-09-05 12:06:28 +01:00
Simon Larsen
e6b8f60977 feat: Update security context for init and primary PostgreSQL containers 2025-09-05 11:59:37 +01:00
Simon Larsen
b823b5924a feat: Add init container for PostgreSQL configuration symlinks and update security context 2025-09-05 11:42:35 +01:00
Simon Larsen
a58ddd94d5 feat: Add POSTGRES_INITDB_ARGS environment variable for PostgreSQL initialization 2025-09-04 20:19:08 +01:00
Simon Larsen
275e15ce96 feat: Update PostgreSQL environment variables and liveness probe configuration 2025-09-04 20:12:20 +01:00
Simon Larsen
1e2a30823c feat: Remove PostgreSQL authentication environment variables from StatefulSet 2025-09-04 19:48:21 +01:00
Simon Larsen
a326e7084e feat: Remove PostgreSQL dependency and associated chart from Helm configuration 2025-09-04 17:55:18 +01:00
Simon Larsen
2c1d20f680 feat: Remove PostgreSQL dependency from Helm chart 2025-09-04 17:54:48 +01:00
Simon Larsen
95bd2db0dd feat: Add security context support for ClickHouse, PostgreSQL, and Redis StatefulSets 2025-09-04 17:44:59 +01:00
Simon Larsen
3ca875254c feat: Add PostgreSQL StatefulSet configuration to Helm chart 2025-09-04 17:35:39 +01:00
Simon Larsen
7f1f78dad6 feat: Add PostgreSQL Secret configuration to Helm chart 2025-09-04 17:27:49 +01:00
Simon Larsen
a0d6468aee feat: Add PostgreSQL ConfigMap and Service templates for Helm chart 2025-09-04 17:27:30 +01:00
Simon Larsen
914c9bc58e feat: Update PostgreSQL configuration in values.yaml and README for built-in support 2025-09-04 17:26:21 +01:00
Simon Larsen
b38031e9f7 feat: Add subPath for data and config mounts in ClickHouse StatefulSet 2025-09-04 17:03:57 +01:00
Simon Larsen
487ca71f84 fix: Rename volume and mount paths for Redis data in StatefulSet configuration 2025-09-04 15:30:27 +01:00
Simon Larsen
67cd8e7db6 refactor: Remove initContainers from ClickHouse StatefulSet configuration 2025-09-04 15:28:17 +01:00
Simon Larsen
f44017d710 feat: Add Helm annotations for release name and namespace in templates 2025-09-04 12:23:16 +01:00
Simon Larsen
78240b906b feat: Add ClickHouse StatefulSet configuration to Helm chart 2025-09-03 21:57:59 +01:00
Simon Larsen
2ef0b3be27 feat: Add ClickHouse ConfigMap template for configuration management 2025-09-03 21:43:20 +01:00
Simon Larsen
0792d8367a feat: Add ClickHouse service and secret configurations to Helm chart 2025-09-03 21:43:01 +01:00
Simon Larsen
920397cead fix: Remove ClickHouse chart package from the repository 2025-09-03 21:41:35 +01:00
Simon Larsen
42c18e94ab feat: Update ClickHouse configuration and service settings in values.yaml 2025-09-03 21:41:29 +01:00
Simon Larsen
533f7eb238 fix: Remove ClickHouse dependency from Chart.yaml and Chart.lock 2025-09-03 21:38:03 +01:00
Simon Larsen
e2f16e85f1 Merge pull request #1993 from OneUptime/bitnami-mgr
Bitnami mgr
2025-09-03 20:12:22 +01:00
Simon Larsen
c98e6b8471 feat: Add KEDA chart dependency to README 2025-09-03 20:12:09 +01:00
Simon Larsen
c16c13fd89 feat: Add built-in Redis configuration to README and update external Redis instructions 2025-09-03 20:10:43 +01:00
Simon Larsen
c8ce0e8819 fix: Remove Redis cluster configuration options from values.yaml 2025-09-03 20:00:14 +01:00
Simon Larsen
9e98f6acdb fix: Remove Redis replica persistence configuration from values.yaml 2025-09-03 19:50:07 +01:00
Simon Larsen
a7a5b15dde feat: Implement Redis StatefulSet configuration in Helm chart 2025-09-03 19:49:49 +01:00
Simon Larsen
3ebb5217a2 feat: Add Redis master and headless service definitions to Helm chart 2025-09-03 19:46:10 +01:00
Simon Larsen
f570ffe1e3 feat: Add Redis ConfigMap template to Helm chart for Redis configuration management 2025-09-03 19:43:57 +01:00
Simon Larsen
ae94bf6d7c fix: Simplify Redis password handling in Helm chart by removing unnecessary conditional checks 2025-09-03 19:42:28 +01:00
Simon Larsen
d9a6e465bb fix: Remove Redis authentication requirement in values.yaml 2025-09-03 19:42:11 +01:00
Simon Larsen
020b171b77 fix: Update Redis password handling in Helm chart to support optional authentication 2025-09-03 19:38:06 +01:00
Simon Larsen
afc4932c28 fix: Remove Redis dependency and related configurations from Helm chart 2025-09-03 19:37:30 +01:00
Nawaz Dhandala
324851c57e fix: Refactor service operations to execute sequentially with improved error handling in AlertService, IncidentService, MonitorService, and ScheduledMaintenanceService 2025-09-03 15:41:23 +01:00
Nawaz Dhandala
380ecfa096 Refactor code for consistency and readability
- Updated array and object property access from single quotes to double quotes in Pagination.ts and Permissions.ts for consistency.
- Added missing commas in function parameters and object literals across multiple files in AlertService.ts, IncidentService.ts, MonitorService.ts, ScheduledMaintenanceService.ts, and WorkspaceNotificationRuleService.ts.
- Improved error logging messages in various services for better clarity.
- Removed unnecessary line breaks in Slack.ts and Workspace.ts for cleaner code.
- Ensured consistent formatting in Routes.ts by adding missing commas and adjusting line breaks.
2025-09-03 15:37:39 +01:00
Simon Larsen
5f9f73ceaa fix: Refactor monitor creation operations to execute sequentially with improved error handling in MonitorService 2025-09-03 15:35:13 +01:00
Simon Larsen
038ca4a920 fix: Update imports and improve formatting in Routes.ts for consistency and readability 2025-09-03 15:34:29 +01:00
Simon Larsen
d15629da0f fix: Refactor scheduled maintenance operations to execute sequentially with improved error handling in ScheduledMaintenanceService 2025-09-03 15:21:03 +01:00
Simon Larsen
363bbf9dea fix: Refactor incident creation operations to execute sequentially with improved error handling in IncidentService 2025-09-03 15:17:16 +01:00
Simon Larsen
6f0a0c8e38 fix: Remove unnecessary line breaks in error messages and logging for improved readability in WorkspaceNotificationRuleService 2025-09-03 15:06:19 +01:00
Simon Larsen
a75a62c708 fix: Refactor promise chain to use async/await for better readability in AlertService; add debug logging in WorkspaceNotificationRuleService 2025-09-03 14:18:05 +01:00
Simon Larsen
db76d716b9 fix: Remove redundant logging of existing workspace channels for cleaner output 2025-09-03 14:02:12 +01:00
Simon Larsen
b0abbf64b4 fix: Improve logging format in postToWorkspaceChannels for better readability 2025-09-03 13:55:43 +01:00
Simon Larsen
3a432cf8e6 Response from Slack API for getting all channels: 2025-09-03 13:54:58 +01:00
Simon Larsen
5c7d18e3ed fix: Update createdByUser field to use _id for consistency in Alert, Incident, and Scheduled Maintenance services 2025-09-03 13:27:37 +01:00
Simon Larsen
2590850ffa fix: Correct projectId usage in alert feed info generation for accurate monitor links 2025-09-03 13:13:14 +01:00
Simon Larsen
0eeb80e16e fix: Add createdByUserId and createdByUser fields to alert, incident, and scheduled maintenance services for improved tracking 2025-09-03 13:09:38 +01:00
Simon Larsen
e1cfe24a24 fix: Update pageData property access to bracket notation for consistency 2025-09-02 22:59:23 +01:00
Simon Larsen
4e4f3a889d fix: Update type casting for statusReport and probeMonitorResponse to 'any' for improved flexibility 2025-09-02 22:51:41 +01:00
Nawaz Dhandala
ede7ae103d fix: Enhance MonitorTemplateUtil to support additional monitor types and improve type safety 2025-09-02 22:15:04 +01:00
Simon Larsen
075c0fb6bd fix: Enhance template variable support for additional monitor types in MonitorTemplateUtil and update documentation 2025-09-02 22:08:51 +01:00
Nawaz Dhandala
5ebdb1ef7d fix: Refactor code for improved readability and maintainability in various components 2025-09-02 21:57:15 +01:00
Simon Larsen
387dbf332e fix: Correct spelling in API endpoint routes for escalation rules 2025-09-02 21:48:54 +01:00
Simon Larsen
9681e1dc88 fix: Remove fallback syntax from incident alert templating examples for clarity 2025-09-02 21:47:05 +01:00
Simon Larsen
fb29014480 Merge branch 'dynamic-alert' 2025-09-02 21:45:15 +01:00
Simon Larsen
1a5c2efc59 fix: Add debug logging for storage map and template value replacement in MonitorTemplateUtil 2025-09-02 21:43:28 +01:00
Simon Larsen
3e31e44ed5 fix: Enhance value replacement logic to properly serialize objects in VMUtil class 2025-09-02 21:26:55 +01:00
Simon Larsen
9e69d69429 fix: Update titles and descriptions for Global Probes settings for clarity 2025-09-02 21:15:21 +01:00
Simon Larsen
a108deac0f fix: Improve documentation links in MonitorCriteriaAlertForm and MonitorCriteriaIncidentForm for clarity 2025-09-02 21:14:03 +01:00
Simon Larsen
c767f14bf1 fix: Correct syntax error in AlertService class 2025-09-02 21:11:28 +01:00
Simon Larsen
d69485c436 fix: Update Global Probes status messages for clarity in ProbePage component 2025-09-02 20:50:20 +01:00
Simon Larsen
67a5bdb7b8 feat: Update Global Probe settings card with improved descriptions and toggle functionality 2025-09-02 20:47:56 +01:00
Nawaz Dhandala
6504731025 refactor: Replace 'any' types with specific types for improved type safety across multiple files 2025-09-02 20:41:24 +01:00
Nawaz Dhandala
773692081c docs: Update guideline to specify stopping after fixing 25 files for review 2025-09-02 20:25:52 +01:00
Nawaz Dhandala
51c6234966 docs: Add guideline to replace "any" types with proper types 2025-09-02 20:23:53 +01:00
Nawaz Dhandala
fac6e9a1fe fix: Correct formatting issues in MonitorTemplateUtil and MonitorCriteriaAlertForm 2025-09-02 20:14:33 +01:00
Nawaz Dhandala
86e5d85d55 feat: Enhance dynamic template documentation links in MonitorCriteriaAlertForm and MonitorCriteriaIncidentForm 2025-09-02 20:10:14 +01:00
Simon Larsen
1c592435e9 Merge pull request #1990 from OneUptime/any-type
Any type
2025-09-02 20:02:38 +01:00
Nawaz Dhandala
02fed5bd6e refactor: Enhance type safety by explicitly defining ref types and simplifying conditional checks 2025-09-02 19:58:41 +01:00
Nawaz Dhandala
dd724fcc6e refactor: Improve type safety by updating formRef initialization and adding optional chaining for setFieldValue 2025-09-02 19:52:28 +01:00
Nawaz Dhandala
6ba26bcb82 refactor: Improve type safety by adding LayoutOptions type and removing 'any' casts in ServiceDependencyGraph 2025-09-02 19:51:45 +01:00
Nawaz Dhandala
799ab3220d refactor: Replace 'any' type with specific types for ref and input for improved type safety 2025-09-02 19:50:53 +01:00
Nawaz Dhandala
f73f2fb732 refactor: Change argValue type from any to unknown for better type safety 2025-09-02 19:50:34 +01:00
Simon Larsen
43d6ead92c fix: Correct formatting issues in MonitorAlert class logging and data processing 2025-09-02 19:22:31 +01:00
Simon Larsen
4c053b3f31 refactor: Improve formatting and readability in MonitorTemplateUtil methods 2025-09-02 19:02:29 +01:00
Simon Larsen
c026e411cf feat: Add dynamic template usage descriptions in MonitorCriteriaAlertForm and MonitorCriteriaIncidentForm 2025-09-02 19:01:22 +01:00
Simon Larsen
65a9e32db1 feat: Implement MonitorTemplateUtil for dynamic template processing in incidents and alerts 2025-09-02 18:58:15 +01:00
Simon Larsen
0b15e97e08 feat: Integrate MonitorTemplateUtil for dynamic alert and incident title/description processing 2025-09-02 18:56:19 +01:00
Simon Larsen
bd74b96596 feat: Add link to Incident & Alert Dynamic Templating documentation in navigation 2025-09-02 18:45:48 +01:00
Nawaz Dhandala
990d3ea750 feat: Add doNotAddGlobalProbesByDefaultOnNewMonitors column to Project table and update related files 2025-09-02 14:59:23 +01:00
Simon Larsen
30665a1907 feat: Add migration to introduce doNotAddGlobalProbesByDefaultOnNewMonitors column in Project table 2025-09-02 14:58:09 +01:00
Simon Larsen
04db4289fa feat: Add setting to control auto-adding of global probes to new monitors 2025-09-02 14:57:18 +01:00
Nawaz Dhandala
01f4c030a7 style: Format allowedDomains and homeUrl for improved readability 2025-09-02 14:29:17 +01:00
Simon Larsen
d060ed8b64 feat: Add dynamic robots.txt route to control indexing based on domain 2025-09-02 14:27:40 +01:00
Nawaz Dhandala
413240733e fix: Remove unnecessary blank lines in Markdown and Probe initialization files 2025-09-02 14:17:15 +01:00
Simon Larsen
b0799093dd feat: Enhance proxy agent support by specifying types for httpAgent and httpsAgent in WebsiteRequest and API classes 2025-09-02 14:10:42 +01:00
Simon Larsen
0ecdc775db feat: Refactor proxy configuration to use getRequestProxyAgents method across multiple modules 2025-09-02 14:02:40 +01:00
Simon Larsen
82065c20b1 feat: Add HTTP/HTTPS proxy support in FetchMonitorTest, FetchList, and Alive jobs 2025-09-02 13:58:15 +01:00
Simon Larsen
3dcd1ee604 feat: Add HTTP/HTTPS proxy support in probeMonitorTest and probeMonitor methods 2025-09-02 13:53:57 +01:00
Simon Larsen
f63c69e6a6 feat: Add per-request HTTP/HTTPS proxy agent support in API and WebsiteRequest classes 2025-09-02 13:52:25 +01:00
Simon Larsen
6ba793e871 refactor: Remove axios proxy configuration from ProxyConfig class 2025-09-02 13:49:58 +01:00
Simon Larsen
7afd243992 feat: Remove backticks from inline code rendering in Markdown 2025-09-01 21:19:39 +01:00
Simon Larsen
4f58155719 feat: Enhance inline code rendering by removing backticks from code content 2025-09-01 21:12:30 +01:00
Simon Larsen
553adc4aef feat: Add custom styling for inline code in Markdown renderer 2025-09-01 21:11:37 +01:00
Simon Larsen
f668a626d7 refactor: Simplify code block styling in Markdown renderer 2025-09-01 21:02:19 +01:00
Nawaz Dhandala
e3bd534295 refactor: Add missing commas in logging statements for SCIM and StatusPageSCIM 2025-09-01 20:50:53 +01:00
Simon Larsen
6aa5c3b314 feat: Enhance SCIM Users endpoint to support filtering by email and improve logging 2025-09-01 20:50:12 +01:00
Simon Larsen
3bc4f7267d refactor: Clean up logging statements and remove unnecessary commas in SCIM.ts 2025-09-01 20:48:54 +01:00
Simon Larsen
a7021cf045 refactor: Replace logSCIMOperation with logger.debug for consistent logging in SCIM and StatusPageSCIM 2025-09-01 20:40:27 +01:00
Simon Larsen
2709e1d976 feat: Update values.yaml to allow insecure images temporarily and specify legacy image repositories for PostgreSQL, ClickHouse, and Redis 2025-09-01 16:38:56 +01:00
Nawaz Dhandala
8ec9d2a930 feat: Add type annotations for proxy-related variables in ProxyConfig and monitors 2025-09-01 14:58:33 +01:00
Nawaz Dhandala
224c225789 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-09-01 14:53:33 +01:00
Simon Larsen
85dae7a307 feat: Add proxy configuration options for probes in values.yaml and update README and probe.yaml 2025-09-01 14:53:24 +01:00
Nawaz Dhandala
332a479c22 feat: Improve proxy configuration handling and logging in monitors 2025-09-01 14:50:52 +01:00
Simon Larsen
d708fbbb52 feat: Add proxy configuration examples to Custom Probe documentation and component 2025-09-01 14:49:40 +01:00
Simon Larsen
03bceb959e feat: Enhance proxy support in SSL and Synthetic Monitors to prefer HTTPS, fallback to HTTP 2025-09-01 14:47:01 +01:00
Simon Larsen
efa411206e feat: Update SSL and Synthetic Monitors to use HTTPS proxy configuration 2025-09-01 14:41:57 +01:00
Simon Larsen
27fd99f2e8 feat: Update proxy configuration to support separate HTTP and HTTPS proxy URLs 2025-09-01 14:40:54 +01:00
Simon Larsen
07361bfeb7 feat: Enhance SyntheticMonitor with proxy support in browser launch options 2025-09-01 14:07:15 +01:00
Simon Larsen
bc8a5be0fa feat: Add proxy support for CustomCodeMonitor and SyntheticMonitor with logging 2025-09-01 14:00:55 +01:00
Simon Larsen
518768078a feat: Implement proxy configuration for HTTP requests and add ProxyConfig utility 2025-09-01 13:56:29 +01:00
Simon Larsen
86e95f99ff feat: Add PROXY_URL configuration option for probe and update example env file 2025-09-01 12:52:50 +01:00
Simon Larsen
ea48f56097 feat: Add custom styles for code blocks in blog posts 2025-09-01 12:43:03 +01:00
Nawaz Dhandala
b8b9dd859a Refactor migration files for consistency and readability; update BillingService and ProjectService for improved code clarity; enhance Countries interface formatting; standardize string quotes in various components; fix minor formatting issues in Settings and SendAnnouncementCreatedNotification. 2025-08-27 14:50:48 +01:00
Simon Larsen
d28c14ef24 feat: Update projectId reference in status page notification logic 2025-08-27 14:49:33 +01:00
Simon Larsen
670bec2a12 feat: Validate projectId and statusPageId in getStatusPageLinkInDashboard method 2025-08-27 14:48:40 +01:00
Simon Larsen
aff24845a8 feat: Add financeAccountingEmail handling in updateCustomerBusinessDetails method 2025-08-27 14:26:59 +01:00
Simon Larsen
f280e97c1b feat: Add migration for financeAccountingEmail field in Project model 2025-08-27 14:14:02 +01:00
Simon Larsen
62facf62dd feat: Add financeAccountingEmail field to Project model and update billing settings 2025-08-27 14:12:16 +01:00
Simon Larsen
db0387d81a feat: Update placeholder condition to include empty string check 2025-08-27 14:06:33 +01:00
Simon Larsen
5c4b19ab3d feat: Update nodemon configurations to improve performance and debugging options 2025-08-27 13:41:05 +01:00
Simon Larsen
463755fa4d This will be synced to Stripe and appear on future invoices. 2025-08-27 13:40:26 +01:00
Simon Larsen
85888572de feat: Update subscriber notification statuses to 'Success' for existing records in Incident and related tables 2025-08-27 13:20:07 +01:00
Simon Larsen
475bb25b2d feat: Add businessDetailsCountry field to Project migration and update index 2025-08-27 13:14:40 +01:00
Simon Larsen
badd200aed feat: Add country selection dropdown for billing details and implement country options 2025-08-27 13:14:01 +01:00
Simon Larsen
b40d87cbc9 feat: Add business details country field to Project model and update billing services to handle country code 2025-08-27 13:05:54 +01:00
Simon Larsen
36d0066b3a refactor: Simplify migration by removing unnecessary constraints and columns from Project and GlobalConfig tables 2025-08-27 13:03:01 +01:00
Simon Larsen
a49a0b2cba fix: Ensure blog post cards maintain full height for consistent layout 2025-08-27 12:48:41 +01:00
Simon Larsen
bada97d474 feat: Enhance customer address handling in Stripe by mapping business details to structured address fields 2025-08-27 12:42:17 +01:00
Simon Larsen
a1699f2d55 feat: Add business details field to Project model and update Stripe customer details 2025-08-27 12:37:09 +01:00
658 changed files with 68068 additions and 7701 deletions

17
.github/instructions/instructions.md vendored Normal file
View File

@@ -0,0 +1,17 @@
---
applyTo: '**'
---
# Building and Compiling
If you would like to compile or build any project. Please cd into the directory and run the following command:
```
npm run compile
```
Ths will make sure there are no type / syntax errors.
# Typescript Types.
Please do not use "any" types. Please create proper types where required.

View File

@@ -19,7 +19,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for accounts service
- name: build docker image
@@ -38,7 +42,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for accounts service
- name: build docker image
@@ -57,7 +65,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for accounts service
- name: build docker image
@@ -76,7 +88,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for accounts service
- name: build docker image
@@ -95,7 +111,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for accounts service
- name: build docker image
@@ -114,7 +134,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for accounts service
- name: build docker image
@@ -133,7 +157,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for accounts service
- name: build docker image
@@ -153,7 +181,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for accounts service
- name: build docker image
@@ -172,7 +204,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for accounts service
@@ -193,7 +229,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for accounts service
- name: build docker image
@@ -212,7 +252,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for accounts service
@@ -232,7 +276,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for home
- name: build docker image
@@ -251,7 +299,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for home
- name: build docker image
@@ -270,7 +322,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image probe api
- name: build docker image
@@ -289,7 +345,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image probe api
- name: build docker image
@@ -308,7 +368,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image probe api
- name: build docker image
@@ -327,7 +391,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image probe api
- name: build docker image
@@ -346,7 +414,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image probe api
- name: build docker image
@@ -365,7 +437,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image probe api
- name: build docker image
@@ -384,7 +460,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for home
- name: build docker image
@@ -403,7 +483,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for mail service
- name: build docker image

View File

@@ -422,4 +422,4 @@ jobs:
with:
timeout_minutes: 30
max_attempts: 3
command: cd MCP && npm install && npm run compile && npm run dep-check
command: cd MCP && npm update @oneuptime/common && npm install && npm run compile && npm run dep-check

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
#
# Pull base image nodejs image.
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
RUN npm config set fetch-retries 5
@@ -17,6 +17,7 @@ ARG APP_VERSION
ENV GIT_SHA=${GIT_SHA}
ENV APP_VERSION=${APP_VERSION}
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# IF APP_VERSION is not set, set it to 1.0.0

View File

@@ -2,6 +2,7 @@ import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import Dictionary from "Common/Types/Dictionary";
// Retrieve resources documentation
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
@@ -16,7 +17,7 @@ export default class ServiceHandler {
// Extract page parameter from request
const page: string | undefined = req.params["page"];
const pageData: any = {};
const pageData: Dictionary<unknown> = {};
// Set default page title and description for the authentication page
pageTitle = "Authentication";

View File

@@ -4,6 +4,7 @@ import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import LocalFile from "Common/Server/Utils/LocalFile";
import Dictionary from "Common/Types/Dictionary";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
@@ -12,9 +13,9 @@ export default class ServiceHandler {
_req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const pageData: any = {};
const pageData: Dictionary<unknown> = {};
pageData.selectCode = await LocalCache.getOrSetString(
pageData["selectCode"] = await LocalCache.getOrSetString(
"data-type",
"select",
async () => {
@@ -22,7 +23,7 @@ export default class ServiceHandler {
},
);
pageData.sortCode = await LocalCache.getOrSetString(
pageData["sortCode"] = await LocalCache.getOrSetString(
"data-type",
"sort",
async () => {
@@ -30,7 +31,7 @@ export default class ServiceHandler {
},
);
pageData.equalToCode = await LocalCache.getOrSetString(
pageData["equalToCode"] = await LocalCache.getOrSetString(
"data-type",
"equal-to",
async () => {
@@ -38,7 +39,7 @@ export default class ServiceHandler {
},
);
pageData.equalToOrNullCode = await LocalCache.getOrSetString(
pageData["equalToOrNullCode"] = await LocalCache.getOrSetString(
"data-type",
"equal-to-or-null",
async () => {
@@ -48,7 +49,7 @@ export default class ServiceHandler {
},
);
pageData.greaterThanCode = await LocalCache.getOrSetString(
pageData["greaterThanCode"] = await LocalCache.getOrSetString(
"data-type",
"greater-than",
async () => {
@@ -58,7 +59,7 @@ export default class ServiceHandler {
},
);
pageData.greaterThanOrEqualCode = await LocalCache.getOrSetString(
pageData["greaterThanOrEqualCode"] = await LocalCache.getOrSetString(
"data-type",
"greater-than-or-equal",
async () => {
@@ -68,7 +69,7 @@ export default class ServiceHandler {
},
);
pageData.lessThanCode = await LocalCache.getOrSetString(
pageData["lessThanCode"] = await LocalCache.getOrSetString(
"data-type",
"less-than",
async () => {
@@ -78,7 +79,7 @@ export default class ServiceHandler {
},
);
pageData.lessThanOrEqualCode = await LocalCache.getOrSetString(
pageData["lessThanOrEqualCode"] = await LocalCache.getOrSetString(
"data-type",
"less-than-or-equal",
async () => {
@@ -88,7 +89,7 @@ export default class ServiceHandler {
},
);
pageData.includesCode = await LocalCache.getOrSetString(
pageData["includesCode"] = await LocalCache.getOrSetString(
"data-type",
"includes",
async () => {
@@ -98,7 +99,7 @@ export default class ServiceHandler {
},
);
pageData.lessThanOrNullCode = await LocalCache.getOrSetString(
pageData["lessThanOrNullCode"] = await LocalCache.getOrSetString(
"data-type",
"less-than-or-equal",
async () => {
@@ -108,7 +109,7 @@ export default class ServiceHandler {
},
);
pageData.greaterThanOrNullCode = await LocalCache.getOrSetString(
pageData["greaterThanOrNullCode"] = await LocalCache.getOrSetString(
"data-type",
"less-than-or-equal",
async () => {
@@ -118,7 +119,7 @@ export default class ServiceHandler {
},
);
pageData.isNullCode = await LocalCache.getOrSetString(
pageData["isNullCode"] = await LocalCache.getOrSetString(
"data-type",
"is-null",
async () => {
@@ -126,7 +127,7 @@ export default class ServiceHandler {
},
);
pageData.notNullCode = await LocalCache.getOrSetString(
pageData["notNullCode"] = await LocalCache.getOrSetString(
"data-type",
"not-null",
async () => {
@@ -134,7 +135,7 @@ export default class ServiceHandler {
},
);
pageData.notEqualToCode = await LocalCache.getOrSetString(
pageData["notEqualToCode"] = await LocalCache.getOrSetString(
"data-type",
"not-equals",
async () => {

View File

@@ -2,6 +2,7 @@ import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import Dictionary from "Common/Types/Dictionary";
// Fetch a list of resources used in the application
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
@@ -17,7 +18,7 @@ export default class ServiceHandler {
// Get the 'page' parameter from the request
const page: string | undefined = req.params["page"];
const pageData: any = {};
const pageData: Dictionary<unknown> = {};
// Set the default page title and description
pageTitle = "Errors";

View File

@@ -2,6 +2,7 @@ import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import Dictionary from "Common/Types/Dictionary";
// Get all resources and featured resources from ResourceUtil
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
@@ -20,10 +21,10 @@ export default class ServiceHandler {
// Get the requested page from the URL parameters
const page: string | undefined = req.params["page"];
const pageData: any = {};
const pageData: Dictionary<unknown> = {};
// Set featured resources for the page
pageData.featuredResources = FeaturedResources;
pageData["featuredResources"] = FeaturedResources;
// Set page title and description
pageTitle = "Introduction";

View File

@@ -3,7 +3,10 @@ import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import PageNotFoundServiceHandler from "./PageNotFound";
import { AppApiRoute } from "Common/ServiceRoute";
import { ColumnAccessControl } from "Common/Types/BaseDatabase/AccessControl";
import { getTableColumns } from "Common/Types/Database/TableColumn";
import {
getTableColumns,
TableColumnMetadata,
} from "Common/Types/Database/TableColumn";
import Dictionary from "Common/Types/Dictionary";
import ObjectID from "Common/Types/ObjectID";
import Permission, {
@@ -33,7 +36,7 @@ export default class ServiceHandler {
let pageTitle: string = "";
let pageDescription: string = "";
let page: string | undefined = req.params["page"];
const pageData: any = {};
const pageData: Dictionary<unknown> = {};
// Check if page is provided
if (!page) {
@@ -56,7 +59,9 @@ export default class ServiceHandler {
page = "model";
// Get table columns for current resource
const tableColumns: any = getTableColumns(currentResource.model);
const tableColumns: Dictionary<TableColumnMetadata> = getTableColumns(
currentResource.model,
);
// Filter out columns with no access
for (const key in tableColumns) {
@@ -77,12 +82,14 @@ export default class ServiceHandler {
continue;
}
if (tableColumns[key].hideColumnInDocumentation) {
if (tableColumns[key] && tableColumns[key]!.hideColumnInDocumentation) {
delete tableColumns[key];
continue;
}
tableColumns[key].permissions = accessControl;
if (tableColumns[key]) {
(tableColumns[key] as any).permissions = accessControl;
}
}
// Remove unnecessary columns
@@ -92,11 +99,11 @@ export default class ServiceHandler {
delete tableColumns["version"];
// Set page data
pageData.title = currentResource.model.singularName;
pageData.description = currentResource.model.tableDescription;
pageData.columns = tableColumns;
pageData["title"] = currentResource.model.singularName;
pageData["description"] = currentResource.model.tableDescription;
pageData["columns"] = tableColumns;
pageData.tablePermissions = {
pageData["tablePermissions"] = {
read: currentResource.model.readRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
@@ -120,7 +127,7 @@ export default class ServiceHandler {
};
// Cache the list request data
pageData.listRequest = await LocalCache.getOrSetString(
pageData["listRequest"] = await LocalCache.getOrSetString(
"model",
"list-request",
async () => {
@@ -130,7 +137,7 @@ export default class ServiceHandler {
);
// Cache the item request data
pageData.itemRequest = await LocalCache.getOrSetString(
pageData["itemRequest"] = await LocalCache.getOrSetString(
"model",
"item-request",
async () => {
@@ -140,7 +147,7 @@ export default class ServiceHandler {
);
// Cache the item response data
pageData.itemResponse = await LocalCache.getOrSetString(
pageData["itemResponse"] = await LocalCache.getOrSetString(
"model",
"item-response",
async () => {
@@ -152,7 +159,7 @@ export default class ServiceHandler {
);
// Cache the count request data
pageData.countRequest = await LocalCache.getOrSetString(
pageData["countRequest"] = await LocalCache.getOrSetString(
"model",
"count-request",
async () => {
@@ -164,7 +171,7 @@ export default class ServiceHandler {
);
// Cache the count response data
pageData.countResponse = await LocalCache.getOrSetString(
pageData["countResponse"] = await LocalCache.getOrSetString(
"model",
"count-response",
async () => {
@@ -175,7 +182,7 @@ export default class ServiceHandler {
},
);
pageData.updateRequest = await LocalCache.getOrSetString(
pageData["updateRequest"] = await LocalCache.getOrSetString(
"model",
"update-request",
async () => {
@@ -186,7 +193,7 @@ export default class ServiceHandler {
},
);
pageData.updateResponse = await LocalCache.getOrSetString(
pageData["updateResponse"] = await LocalCache.getOrSetString(
"model",
"update-response",
async () => {
@@ -197,7 +204,7 @@ export default class ServiceHandler {
},
);
pageData.createRequest = await LocalCache.getOrSetString(
pageData["createRequest"] = await LocalCache.getOrSetString(
"model",
"create-request",
async () => {
@@ -208,7 +215,7 @@ export default class ServiceHandler {
},
);
pageData.createResponse = await LocalCache.getOrSetString(
pageData["createResponse"] = await LocalCache.getOrSetString(
"model",
"create-response",
async () => {
@@ -219,7 +226,7 @@ export default class ServiceHandler {
},
);
pageData.deleteRequest = await LocalCache.getOrSetString(
pageData["deleteRequest"] = await LocalCache.getOrSetString(
"model",
"delete-request",
async () => {
@@ -230,7 +237,7 @@ export default class ServiceHandler {
},
);
pageData.deleteResponse = await LocalCache.getOrSetString(
pageData["deleteResponse"] = await LocalCache.getOrSetString(
"model",
"delete-response",
async () => {
@@ -242,7 +249,7 @@ export default class ServiceHandler {
);
// Get list response from cache or set it if it's not available
pageData.listResponse = await LocalCache.getOrSetString(
pageData["listResponse"] = await LocalCache.getOrSetString(
"model",
"list-response",
async () => {
@@ -254,14 +261,15 @@ export default class ServiceHandler {
);
// Generate a unique ID for the example object
pageData.exampleObjectID = ObjectID.generate();
pageData["exampleObjectID"] = ObjectID.generate();
// Construct the API path for the current resource
pageData.apiPath =
pageData["apiPath"] =
AppApiRoute.toString() + currentResource.model.crudApiPath?.toString();
// Check if the current resource is a master admin API
pageData.isMasterAdminApiDocs = currentResource.model.isMasterAdminApiDocs;
pageData["isMasterAdminApiDocs"] =
currentResource.model.isMasterAdminApiDocs;
// Render the index page with the required data
return res.render(`${ViewsPath}/pages/index`, {

View File

@@ -7,6 +7,7 @@ import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import URL from "Common/Types/API/URL";
import Dictionary from "Common/Types/Dictionary";
// Fetch a list of resources used in the application
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
@@ -22,7 +23,7 @@ export default class ServiceHandler {
// Get the 'page' parameter from the request
const page: string | undefined = req.params["page"];
const pageData: any = {
const pageData: Dictionary<unknown> = {
hostUrl: new URL(HttpProtocol, Host).toString(),
};

View File

@@ -4,6 +4,7 @@ import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import LocalFile from "Common/Server/Utils/LocalFile";
import Dictionary from "Common/Types/Dictionary";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); // Get all resources from ResourceUtil
@@ -15,14 +16,14 @@ export default class ServiceHandler {
let pageTitle: string = ""; // Initialize page title
let pageDescription: string = ""; // Initialize page description
const page: string | undefined = req.params["page"]; // Get the page parameter from the request
const pageData: any = {}; // Initialize page data object
const pageData: Dictionary<unknown> = {}; // Initialize page data object
// Set page title and description
pageTitle = "Pagination";
pageDescription = "Learn how to paginate requests with OneUptime API";
// Get response and request code from LocalCache or LocalFile
pageData.responseCode = await LocalCache.getOrSetString(
pageData["responseCode"] = await LocalCache.getOrSetString(
"pagination",
"response",
async () => {
@@ -33,7 +34,7 @@ export default class ServiceHandler {
},
);
pageData.requestCode = await LocalCache.getOrSetString(
pageData["requestCode"] = await LocalCache.getOrSetString(
"pagination",
"request",
async () => {

View File

@@ -3,6 +3,7 @@ import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { PermissionHelper, PermissionProps } from "Common/Types/Permission";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import Dictionary from "Common/Types/Dictionary";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
@@ -17,14 +18,14 @@ export default class ServiceHandler {
// Get the requested page
const page: string | undefined = req.params["page"];
const pageData: any = {};
const pageData: Dictionary<unknown> = {};
// Set page title and description
pageTitle = "Permissions";
pageDescription = "Learn how permissions work with OneUptime";
// Filter permissions to only include those assignable to tenants
pageData.permissions = PermissionHelper.getAllPermissionProps().filter(
pageData["permissions"] = PermissionHelper.getAllPermissionProps().filter(
(i: PermissionProps) => {
return i.isAssignableToTenant;
},

View File

@@ -1,8 +1,14 @@
{
"watch": ["./","../Common/Server", "../Common/Types", "../Common/Utils", "../Common/Models"],
"ext": "ts,json,tsx,env,js,jsx,hbs",
"ext": "ts,tsx",
"ignore": [
"./node_modules/**",
"./public/**",
"./bin/**",
"./build/**",
"greenlock.d/*"
],
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
"watchOptions": {"useFsEvents": false, "interval": 500},
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
"exec": "node -r ts-node/register/transpile-only Index.ts"
}

View File

@@ -66,6 +66,7 @@
"crypto-js": "^4.2.0",
"dotenv": "^16.4.4",
"ejs": "^3.1.10",
"elkjs": "^0.10.0",
"esbuild": "^0.25.5",
"express": "^4.21.1",
"formik": "^2.4.6",

View File

@@ -3,7 +3,7 @@
#
# Pull base image nodejs image.
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
RUN npm config set fetch-retries 5
@@ -17,6 +17,7 @@ ARG APP_VERSION
ENV GIT_SHA=${GIT_SHA}
ENV APP_VERSION=${APP_VERSION}
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# IF APP_VERSION is not set, set it to 1.0.0

View File

@@ -1,7 +1,10 @@
{
"watch": ["./","../Common/UI", "../Common/Types", "../Common/Utils", "../Common/Models"],
"ext": "ts,json,tsx,env,js,jsx,hbs",
"ext": "ts,tsx",
"ignore": [
"./node_modules/**",
"./public/**",
"./bin/**",
"./public/**",
"./public/dist/**",
"./build/*",

View File

@@ -70,6 +70,7 @@
"crypto-js": "^4.2.0",
"dotenv": "^16.4.4",
"ejs": "^3.1.10",
"elkjs": "^0.10.0",
"esbuild": "^0.25.5",
"express": "^4.21.1",
"formik": "^2.4.6",

View File

@@ -1,6 +1,8 @@
import {
LOGIN_API_URL,
VERIFY_TWO_FACTOR_AUTH_API_URL,
VERIFY_TOTP_AUTH_API_URL,
GENERATE_WEBAUTHN_AUTH_OPTIONS_API_URL,
VERIFY_WEBAUTHN_AUTH_API_URL,
} from "../Utils/ApiPaths";
import Route from "Common/Types/API/Route";
import URL from "Common/Types/API/URL";
@@ -12,17 +14,20 @@ import { DASHBOARD_URL } from "Common/UI/Config";
import OneUptimeLogo from "Common/UI/Images/logos/OneUptimeSVG/3-transparent.svg";
import UiAnalytics from "Common/UI/Utils/Analytics";
import LoginUtil from "Common/UI/Utils/Login";
import UserTwoFactorAuth from "Common/Models/DatabaseModels/UserTwoFactorAuth";
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
import UserWebAuthn from "Common/Models/DatabaseModels/UserWebAuthn";
import Navigation from "Common/UI/Utils/Navigation";
import UserUtil from "Common/UI/Utils/User";
import User from "Common/Models/DatabaseModels/User";
import React from "react";
import useAsyncEffect from "use-async-effect";
import StaticModelList from "Common/UI/Components/ModelList/StaticModelList";
import BasicForm from "Common/UI/Components/Forms/BasicForm";
import API from "Common/UI/Utils/API/API";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import Base64 from "Common/Utils/Base64";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import ComponentLoader from "Common/UI/Components/ComponentLoader/ComponentLoader";
const LoginPage: () => JSX.Element = () => {
const apiUrl: URL = LOGIN_API_URL;
@@ -36,14 +41,32 @@ const LoginPage: () => JSX.Element = () => {
const [showTwoFactorAuth, setShowTwoFactorAuth] =
React.useState<boolean>(false);
const [twoFactorAuthList, setTwoFactorAuthList] = React.useState<
UserTwoFactorAuth[]
>([]);
const [totpAuthList, setTotpAuthList] = React.useState<UserTotpAuth[]>([]);
const [selectedTwoFactorAuth, setSelectedTwoFactorAuth] = React.useState<
UserTwoFactorAuth | undefined
const [webAuthnList, setWebAuthnList] = React.useState<UserWebAuthn[]>([]);
const [selectedTotpAuth, setSelectedTotpAuth] = React.useState<
UserTotpAuth | undefined
>(undefined);
const [selectedWebAuthn, setSelectedWebAuthn] = React.useState<
UserWebAuthn | undefined
>(undefined);
type TwoFactorMethod = {
type: "totp" | "webauthn";
item: UserTotpAuth | UserWebAuthn;
};
const twoFactorMethods: TwoFactorMethod[] = [
...totpAuthList.map((item: UserTotpAuth) => {
return { type: "totp" as const, item };
}),
...webAuthnList.map((item: UserWebAuthn) => {
return { type: "webauthn" as const, item };
}),
];
const [isTwoFactorAuthLoading, setIsTwoFactorAuthLoading] =
React.useState<boolean>(false);
const [twofactorAuthError, setTwoFactorAuthError] =
@@ -57,6 +80,96 @@ const LoginPage: () => JSX.Element = () => {
}
}, []);
useAsyncEffect(async () => {
if (selectedWebAuthn) {
setIsTwoFactorAuthLoading(true);
try {
const result: HTTPResponse<JSONObject> = await API.post({
url: GENERATE_WEBAUTHN_AUTH_OPTIONS_API_URL,
data: {
data: {
email: initialValues["email"],
},
},
});
if (result instanceof HTTPErrorResponse) {
throw result;
}
const data: any = result.data as any;
// Convert base64url strings back to Uint8Array
data.options.challenge = Base64.base64UrlToUint8Array(
data.options.challenge,
);
if (data.options.allowCredentials) {
data.options.allowCredentials.forEach((cred: any) => {
cred.id = Base64.base64UrlToUint8Array(cred.id);
});
}
// Use WebAuthn API
const credential: PublicKeyCredential =
(await navigator.credentials.get({
publicKey: data.options,
})) as PublicKeyCredential;
const assertionResponse: AuthenticatorAssertionResponse =
credential.response as AuthenticatorAssertionResponse;
// Verify
const verifyResult: HTTPResponse<JSONObject> = await API.post({
url: VERIFY_WEBAUTHN_AUTH_API_URL,
data: {
data: {
...initialValues,
challenge: data.challenge,
credential: {
id: credential.id,
rawId: Base64.uint8ArrayToBase64Url(
new Uint8Array(credential.rawId),
),
response: {
authenticatorData: Base64.uint8ArrayToBase64Url(
new Uint8Array(assertionResponse.authenticatorData),
),
clientDataJSON: Base64.uint8ArrayToBase64Url(
new Uint8Array(assertionResponse.clientDataJSON),
),
signature: Base64.uint8ArrayToBase64Url(
new Uint8Array(assertionResponse.signature),
),
userHandle: assertionResponse.userHandle
? Base64.uint8ArrayToBase64Url(
new Uint8Array(assertionResponse.userHandle),
)
: null,
},
type: credential.type,
},
},
},
});
if (verifyResult instanceof HTTPErrorResponse) {
throw verifyResult;
}
const user: User = User.fromJSON(
verifyResult.data as JSONObject,
User,
) as User;
const miscData: JSONObject = {};
login(user as User, miscData);
} catch (error) {
setTwoFactorAuthError(API.getFriendlyErrorMessage(error as Error));
}
setIsTwoFactorAuthLoading(false);
}
}, [selectedWebAuthn]);
type LoginFunction = (user: User, miscData: JSONObject) => void;
const login: LoginFunction = (user: User, miscData: JSONObject): void => {
@@ -156,16 +269,23 @@ const LoginPage: () => JSX.Element = () => {
) => {
if (
miscData &&
(miscData as JSONObject)["twoFactorAuth"] === true
((((miscData as JSONObject)["totpAuthList"] as JSONArray)
?.length || 0) > 0 ||
(((miscData as JSONObject)["webAuthnList"] as JSONArray)
?.length || 0) > 0)
) {
const twoFactorAuthList: Array<UserTwoFactorAuth> =
UserTwoFactorAuth.fromJSONArray(
(miscData as JSONObject)[
"twoFactorAuthList"
] as JSONArray,
UserTwoFactorAuth,
const totpAuthList: Array<UserTotpAuth> =
UserTotpAuth.fromJSONArray(
(miscData as JSONObject)["totpAuthList"] as JSONArray,
UserTotpAuth,
);
setTwoFactorAuthList(twoFactorAuthList);
const webAuthnList: Array<UserWebAuthn> =
UserWebAuthn.fromJSONArray(
(miscData as JSONObject)["webAuthnList"] as JSONArray,
UserWebAuthn,
);
setTotpAuthList(totpAuthList);
setWebAuthnList(webAuthnList);
setShowTwoFactorAuth(true);
return;
}
@@ -187,19 +307,53 @@ const LoginPage: () => JSX.Element = () => {
/>
)}
{showTwoFactorAuth && !selectedTwoFactorAuth && (
<StaticModelList<UserTwoFactorAuth>
titleField="name"
descriptionField=""
selectedItems={[]}
list={twoFactorAuthList}
onClick={(item: UserTwoFactorAuth) => {
setSelectedTwoFactorAuth(item);
}}
/>
{showTwoFactorAuth && !selectedTotpAuth && !selectedWebAuthn && (
<div className="space-y-4">
{twoFactorMethods.map(
(method: TwoFactorMethod, index: number) => {
return (
<div
key={index}
className="cursor-pointer p-4 border border-gray-300 rounded-lg hover:bg-gray-50"
onClick={() => {
if (method.type === "totp") {
setSelectedTotpAuth(method.item as UserTotpAuth);
} else {
setSelectedWebAuthn(method.item as UserWebAuthn);
}
}}
>
<div className="font-medium">
{(method.item as any).name}
</div>
<div className="text-sm text-gray-500">
{method.type === "totp"
? "Authenticator App"
: "Security Key"}
</div>
</div>
);
},
)}
</div>
)}
{showTwoFactorAuth && selectedTwoFactorAuth && (
{showTwoFactorAuth && selectedWebAuthn && (
<div className="text-center">
<div className="text-lg font-medium mb-4">
Authenticating with Security Key
</div>
<div className="text-sm text-gray-500 mb-4">
Please follow the instructions on your security key device.
</div>
{isTwoFactorAuthLoading && <ComponentLoader />}
{twofactorAuthError && (
<ErrorMessage message={twofactorAuthError} />
)}
</div>
)}
{showTwoFactorAuth && selectedTotpAuth && (
<BasicForm
id="two-factor-auth-form"
name="Two Factor Auth"
@@ -225,14 +379,17 @@ const LoginPage: () => JSX.Element = () => {
try {
const code: string = data["code"] as string;
const twoFactorAuthId: string =
selectedTwoFactorAuth.id?.toString() as string;
selectedTotpAuth!.id?.toString() as string;
const result: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.post(VERIFY_TWO_FACTOR_AUTH_API_URL, {
await API.post({
url: VERIFY_TOTP_AUTH_API_URL,
data: {
...initialValues,
code: code,
twoFactorAuthId: twoFactorAuthId,
data: {
...initialValues,
code: code,
twoFactorAuthId: twoFactorAuthId,
},
},
});
@@ -261,7 +418,7 @@ const LoginPage: () => JSX.Element = () => {
)}
</div>
<div className="mt-10 text-center">
{!selectedTwoFactorAuth && (
{!selectedTotpAuth && !selectedWebAuthn && (
<div className="text-muted mb-0 text-gray-500">
Don&apos;t have an account?{" "}
<Link
@@ -272,11 +429,12 @@ const LoginPage: () => JSX.Element = () => {
</Link>
</div>
)}
{selectedTwoFactorAuth ? (
{selectedTotpAuth || selectedWebAuthn ? (
<div className="text-muted mb-0 text-gray-500">
<Link
onClick={() => {
setSelectedTwoFactorAuth(undefined);
setSelectedTotpAuth(undefined);
setSelectedWebAuthn(undefined);
}}
className="text-indigo-500 hover:text-indigo-900 cursor-pointer"
>

View File

@@ -42,12 +42,12 @@ const LoginPage: () => JSX.Element = () => {
try {
// get sso config by email.
const listResult: HTTPErrorResponse | HTTPResponse<JSONArray> =
await API.get(
URL.fromString(apiUrl.toString()).addQueryParam(
await API.get({
url: URL.fromString(apiUrl.toString()).addQueryParam(
"email",
email.toString(),
),
);
});
if (listResult instanceof HTTPErrorResponse) {
throw listResult;

View File

@@ -1,6 +1,6 @@
import Route from "Common/Types/API/Route";
import URL from "Common/Types/API/URL";
import { IDENTITY_URL } from "Common/UI/Config";
import { IDENTITY_URL, APP_API_URL } from "Common/UI/Config";
export const SIGNUP_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route("/signup"),
@@ -9,9 +9,17 @@ export const LOGIN_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route("/login"),
);
export const VERIFY_TWO_FACTOR_AUTH_API_URL: URL = URL.fromURL(
export const VERIFY_TOTP_AUTH_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute(
new Route("/verify-totp-auth"),
);
export const GENERATE_WEBAUTHN_AUTH_OPTIONS_API_URL: URL = URL.fromURL(
APP_API_URL,
).addRoute(new Route("/user-webauthn/generate-authentication-options"));
export const VERIFY_WEBAUTHN_AUTH_API_URL: URL = URL.fromURL(
IDENTITY_URL,
).addRoute(new Route("/verify-two-factor-auth"));
).addRoute(new Route("/verify-webauthn-auth"));
export const SERVICE_PROVIDER_LOGIN_URL: URL = URL.fromURL(
IDENTITY_URL,

View File

@@ -3,7 +3,7 @@
#
# Pull base image nodejs image.
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
RUN npm config set fetch-retries 5
@@ -17,6 +17,7 @@ ARG APP_VERSION
ENV GIT_SHA=${GIT_SHA}
ENV APP_VERSION=${APP_VERSION}
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# IF APP_VERSION is not set, set it to 1.0.0

View File

@@ -1,7 +1,10 @@
{
"watch": ["./","../Common/UI", "../Common/Types", "../Common/Utils", "../Common/Models"],
"ext": "ts,json,tsx,env,js,jsx,hbs",
"ext": "ts,tsx",
"ignore": [
"./node_modules/**",
"./public/**",
"./bin/**",
"./public/**",
"./public/dist/**",
"./build/*",

View File

@@ -69,6 +69,7 @@
"crypto-js": "^4.2.0",
"dotenv": "^16.4.4",
"ejs": "^3.1.10",
"elkjs": "^0.10.0",
"esbuild": "^0.25.5",
"express": "^4.21.1",
"formik": "^2.4.6",

View File

@@ -5,6 +5,7 @@ import Projects from "./Pages/Projects/Index";
import SettingsAPIKey from "./Pages/Settings/APIKey/Index";
import SettingsAuthentication from "./Pages/Settings/Authentication/Index";
import SettingsCallSMS from "./Pages/Settings/CallSMS/Index";
import SettingsWhatsApp from "./Pages/Settings/WhatsApp/Index";
// Settings Pages.
import SettingsEmail from "./Pages/Settings/Email/Index";
import SettingsProbes from "./Pages/Settings/Probes/Index";
@@ -105,6 +106,11 @@ const App: () => JSX.Element = () => {
element={<SettingsCallSMS />}
/>
<PageRoute
path={RouteMap[PageMap.SETTINGS_WHATSAPP]?.toString() || ""}
element={<SettingsWhatsApp />}
/>
<PageRoute
path={RouteMap[PageMap.SETTINGS_PROBES]?.toString() || ""}
element={<SettingsProbes />}

View File

@@ -54,9 +54,9 @@ const DashboardFooter: () => JSX.Element = () => {
const fetchAppVersion: (appName: string) => Promise<JSONObject> = async (
appName: string,
): Promise<JSONObject> => {
const response: HTTPResponse<JSONObject> = await API.get<JSONObject>(
URL.fromString(`${HTTP_PROTOCOL}/${HOST}${appName}/version`),
);
const response: HTTPResponse<JSONObject> = await API.get<JSONObject>({
url: URL.fromString(`${HTTP_PROTOCOL}/${HOST}${appName}/version`),
});
if (response.data) {
return response.data as JSONObject;

View File

@@ -50,6 +50,15 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
}}
icon={IconProp.Call}
/>
<SideMenuItem
link={{
title: "WhatsApp",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_WHATSAPP] as Route,
),
}}
icon={IconProp.WhatsApp}
/>
</SideMenuSection>
<SideMenuSection title="Monitoring">

View File

@@ -0,0 +1,454 @@
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import DashboardSideMenu from "../SideMenu";
import Route from "Common/Types/API/Route";
import ObjectID from "Common/Types/ObjectID";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
import Page from "Common/UI/Components/Page/Page";
import FieldType from "Common/UI/Components/Types/FieldType";
import GlobalConfig from "Common/Models/DatabaseModels/GlobalConfig";
import React, { FunctionComponent, ReactElement, useState } from "react";
import Card from "Common/UI/Components/Card/Card";
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/MarkdownViewer";
import BasicForm from "Common/UI/Components/Forms/BasicForm";
import Alert, { AlertType } from "Common/UI/Components/Alerts/Alert";
import { JSONObject } from "Common/Types/JSON";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
import API from "Common/UI/Utils/API/API";
import { APP_API_URL } from "Common/UI/Config";
import WhatsAppTemplateMessages, {
WhatsAppTemplateId,
WhatsAppTemplateIds,
WhatsAppTemplateLanguage,
} from "Common/Types/WhatsApp/WhatsAppTemplates";
type ToFriendlyName = (value: string) => string;
const toFriendlyName: ToFriendlyName = (value: string): string => {
return value
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/_/g, " ")
.replace(/\s+/g, " ")
.trim();
};
type ExtractTemplateVariables = (template: string) => Array<string>;
const extractTemplateVariables: ExtractTemplateVariables = (
template: string,
): Array<string> => {
const matches: RegExpMatchArray | null = template.match(/\{\{(.*?)\}\}/g);
if (!matches) {
return [];
}
const uniqueVariables: Set<string> = new Set<string>();
for (const match of matches) {
const variable: string = match.replace("{{", "").replace("}}", "").trim();
if (variable) {
uniqueVariables.add(variable);
}
}
return Array.from(uniqueVariables).sort((a: string, b: string) => {
return a.localeCompare(b);
});
};
type BuildWhatsAppSetupMarkdown = () => string;
const buildWhatsAppSetupMarkdown: BuildWhatsAppSetupMarkdown = (): string => {
const templateKeys: Array<keyof typeof WhatsAppTemplateIds> = Object.keys(
WhatsAppTemplateIds,
) as Array<keyof typeof WhatsAppTemplateIds>;
const description: string =
"Follow these steps to connect Meta WhatsApp with OneUptime so notifications can be delivered via WhatsApp.";
const prerequisitesList: Array<string> = [
"Meta Business Manager admin access for the WhatsApp Business Account.",
"A WhatsApp Business phone number approved for API messaging.",
"Admin access to OneUptime with permission to edit global notification settings.",
];
const setupStepsList: Array<string> = [
"Sign in to the [Meta Business Manager](https://business.facebook.com/) with admin access to your WhatsApp Business Account.",
"From **Business Settings → Accounts → WhatsApp Accounts**, create or select the account that owns your sender phone number.",
"In Buisness Portfolio, create a system user and assign it to the WhatsApp Business Account with the role of **Admin**.",
"Generate a token for this system user and this will be your long-lived access token. Make sure to select the **whatsapp_business_management** and **whatsapp_business_messaging** permissions when generating the token.",
"Paste the access token and phone number ID into the **Meta WhatsApp Settings** card above, then save.",
"For the **Business Account ID**, go to **Business Settings → Business Info** (or **Business Settings → WhatsApp Accounts → Settings**) and copy the **WhatsApp Business Account ID** value.",
"To locate the **App ID** and **App Secret**, open [Meta for Developers](https://developers.facebook.com/apps/), select your WhatsApp app, then navigate to **Settings → Basic**. The App ID is shown at the top; click **Show** next to **App Secret** to reveal and copy it.",
"Create each template listed below in the Meta WhatsApp Manager. Make sure the template name, language, and variables match exactly. You can however change the content to your preference. Please make sure it's approved by Meta.",
"Send a test notification from OneUptime to confirm that WhatsApp delivery succeeds.",
];
const prerequisitesMarkdown: string = prerequisitesList
.map((item: string) => {
return `- ${item}`;
})
.join("\n");
const setupStepsMarkdown: string = setupStepsList
.map((item: string, index: number) => {
return `${index + 1}. ${item}`;
})
.join("\n");
const tableRows: string = templateKeys
.map((enumKey: keyof typeof WhatsAppTemplateIds) => {
const templateId: WhatsAppTemplateId = WhatsAppTemplateIds[enumKey];
const friendlyName: string = toFriendlyName(enumKey.toString());
const templateMessage: string = WhatsAppTemplateMessages[templateId];
const language: string = WhatsAppTemplateLanguage[templateId] || "en";
const variables: Array<string> =
extractTemplateVariables(templateMessage);
const variableList: string =
variables.length > 0
? variables
.map((variable: string) => {
return `\`${variable}\``;
})
.join(", ")
: "_None_";
return `| ${friendlyName} | \`${templateId}\` | ${language} | ${variableList} |`;
})
.join("\n");
const templateBodies: string = templateKeys
.map((enumKey: keyof typeof WhatsAppTemplateIds) => {
const templateId: WhatsAppTemplateId = WhatsAppTemplateIds[enumKey];
const friendlyName: string = toFriendlyName(enumKey.toString());
const templateMessage: string = WhatsAppTemplateMessages[templateId];
const language: string = WhatsAppTemplateLanguage[templateId] || "en";
const variables: Array<string> =
extractTemplateVariables(templateMessage);
const variableMarkdown: string =
variables.length > 0
? variables
.map((variable: string) => {
return `- \`${variable}\``;
})
.join("\n")
: "_None_";
const variablesHeading: string = variables.length
? `**Variables (${variables.length})**`
: "**Variables**";
return [
`#### ${friendlyName}`,
"",
`**Template Name:** \`${templateId}\``,
`**Language:** ${language}`,
"",
variablesHeading,
variableMarkdown,
"",
"**Body**",
"```plaintext",
templateMessage,
"```",
"",
"---",
].join("\n");
})
.join("\n\n");
const templateSummaryTable: string = [
"| Friendly Name | Template Name | Language | Variables |",
"| --- | --- | --- | --- |",
tableRows,
]
.filter(Boolean)
.join("\n");
return [
description,
"### Prerequisites",
prerequisitesMarkdown,
"### Setup Steps",
setupStepsMarkdown,
"### Required WhatsApp Templates",
templateSummaryTable,
"### Template Bodies",
"> Copy the exact template body below—including punctuation and spacing—when creating each template inside Meta. The variables list shows every placeholder that must be configured in WhatsApp Manager.",
templateBodies,
]
.filter(Boolean)
.join("\n\n");
};
const whatsappSetupMarkdown: string = buildWhatsAppSetupMarkdown();
const SettingsWhatsApp: FunctionComponent = (): ReactElement => {
const [isSendingTest, setIsSendingTest] = useState<boolean>(false);
const [testError, setTestError] = useState<string>("");
const [testSuccess, setTestSuccess] = useState<string>("");
return (
<Page
title={"Admin Settings"}
breadcrumbLinks={[
{
title: "Admin Dashboard",
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
},
{
title: "Settings",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS] as Route,
),
},
{
title: "WhatsApp",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_WHATSAPP] as Route,
),
},
]}
sideMenu={<DashboardSideMenu />}
>
<CardModelDetail
name="Meta WhatsApp Settings"
cardProps={{
title: "Meta WhatsApp Settings",
description:
"Configure Meta WhatsApp credentials. These values are used to send WhatsApp notifications from OneUptime.",
}}
isEditable={true}
editButtonText="Edit Meta WhatsApp Config"
formSteps={[
{
title: "Credentials",
id: "meta-credentials",
},
{
title: "Meta App",
id: "meta-app",
},
]}
formFields={[
{
field: {
metaWhatsAppAccessToken: true,
},
title: "Access Token",
stepId: "meta-credentials",
fieldType: FormFieldSchemaType.EncryptedText,
required: true,
description:
"Long-lived access token generated in the Meta WhatsApp Business Platform.",
placeholder: "EAAG...",
},
{
field: {
metaWhatsAppPhoneNumberId: true,
},
title: "Phone Number ID",
stepId: "meta-credentials",
fieldType: FormFieldSchemaType.Text,
required: true,
description:
"The WhatsApp Business phone number ID associated with your sending number.",
placeholder: "123456789012345",
},
{
field: {
metaWhatsAppBusinessAccountId: true,
},
title: "Business Account ID",
stepId: "meta-credentials",
fieldType: FormFieldSchemaType.Text,
required: false,
description:
"Optional Business Account ID that owns the WhatsApp templates.",
placeholder: "123456789012345",
},
{
field: {
metaWhatsAppAppId: true,
},
title: "App ID",
stepId: "meta-app",
fieldType: FormFieldSchemaType.Text,
required: false,
description:
"Optional Facebook App ID tied to your WhatsApp integration.",
placeholder: "987654321098765",
},
{
field: {
metaWhatsAppAppSecret: true,
},
title: "App Secret",
stepId: "meta-app",
fieldType: FormFieldSchemaType.EncryptedText,
required: false,
description:
"Optional Facebook App Secret used for webhook signature verification.",
placeholder: "Facebook App Secret",
},
]}
modelDetailProps={{
modelType: GlobalConfig,
id: "model-detail-global-config-meta-whatsapp",
fields: [
{
field: {
metaWhatsAppAccessToken: true,
},
title: "Access Token",
fieldType: FieldType.HiddenText,
placeholder: "Not Configured",
},
{
field: {
metaWhatsAppPhoneNumberId: true,
},
title: "Phone Number ID",
fieldType: FieldType.Text,
placeholder: "Not Configured",
},
{
field: {
metaWhatsAppBusinessAccountId: true,
},
title: "Business Account ID",
fieldType: FieldType.Text,
placeholder: "Not Configured",
},
{
field: {
metaWhatsAppAppId: true,
},
title: "App ID",
fieldType: FieldType.Text,
placeholder: "Not Configured",
},
{
field: {
metaWhatsAppAppSecret: true,
},
title: "App Secret",
fieldType: FieldType.HiddenText,
placeholder: "Not Configured",
},
],
modelId: ObjectID.getZeroObjectID(),
}}
/>
<Card
title="Send Test WhatsApp Message"
description="Send a test WhatsApp template message to confirm your Meta configuration."
>
{testSuccess ? (
<Alert
type={AlertType.SUCCESS}
title={testSuccess}
className="mb-4"
/>
) : (
<></>
)}
<BasicForm
id="send-test-whatsapp-form"
name="Send Test WhatsApp Message"
isLoading={isSendingTest}
error={testError || ""}
submitButtonText="Send Test Message"
maxPrimaryButtonWidth={true}
initialValues={{
phoneNumber: "",
}}
fields={[
{
field: {
phoneNumber: true,
},
title: "Recipient WhatsApp Number",
description:
"Enter the full international phone number (including country code) that should receive the test message.",
placeholder: "+11234567890",
required: true,
fieldType: FormFieldSchemaType.Phone,
disableSpellCheck: true,
},
]}
onSubmit={async (
values: JSONObject,
onSubmitSuccessful?: () => void,
) => {
const toPhone: string = String(values["phoneNumber"] || "").trim();
if (!toPhone) {
setTestSuccess("");
setTestError(
"Please enter a WhatsApp number before sending a test message.",
);
return;
}
setIsSendingTest(true);
setTestError("");
setTestSuccess("");
try {
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.post({
url: URL.fromString(APP_API_URL.toString()).addRoute(
"/notification/whatsapp/test",
),
data: {
toPhone,
templateKey: WhatsAppTemplateIds.TestNotification,
templateLanguageCode:
WhatsAppTemplateLanguage[
WhatsAppTemplateIds.TestNotification
],
},
});
if (response instanceof HTTPErrorResponse) {
throw response;
}
if (response.isFailure()) {
throw new Error("Failed to send test WhatsApp message.");
}
setTestSuccess(
"Test WhatsApp message sent successfully. Check the recipient device to confirm delivery.",
);
if (onSubmitSuccessful) {
onSubmitSuccessful();
}
} catch (err) {
setTestError(API.getFriendlyMessage(err));
} finally {
setIsSendingTest(false);
}
}}
/>
</Card>
<Card
title="Meta WhatsApp Setup Guide"
description="Steps to connect Meta WhatsApp and the templates you must provision."
>
<MarkdownViewer text={whatsappSetupMarkdown} />
</Card>
</Page>
);
};
export default SettingsWhatsApp;

View File

@@ -15,6 +15,7 @@ enum PageMap {
SETTINGS_HOST = "SETTINGS_HOST",
SETTINGS_SMTP = "SETTINGS_SMTP",
SETTINGS_CALL_AND_SMS = "SETTINGS_CALL_AND_SMS",
SETTINGS_WHATSAPP = "SETTINGS_WHATSAPP",
SETTINGS_PROBES = "SETTINGS_PROBES",
SETTINGS_AUTHENTICATION = "SETTINGS_AUTHENTICATION",
SETTINGS_API_KEY = "SETTINGS_API_KEY",

View File

@@ -25,6 +25,7 @@ const RouteMap: Dictionary<Route> = {
[PageMap.SETTINGS_HOST]: new Route(`/admin/settings/host`),
[PageMap.SETTINGS_SMTP]: new Route(`/admin/settings/smtp`),
[PageMap.SETTINGS_CALL_AND_SMS]: new Route(`/admin/settings/call-and-sms`),
[PageMap.SETTINGS_WHATSAPP]: new Route(`/admin/settings/whatsapp`),
[PageMap.SETTINGS_PROBES]: new Route(`/admin/settings/probes`),
[PageMap.SETTINGS_AUTHENTICATION]: new Route(
`/admin/settings/authentication`,

View File

@@ -3,7 +3,7 @@
#
# Pull base image nodejs image.
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
FROM public.ecr.aws/docker/library/node:24.9-alpine3.21
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
RUN npm config set fetch-retries 5
@@ -17,6 +17,7 @@ ARG APP_VERSION
ENV GIT_SHA=${GIT_SHA}
ENV APP_VERSION=${APP_VERSION}
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# IF APP_VERSION is not set, set it to 1.0.0

View File

@@ -1,5 +1,6 @@
import BaseAPI from "Common/Server/API/BaseAPI";
import BaseAnalyticsAPI from "Common/Server/API/BaseAnalyticsAPI";
import BillingAPI from "Common/Server/API/BillingAPI";
import BillingInvoiceAPI from "Common/Server/API/BillingInvoiceAPI";
import BillingPaymentMethodAPI from "Common/Server/API/BillingPaymentMethodAPI";
import CopilotCodeRepositoryAPI from "Common/Server/API/CopilotCodeRepositoryAPI";
@@ -23,12 +24,14 @@ import WorkspaceNotificationRuleAPI from "Common/Server/API/WorkspaceNotificatio
import StatusPageDomainAPI from "Common/Server/API/StatusPageDomainAPI";
import StatusPageSubscriberAPI from "Common/Server/API/StatusPageSubscriberAPI";
import UserCallAPI from "Common/Server/API/UserCallAPI";
import UserTwoFactorAuthAPI from "Common/Server/API/UserTwoFactorAuthAPI";
import UserTotpAuthAPI from "Common/Server/API/UserTotpAuthAPI";
import UserWebAuthnAPI from "Common/Server/API/UserWebAuthnAPI";
import MonitorTest from "Common/Models/DatabaseModels/MonitorTest";
// User Notification methods.
import UserEmailAPI from "Common/Server/API/UserEmailAPI";
import UserNotificationLogTimelineAPI from "Common/Server/API/UserOnCallLogTimelineAPI";
import UserSMSAPI from "Common/Server/API/UserSmsAPI";
import UserWhatsAppAPI from "Common/Server/API/UserWhatsAppAPI";
import UserPushAPI from "Common/Server/API/UserPushAPI";
import ApiKeyPermissionService, {
Service as ApiKeyPermissionServiceType,
@@ -282,6 +285,9 @@ import ShortLinkService, {
import SmsLogService, {
Service as SmsLogServiceType,
} from "Common/Server/Services/SmsLogService";
import WhatsAppLogService, {
Service as WhatsAppLogServiceType,
} from "Common/Server/Services/WhatsAppLogService";
import PushNotificationLogService, {
Service as PushNotificationLogServiceType,
} from "Common/Server/Services/PushNotificationLogService";
@@ -327,6 +333,9 @@ import TeamMemberService, {
import TeamPermissionService, {
Service as TeamPermissionServiceType,
} from "Common/Server/Services/TeamPermissionService";
import TeamComplianceSettingService, {
TeamComplianceSettingService as TeamComplianceSettingServiceType,
} from "Common/Server/Services/TeamComplianceSettingService";
import TeamService, {
Service as TeamServiceType,
} from "Common/Server/Services/TeamService";
@@ -453,6 +462,7 @@ import ServiceCatalogOwnerUser from "Common/Models/DatabaseModels/ServiceCatalog
import ServiceCopilotCodeRepository from "Common/Models/DatabaseModels/ServiceCopilotCodeRepository";
import ShortLink from "Common/Models/DatabaseModels/ShortLink";
import SmsLog from "Common/Models/DatabaseModels/SmsLog";
import WhatsAppLog from "Common/Models/DatabaseModels/WhatsAppLog";
import StatusPageAnnouncement from "Common/Models/DatabaseModels/StatusPageAnnouncement";
// Custom Fields API
import StatusPageCustomField from "Common/Models/DatabaseModels/StatusPageCustomField";
@@ -469,6 +479,7 @@ import StatusPageSSO from "Common/Models/DatabaseModels/StatusPageSso";
import Team from "Common/Models/DatabaseModels/Team";
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
import TeamPermission from "Common/Models/DatabaseModels/TeamPermission";
import TeamComplianceSetting from "Common/Models/DatabaseModels/TeamComplianceSetting";
import TelemetryService from "Common/Models/DatabaseModels/TelemetryService";
import TelemetryUsageBilling from "Common/Models/DatabaseModels/TelemetryUsageBilling";
import User from "Common/Models/DatabaseModels/User";
@@ -521,6 +532,7 @@ import ScheduledMaintenanceFeedService, {
} from "Common/Server/Services/ScheduledMaintenanceFeedService";
import SlackAPI from "Common/Server/API/SlackAPI";
import MicrosoftTeamsAPI from "Common/Server/API/MicrosoftTeamsAPI";
import WorkspaceProjectAuthToken from "Common/Models/DatabaseModels/WorkspaceProjectAuthToken";
import WorkspaceProjectAuthTokenService, {
@@ -550,10 +562,7 @@ import MetricTypeService, {
import MetricType from "Common/Models/DatabaseModels/MetricType";
import OnCallDutyPolicyAPI from "Common/Server/API/OnCallDutyPolicyAPI";
// OnCallDutyPolicyOwnerTeam
// OnCallDutyPolicyOwnerUser
// OnCallDutyPolicyFeed
import TeamComplianceAPI from "Common/Server/API/TeamComplianceAPI";
import OnCallDutyPolicyFeed from "Common/Models/DatabaseModels/OnCallDutyPolicyFeed";
import OnCallDutyPolicyFeedService, {
@@ -1167,6 +1176,14 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<TeamComplianceSetting, TeamComplianceSettingServiceType>(
TeamComplianceSetting,
TeamComplianceSettingService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<MonitorStatus, MonitorStatusServiceType>(
@@ -1526,6 +1543,14 @@ const BaseAPIFeatureSet: FeatureSet = {
new BaseAPI<SmsLog, SmsLogServiceType>(SmsLog, SmsLogService).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<WhatsAppLog, WhatsAppLogServiceType>(
WhatsAppLog,
WhatsAppLogService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<PushNotificationLog, PushNotificationLogServiceType>(
@@ -1581,7 +1606,6 @@ const BaseAPIFeatureSet: FeatureSet = {
MonitorTimelineStatusService,
).getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new ShortLinkAPI().getRouter());
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new MonitorAPI().getRouter());
app.use(
@@ -1595,6 +1619,12 @@ const BaseAPIFeatureSet: FeatureSet = {
new OnCallDutyPolicyAPI().getRouter(),
);
// TeamComplianceAPI
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new TeamComplianceAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new WorkspaceNotificationRuleAPI().getRouter(),
@@ -1619,6 +1649,10 @@ const BaseAPIFeatureSet: FeatureSet = {
new ResellerPlanAPI().getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new SlackAPI().getRouter());
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new MicrosoftTeamsAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new GlobalConfigAPI().getRouter(),
@@ -1646,10 +1680,18 @@ const BaseAPIFeatureSet: FeatureSet = {
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserCallAPI().getRouter());
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new UserTwoFactorAuthAPI().getRouter(),
new UserTotpAuthAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new UserWebAuthnAPI().getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserEmailAPI().getRouter());
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserSMSAPI().getRouter());
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new UserWhatsAppAPI().getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserPushAPI().getRouter());
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new ProbeAPI().getRouter());
@@ -1670,6 +1712,8 @@ const BaseAPIFeatureSet: FeatureSet = {
new BillingInvoiceAPI().getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new BillingAPI().getRouter());
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<

View File

@@ -24,7 +24,7 @@ import AccessTokenService from "Common/Server/Services/AccessTokenService";
import EmailVerificationTokenService from "Common/Server/Services/EmailVerificationTokenService";
import MailService from "Common/Server/Services/MailService";
import UserService from "Common/Server/Services/UserService";
import UserTwoFactorAuthService from "Common/Server/Services/UserTwoFactorAuthService";
import UserTotpAuthService from "Common/Server/Services/UserTotpAuthService";
import CookieUtil from "Common/Server/Utils/Cookie";
import Express, {
ExpressRequest,
@@ -34,10 +34,12 @@ import Express, {
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
import TwoFactorAuth from "Common/Server/Utils/TwoFactorAuth";
import TotpAuth from "Common/Server/Utils/TotpAuth";
import EmailVerificationToken from "Common/Models/DatabaseModels/EmailVerificationToken";
import User from "Common/Models/DatabaseModels/User";
import UserTwoFactorAuth from "Common/Models/DatabaseModels/UserTwoFactorAuth";
import UserTotpAuth from "Common/Models/DatabaseModels/UserTotpAuth";
import UserWebAuthn from "Common/Models/DatabaseModels/UserWebAuthn";
import UserWebAuthnService from "Common/Server/Services/UserWebAuthnService";
const router: ExpressRouter = Express.getRouter();
@@ -503,7 +505,7 @@ router.post(
);
router.post(
"/verify-two-factor-auth",
"/verify-totp-auth",
async (
req: ExpressRequest,
res: ExpressResponse,
@@ -513,7 +515,25 @@ router.post(
req: req,
res: res,
next: next,
verifyTwoFactorAuth: true,
verifyTotpAuth: true,
verifyWebAuthn: false,
});
},
);
router.post(
"/verify-webauthn-auth",
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
return login({
req: req,
res: res,
next: next,
verifyTotpAuth: false,
verifyWebAuthn: true,
});
},
);
@@ -529,62 +549,99 @@ router.post(
req: req,
res: res,
next: next,
verifyTwoFactorAuth: false,
verifyTotpAuth: false,
verifyWebAuthn: false,
});
},
);
type FetchTwoFactorAuthListFunction = (
userId: ObjectID,
) => Promise<Array<UserTwoFactorAuth>>;
type FetchTotpAuthListFunction = (userId: ObjectID) => Promise<{
totpAuthList: Array<UserTotpAuth>;
webAuthnList: Array<UserWebAuthn>;
}>;
const fetchTwoFactorAuthList: FetchTwoFactorAuthListFunction = async (
const fetchTotpAuthList: FetchTotpAuthListFunction = async (
userId: ObjectID,
): Promise<Array<UserTwoFactorAuth>> => {
const twoFactorAuthList: Array<UserTwoFactorAuth> =
await UserTwoFactorAuthService.findBy({
query: {
userId: userId,
isVerified: true,
},
select: {
_id: true,
userId: true,
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
props: {
isRoot: true,
},
});
): Promise<{
totpAuthList: Array<UserTotpAuth>;
webAuthnList: Array<UserWebAuthn>;
}> => {
const totpAuthList: Array<UserTotpAuth> = await UserTotpAuthService.findBy({
query: {
userId: userId,
isVerified: true,
},
select: {
_id: true,
userId: true,
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
props: {
isRoot: true,
},
});
return twoFactorAuthList;
const webAuthnList: Array<UserWebAuthn> = await UserWebAuthnService.findBy({
query: {
userId: userId,
isVerified: true,
},
select: {
_id: true,
userId: true,
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
props: {
isRoot: true,
},
});
return {
totpAuthList: totpAuthList || [],
webAuthnList: webAuthnList || [],
};
};
type LoginFunction = (options: {
req: ExpressRequest;
res: ExpressResponse;
next: NextFunction;
verifyTwoFactorAuth: boolean;
verifyTotpAuth: boolean;
verifyWebAuthn: boolean;
}) => Promise<void>;
const login: LoginFunction = async (options: {
req: ExpressRequest;
res: ExpressResponse;
next: NextFunction;
verifyTwoFactorAuth: boolean;
verifyTotpAuth: boolean;
verifyWebAuthn: boolean;
}): Promise<void> => {
const req: ExpressRequest = options.req;
const res: ExpressResponse = options.res;
const next: NextFunction = options.next;
const verifyTwoFactorAuth: boolean = options.verifyTwoFactorAuth;
const verifyTotpAuth: boolean = options.verifyTotpAuth;
const verifyWebAuthn: boolean = options.verifyWebAuthn;
try {
const data: JSONObject = req.body["data"];
logger.debug("Login request data: " + JSON.stringify(req.body, null, 2));
const user: User = BaseModel.fromJSON(data as JSONObject, User) as User;
if (!user.email || !user.password) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Email and password are required."),
);
}
await user.password?.hashValue(EncryptionSecret);
const alreadySavedUser: User | null = await UserService.findOneBy({
@@ -638,13 +695,21 @@ const login: LoginFunction = async (options: {
);
}
if (alreadySavedUser.enableTwoFactorAuth && !verifyTwoFactorAuth) {
if (
alreadySavedUser.enableTwoFactorAuth &&
!verifyTotpAuth &&
!verifyWebAuthn
) {
// If two factor auth is enabled then we will send the user to the two factor auth page.
const twoFactorAuthList: Array<UserTwoFactorAuth> =
await fetchTwoFactorAuthList(alreadySavedUser.id!);
const { totpAuthList, webAuthnList } = await fetchTotpAuthList(
alreadySavedUser.id!,
);
if (!twoFactorAuthList || twoFactorAuthList.length === 0) {
if (
(!totpAuthList || totpAuthList.length === 0) &&
(!webAuthnList || webAuthnList.length === 0)
) {
const errorMessage: string = IsBillingEnabled
? "Two Factor Authentication is enabled but no two factor auth is setup. Please contact OneUptime support for help."
: "Two Factor Authentication is enabled but no two factor auth is setup. Please contact your server admin to disable two factor auth for this account.";
@@ -656,62 +721,68 @@ const login: LoginFunction = async (options: {
);
}
return Response.sendEntityResponse(req, res, user, User, {
return Response.sendEntityResponse(req, res, alreadySavedUser, User, {
miscData: {
twoFactorAuthList: UserTwoFactorAuth.toJSONArray(
twoFactorAuthList,
UserTwoFactorAuth,
),
twoFactorAuth: true,
totpAuthList: UserTotpAuth.toJSONArray(totpAuthList, UserTotpAuth),
webAuthnList: UserWebAuthn.toJSONArray(webAuthnList, UserWebAuthn),
},
});
}
if (verifyTwoFactorAuth) {
// code from req
const code: string = data["code"] as string;
const twoFactorAuthId: string = data["twoFactorAuthId"] as string;
if (verifyTotpAuth || verifyWebAuthn) {
if (verifyTotpAuth) {
// code from req
const code: string = data["code"] as string;
const twoFactorAuthId: string = data["twoFactorAuthId"] as string;
const twoFactorAuth: UserTwoFactorAuth | null =
await UserTwoFactorAuthService.findOneBy({
query: {
_id: twoFactorAuthId,
userId: alreadySavedUser.id!,
isVerified: true,
},
select: {
_id: true,
twoFactorSecret: true,
},
props: {
isRoot: true,
},
const totpAuth: UserTotpAuth | null =
await UserTotpAuthService.findOneBy({
query: {
_id: twoFactorAuthId,
userId: alreadySavedUser.id!,
isVerified: true,
},
select: {
_id: true,
twoFactorSecret: true,
},
props: {
isRoot: true,
},
});
if (!totpAuth) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid two factor auth id."),
);
}
const isVerified: boolean = TotpAuth.verifyToken({
token: code,
secret: totpAuth.twoFactorSecret!,
email: alreadySavedUser.email!,
});
if (!twoFactorAuth) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid two factor auth id."),
);
if (!isVerified) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code."),
);
}
} else if (verifyWebAuthn) {
const expectedChallenge: string = data["challenge"] as string;
const credential: any = data["credential"];
await UserWebAuthnService.verifyAuthentication({
userId: alreadySavedUser.id!.toString(),
challenge: expectedChallenge,
credential: credential,
});
}
const isVerified: boolean = TwoFactorAuth.verifyToken({
token: code,
secret: twoFactorAuth.twoFactorSecret!,
email: alreadySavedUser.email!,
});
if (!isVerified) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code."),
);
}
}
// Refresh Permissions for this user here.
} // Refresh Permissions for this user here.
await AccessTokenService.refreshUserAllPermissions(alreadySavedUser.id!);
if (alreadySavedUser.password.toString() === user.password!.toString()) {

File diff suppressed because it is too large Load Diff

View File

@@ -40,12 +40,18 @@ import Name from "Common/Types/Name";
const router: ExpressRouter = Express.getRouter();
// This route is used to get the SSO config for the user.
// when the user logs in from OneUptime and not from the IDP.
/*
* This route is used to get the SSO config for the user.
* when the user logs in from OneUptime and not from the IDP.
*/
router.get(
"/service-provider-login",
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
if (!req.query["email"]) {
return Response.sendErrorResponse(
@@ -150,7 +156,11 @@ router.get(
} catch (err) {
logger.error(err);
Response.sendErrorResponse(req, res, err as Exception);
if (err instanceof Exception) {
return next(err);
}
return next(new ServerException());
}
},
);
@@ -160,7 +170,7 @@ router.get(
async (
req: ExpressRequest,
res: ExpressResponse,
_next: NextFunction,
next: NextFunction,
): Promise<void> => {
try {
if (!req.params["projectId"]) {
@@ -236,22 +246,42 @@ router.get(
} catch (err) {
logger.error(err);
Response.sendErrorResponse(req, res, err as Exception);
if (err instanceof Exception) {
return next(err);
}
return next(new ServerException());
}
},
);
router.get(
"/idp-login/:projectId/:projectSsoId",
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
return await loginUserWithSso(req, res);
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
await loginUserWithSso(req, res);
} catch (err) {
return next(err);
}
},
);
router.post(
"/idp-login/:projectId/:projectSsoId",
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
return await loginUserWithSso(req, res);
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
await loginUserWithSso(req, res);
} catch (err) {
return next(err);
}
},
);
@@ -434,8 +464,10 @@ const loginUserWithSso: LoginUserWithSsoFunction = async (
isNewUser = true;
}
// If he does not then add him to teams that he should belong and log in.
// This should never happen because email is verified before he logs in with SSO.
/*
* If he does not then add him to teams that he should belong and log in.
* This should never happen because email is verified before he logs in with SSO.
*/
if (!alreadySavedUser.isEmailVerified && !isNewUser) {
await AuthenticationEmail.sendVerificationEmail(alreadySavedUser!);

View File

@@ -4,6 +4,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
OneUptimeRequest,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
@@ -19,7 +20,6 @@ import LIMIT_MAX, { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import {
formatUserForSCIM,
generateServiceProviderConfig,
logSCIMOperation,
} from "../Utils/SCIMUtils";
import Text from "Common/Types/Text";
import HashedString from "Common/Types/HashedString";
@@ -30,12 +30,14 @@ const router: ExpressRouter = Express.getRouter();
router.get(
"/status-page-scim/v2/:statusPageScimId/ServiceProviderConfig",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logSCIMOperation(
"ServiceProviderConfig",
"status-page",
req.params["statusPageScimId"]!,
logger.debug(
`Status Page SCIM ServiceProviderConfig - scimId: ${req.params["statusPageScimId"]!}`,
);
const serviceProviderConfig: JSONObject = generateServiceProviderConfig(
@@ -47,7 +49,7 @@ router.get(
return Response.sendJsonObjectResponse(req, res, serviceProviderConfig);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -56,7 +58,11 @@ router.get(
router.get(
"/status-page-scim/v2/:statusPageScimId/Users",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Users list request for statusPageScimId: ${req.params["statusPageScimId"]}`,
@@ -73,17 +79,54 @@ router.get(
parseInt(req.query["count"] as string) || 100,
LIMIT_PER_PROJECT,
);
const filter: string = req.query["filter"] as string;
logger.debug(
`Status Page SCIM Users - statusPageId: ${statusPageId}, startIndex: ${startIndex}, count: ${count}`,
`Status Page SCIM Users - statusPageId: ${statusPageId}, startIndex: ${startIndex}, count: ${count}, filter: ${filter || "none"}`,
);
// Build query for status page users
const query: any = {
statusPageId: statusPageId,
};
// Handle SCIM filter for userName
if (filter) {
const emailMatch: RegExpMatchArray | null = filter.match(
/userName eq "([^"]+)"/i,
);
if (emailMatch) {
const email: string = emailMatch[1]!;
logger.debug(
`Status Page SCIM Users list - statusPageScimId: ${req.params["statusPageScimId"]!}, filter by email: ${email}`,
);
if (email) {
if (Email.isValid(email)) {
query.email = new Email(email);
logger.debug(
`Status Page SCIM Users list - statusPageScimId: ${req.params["statusPageScimId"]!}, filtering by email: ${email}`,
);
} else {
logger.debug(
`Status Page SCIM Users list - statusPageScimId: ${req.params["statusPageScimId"]!}, invalid email format in filter: ${email}`,
);
return Response.sendJsonObjectResponse(req, res, {
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
totalResults: 0,
startIndex: startIndex,
itemsPerPage: 0,
Resources: [],
});
}
}
}
}
// Get all private users for this status page
const statusPageUsers: Array<StatusPagePrivateUser> =
await StatusPagePrivateUserService.findBy({
query: {
statusPageId: statusPageId,
},
query: query,
select: {
_id: true,
email: true,
@@ -130,7 +173,7 @@ router.get(
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -139,7 +182,11 @@ router.get(
router.get(
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Get individual user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
@@ -197,7 +244,7 @@ router.get(
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -206,7 +253,11 @@ router.get(
router.post(
"/status-page-scim/v2/:statusPageScimId/Users",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Create user request for statusPageScimId: ${req.params["statusPageScimId"]}`,
@@ -299,7 +350,7 @@ router.post(
return Response.sendJsonObjectResponse(req, res, createdUser);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -308,7 +359,11 @@ router.post(
router.put(
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Update user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
@@ -455,7 +510,7 @@ router.put(
return Response.sendJsonObjectResponse(req, res, user);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);
@@ -464,7 +519,11 @@ router.put(
router.delete(
"/status-page-scim/v2/:statusPageScimId/Users/:userId",
SCIMMiddleware.isAuthorizedSCIMRequest,
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
logger.debug(
`Status Page SCIM Delete user request for userId: ${req.params["userId"]}, statusPageScimId: ${req.params["statusPageScimId"]}`,
@@ -528,7 +587,7 @@ router.delete(
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(req, res, err as BadRequestException);
return next(err);
}
},
);

View File

@@ -115,7 +115,11 @@ router.get(
router.post(
"/status-page-idp-login/:statusPageId/:statusPageSsoId",
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const samlResponseBase64: string = req.body.SAMLResponse;
@@ -312,7 +316,11 @@ router.post(
);
} catch (err) {
logger.error(err);
Response.sendErrorResponse(req, res, new ServerException());
if (err instanceof Exception) {
return next(err);
}
return next(new ServerException());
}
},
);

View File

@@ -35,6 +35,8 @@ export default class AuthenticationEmail {
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
logger.debug("Sending verification email");
MailService.sendMail({
toEmail: user.email!,
subject: "Please verify email.",
@@ -50,8 +52,13 @@ export default class AuthenticationEmail {
).toString(),
homeUrl: new URL(httpProtocol, host).toString(),
},
}).catch((err: Error) => {
logger.error(err);
});
})
.then(() => {
logger.debug("Verification email sent");
})
.catch((err: Error) => {
logger.debug("Error sending verification email");
logger.error(err);
});
}
}

View File

@@ -227,6 +227,27 @@ export const generateUsersListResponse: (
};
};
/**
* Generate SCIM ListResponse for groups
*/
export const generateGroupsListResponse: (
groups: JSONObject[],
startIndex: number,
totalResults: number,
) => JSONObject = (
groups: JSONObject[],
startIndex: number,
totalResults: number,
): JSONObject => {
return {
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
totalResults: totalResults,
startIndex: startIndex,
itemsPerPage: groups.length,
Resources: groups,
};
};
/**
* Parse query parameters for SCIM list requests
*/
@@ -242,23 +263,3 @@ export const parseSCIMQueryParams: (req: ExpressRequest) => {
return { startIndex, count };
};
/**
* Log SCIM operation with consistent format
*/
export const logSCIMOperation: (
operation: string,
scimType: "project" | "status-page",
scimId: string,
details?: string,
) => void = (
operation: string,
scimType: "project" | "status-page",
scimId: string,
details?: string,
): void => {
const logPrefix: string =
scimType === "project" ? "Project SCIM" : "Status Page SCIM";
const message: string = `${logPrefix} ${operation} - scimId: ${scimId}${details ? `, ${details}` : ""}`;
logger.debug(message);
};

View File

@@ -184,15 +184,17 @@ export default class SSOUtil {
return null;
}
// get displayName attribute.
// {
// "$": {
// "Name": "http://schemas.microsoft.com/identity/claims/displayname"
// },
// "AttributeValue": [
// "Nawaz Dhandala"
// ]
// },
/*
* 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;

View File

@@ -12,6 +12,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
@@ -22,139 +23,146 @@ const router: ExpressRouter = Express.getRouter();
router.post(
"/make-call",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = JSONFunctions.deserialize(req.body);
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = JSONFunctions.deserialize(req.body);
await CallService.makeCall(body["callRequest"] as CallRequest, {
projectId: body["projectId"] as ObjectID,
isSensitive: (body["isSensitive"] as boolean) || false,
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
customTwilioConfig: body["customTwilioConfig"] as any,
incidentId: (body["incidentId"] as ObjectID) || undefined,
alertId: (body["alertId"] as ObjectID) || undefined,
scheduledMaintenanceId:
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
statusPageAnnouncementId:
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
userId: (body["userId"] as ObjectID) || undefined,
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
onCallPolicyEscalationRuleId:
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
onCallDutyPolicyExecutionLogTimelineId:
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
undefined,
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
teamId: (body["teamId"] as ObjectID) || undefined,
});
await CallService.makeCall(body["callRequest"] as CallRequest, {
projectId: body["projectId"] as ObjectID,
isSensitive: (body["isSensitive"] as boolean) || false,
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
customTwilioConfig: body["customTwilioConfig"] as any,
incidentId: (body["incidentId"] as ObjectID) || undefined,
alertId: (body["alertId"] as ObjectID) || undefined,
scheduledMaintenanceId:
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
statusPageAnnouncementId:
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
userId: (body["userId"] as ObjectID) || undefined,
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
onCallPolicyEscalationRuleId:
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
onCallDutyPolicyExecutionLogTimelineId:
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
undefined,
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
teamId: (body["teamId"] as ObjectID) || undefined,
});
return Response.sendEmptySuccessResponse(req, res);
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
router.post(
"/test",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
const callSMSConfigId: ObjectID = new ObjectID(
body["callSMSConfigId"] as string,
);
const callSMSConfigId: ObjectID = new ObjectID(
body["callSMSConfigId"] as string,
);
const config: ProjectCallSMSConfig | null =
await ProjectCallSMSConfigService.findOneById({
id: callSMSConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
twilioAccountSID: true,
twilioAuthToken: true,
twilioPrimaryPhoneNumber: true,
twilioSecondaryPhoneNumbers: true,
projectId: true,
},
});
const config: ProjectCallSMSConfig | null =
await ProjectCallSMSConfigService.findOneById({
id: callSMSConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
twilioAccountSID: true,
twilioAuthToken: true,
twilioPrimaryPhoneNumber: true,
twilioSecondaryPhoneNumbers: true,
projectId: true,
},
});
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"call and sms config not found for id" + callSMSConfigId.toString(),
),
);
}
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"call and sms config not found for id" + callSMSConfigId.toString(),
),
);
}
const toPhone: Phone = new Phone(body["toPhone"] as string);
const toPhone: Phone = new Phone(body["toPhone"] as string);
if (!toPhone) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toPhone is required"),
);
}
if (!toPhone) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toPhone is required"),
);
}
// if any of the twilio config is missing, we will not send make the call
// if any of the twilio config is missing, we will not send make the call
if (!config.twilioAccountSID) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAccountSID is required"),
);
}
if (!config.twilioAccountSID) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAccountSID is required"),
);
}
if (!config.twilioAuthToken) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAuthToken is required"),
);
}
if (!config.twilioAuthToken) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAuthToken is required"),
);
}
if (!config.twilioPrimaryPhoneNumber) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioPrimaryPhoneNumber is required"),
);
}
if (!config.twilioPrimaryPhoneNumber) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioPrimaryPhoneNumber is required"),
);
}
const twilioConfig: TwilioConfig | undefined =
ProjectCallSMSConfigService.toTwilioConfig(config);
const twilioConfig: TwilioConfig | undefined =
ProjectCallSMSConfigService.toTwilioConfig(config);
try {
if (!twilioConfig) {
throw new BadDataException("twilioConfig is undefined");
try {
if (!twilioConfig) {
throw new BadDataException("twilioConfig is undefined");
}
const testCallRequest: CallRequest = {
data: [
{
sayMessage: "This is a test call from OneUptime.",
},
],
to: toPhone,
};
await CallService.makeCall(testCallRequest, {
projectId: config.projectId,
customTwilioConfig: twilioConfig,
});
} catch (err) {
logger.error(err);
throw new BadDataException(
"Error making test call. Please check the twilio logs for more details",
);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
const testCallRequest: CallRequest = {
data: [
{
sayMessage: "This is a test call from OneUptime.",
},
],
to: toPhone,
};
await CallService.makeCall(testCallRequest, {
projectId: config.projectId,
customTwilioConfig: twilioConfig,
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"Error making test call. Please check the twilio logs for more details",
),
);
}
return Response.sendEmptySuccessResponse(req, res);
});
},
);
export default router;

View File

@@ -11,6 +11,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
@@ -19,70 +20,74 @@ const router: ExpressRouter = Express.getRouter();
router.post(
"/send",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
const mail: EmailMessage = {
templateType: body["templateType"] as EmailTemplateType,
toEmail: new Email(body["toEmail"] as string),
subject: body["subject"] as string,
vars: body["vars"] as Dictionary<string>,
body: (body["body"] as string) || "",
};
const mail: EmailMessage = {
templateType: body["templateType"] as EmailTemplateType,
toEmail: new Email(body["toEmail"] as string),
subject: body["subject"] as string,
vars: body["vars"] as Dictionary<string>,
body: (body["body"] as string) || "",
};
let mailServer: EmailServer | undefined = undefined;
let mailServer: EmailServer | undefined = undefined;
if (hasMailServerSettingsInBody(body)) {
mailServer = MailService.getEmailServer(req.body);
if (hasMailServerSettingsInBody(body)) {
mailServer = MailService.getEmailServer(req.body);
}
await MailService.send(mail, {
projectId: body["projectId"]
? new ObjectID(body["projectId"] as string)
: undefined,
emailServer: mailServer,
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
incidentId: body["incidentId"]
? new ObjectID(body["incidentId"].toString())
: undefined,
alertId: body["alertId"]
? new ObjectID(body["alertId"].toString())
: undefined,
scheduledMaintenanceId: body["scheduledMaintenanceId"]
? new ObjectID(body["scheduledMaintenanceId"].toString())
: undefined,
statusPageId: body["statusPageId"]
? new ObjectID(body["statusPageId"].toString())
: undefined,
statusPageAnnouncementId: body["statusPageAnnouncementId"]
? new ObjectID(body["statusPageAnnouncementId"].toString())
: undefined,
userId: body["userId"]
? new ObjectID(body["userId"].toString())
: undefined,
onCallPolicyId: body["onCallPolicyId"]
? new ObjectID(body["onCallPolicyId"].toString())
: undefined,
onCallPolicyEscalationRuleId: body["onCallPolicyEscalationRuleId"]
? new ObjectID(body["onCallPolicyEscalationRuleId"].toString())
: undefined,
onCallDutyPolicyExecutionLogTimelineId: body[
"onCallDutyPolicyExecutionLogTimelineId"
]
? new ObjectID(
body["onCallDutyPolicyExecutionLogTimelineId"].toString(),
)
: undefined,
onCallScheduleId: body["onCallScheduleId"]
? new ObjectID(body["onCallScheduleId"].toString())
: undefined,
teamId: body["teamId"]
? new ObjectID(body["teamId"].toString())
: undefined,
});
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
await MailService.send(mail, {
projectId: body["projectId"]
? new ObjectID(body["projectId"] as string)
: undefined,
emailServer: mailServer,
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
incidentId: body["incidentId"]
? new ObjectID(body["incidentId"].toString())
: undefined,
alertId: body["alertId"]
? new ObjectID(body["alertId"].toString())
: undefined,
scheduledMaintenanceId: body["scheduledMaintenanceId"]
? new ObjectID(body["scheduledMaintenanceId"].toString())
: undefined,
statusPageId: body["statusPageId"]
? new ObjectID(body["statusPageId"].toString())
: undefined,
statusPageAnnouncementId: body["statusPageAnnouncementId"]
? new ObjectID(body["statusPageAnnouncementId"].toString())
: undefined,
userId: body["userId"]
? new ObjectID(body["userId"].toString())
: undefined,
onCallPolicyId: body["onCallPolicyId"]
? new ObjectID(body["onCallPolicyId"].toString())
: undefined,
onCallPolicyEscalationRuleId: body["onCallPolicyEscalationRuleId"]
? new ObjectID(body["onCallPolicyEscalationRuleId"].toString())
: undefined,
onCallDutyPolicyExecutionLogTimelineId: body[
"onCallDutyPolicyExecutionLogTimelineId"
]
? new ObjectID(
body["onCallDutyPolicyExecutionLogTimelineId"].toString(),
)
: undefined,
onCallScheduleId: body["onCallScheduleId"]
? new ObjectID(body["onCallScheduleId"].toString())
: undefined,
teamId: body["teamId"]
? new ObjectID(body["teamId"].toString())
: undefined,
});
return Response.sendEmptySuccessResponse(req, res);
},
);

View File

@@ -5,6 +5,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
import { JSONObject } from "Common/Types/JSON";
@@ -15,50 +16,54 @@ const router: ExpressRouter = Express.getRouter();
router.post(
"/send",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = JSONFunctions.deserialize(req.body);
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = JSONFunctions.deserialize(req.body);
// Support both new devices format and legacy deviceTokens/deviceNames format
let devices: Array<{ token: string; name?: string }> = [];
// Support both new devices format and legacy deviceTokens/deviceNames format
let devices: Array<{ token: string; name?: string }> = [];
if (body["devices"]) {
// New format: devices as array of objects
devices = body["devices"] as Array<{ token: string; name?: string }>;
} else {
throw new Error("Invalid request format: 'devices' array is required.");
if (body["devices"]) {
// New format: devices as array of objects
devices = body["devices"] as Array<{ token: string; name?: string }>;
} else {
throw new Error("Invalid request format: 'devices' array is required.");
}
await PushService.send(
{
devices,
deviceType: (body["deviceType"] as any) || "web",
message: body["message"] as any,
},
{
projectId: (body["projectId"] as ObjectID) || undefined,
isSensitive: (body["isSensitive"] as boolean) || false,
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
incidentId: (body["incidentId"] as ObjectID) || undefined,
alertId: (body["alertId"] as ObjectID) || undefined,
scheduledMaintenanceId:
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
statusPageAnnouncementId:
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
userId: (body["userId"] as ObjectID) || undefined,
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
onCallPolicyEscalationRuleId:
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
onCallDutyPolicyExecutionLogTimelineId:
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
undefined,
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
teamId: (body["teamId"] as ObjectID) || undefined,
},
);
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
await PushService.send(
{
devices,
deviceType: (body["deviceType"] as any) || "web",
message: body["message"] as any,
},
{
projectId: (body["projectId"] as ObjectID) || undefined,
isSensitive: (body["isSensitive"] as boolean) || false,
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
incidentId: (body["incidentId"] as ObjectID) || undefined,
alertId: (body["alertId"] as ObjectID) || undefined,
scheduledMaintenanceId:
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
statusPageAnnouncementId:
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
userId: (body["userId"] as ObjectID) || undefined,
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
onCallPolicyEscalationRuleId:
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
onCallDutyPolicyExecutionLogTimelineId:
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
undefined,
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
teamId: (body["teamId"] as ObjectID) || undefined,
},
);
return Response.sendEmptySuccessResponse(req, res);
},
);

View File

@@ -11,6 +11,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
@@ -21,130 +22,141 @@ const router: ExpressRouter = Express.getRouter();
router.post(
"/send",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = JSONFunctions.deserialize(req.body);
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = JSONFunctions.deserialize(req.body);
await SmsService.sendSms(body["to"] as Phone, body["message"] as string, {
projectId: body["projectId"] as ObjectID,
isSensitive: (body["isSensitive"] as boolean) || false,
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
customTwilioConfig: body["customTwilioConfig"] as any,
incidentId: (body["incidentId"] as ObjectID) || undefined,
alertId: (body["alertId"] as ObjectID) || undefined,
scheduledMaintenanceId:
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
statusPageAnnouncementId:
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
userId: (body["userId"] as ObjectID) || undefined,
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
onCallPolicyEscalationRuleId:
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
onCallDutyPolicyExecutionLogTimelineId:
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
undefined,
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
teamId: (body["teamId"] as ObjectID) || undefined,
});
await SmsService.sendSms(body["to"] as Phone, body["message"] as string, {
projectId: body["projectId"] as ObjectID,
isSensitive: (body["isSensitive"] as boolean) || false,
userOnCallLogTimelineId:
(body["userOnCallLogTimelineId"] as ObjectID) || undefined,
customTwilioConfig: body["customTwilioConfig"] as any,
incidentId: (body["incidentId"] as ObjectID) || undefined,
alertId: (body["alertId"] as ObjectID) || undefined,
scheduledMaintenanceId:
(body["scheduledMaintenanceId"] as ObjectID) || undefined,
statusPageId: (body["statusPageId"] as ObjectID) || undefined,
statusPageAnnouncementId:
(body["statusPageAnnouncementId"] as ObjectID) || undefined,
userId: (body["userId"] as ObjectID) || undefined,
onCallPolicyId: (body["onCallPolicyId"] as ObjectID) || undefined,
onCallPolicyEscalationRuleId:
(body["onCallPolicyEscalationRuleId"] as ObjectID) || undefined,
onCallDutyPolicyExecutionLogTimelineId:
(body["onCallDutyPolicyExecutionLogTimelineId"] as ObjectID) ||
undefined,
onCallScheduleId: (body["onCallScheduleId"] as ObjectID) || undefined,
teamId: (body["teamId"] as ObjectID) || undefined,
});
return Response.sendEmptySuccessResponse(req, res);
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
router.post(
"/test",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
const callSMSConfigId: ObjectID = new ObjectID(
body["callSMSConfigId"] as string,
);
const callSMSConfigId: ObjectID = new ObjectID(
body["callSMSConfigId"] as string,
);
const config: ProjectCallSMSConfig | null =
await ProjectCallSMSConfigService.findOneById({
id: callSMSConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
twilioAccountSID: true,
twilioAuthToken: true,
twilioPrimaryPhoneNumber: true,
twilioSecondaryPhoneNumbers: true,
projectId: true,
},
});
const config: ProjectCallSMSConfig | null =
await ProjectCallSMSConfigService.findOneById({
id: callSMSConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
twilioAccountSID: true,
twilioAuthToken: true,
twilioPrimaryPhoneNumber: true,
twilioSecondaryPhoneNumbers: true,
projectId: true,
},
});
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"call and sms config not found for id" + callSMSConfigId.toString(),
),
);
}
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"call and sms config not found for id" + callSMSConfigId.toString(),
),
);
}
const toPhone: Phone = new Phone(body["toPhone"] as string);
const toPhone: Phone = new Phone(body["toPhone"] as string);
if (!toPhone) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toPhone is required"),
);
}
if (!toPhone) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toPhone is required"),
);
}
// if any of the twilio config is missing, we will not send make the call
// if any of the twilio config is missing, we will not send make the call
if (!config.twilioAccountSID) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAccountSID is required"),
);
}
if (!config.twilioAccountSID) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAccountSID is required"),
);
}
if (!config.twilioAuthToken) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAuthToken is required"),
);
}
if (!config.twilioAuthToken) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioAuthToken is required"),
);
}
if (!config.twilioPrimaryPhoneNumber) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioPrimaryPhoneNumber is required"),
);
}
if (!config.twilioPrimaryPhoneNumber) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("twilioPrimaryPhoneNumber is required"),
);
}
const twilioConfig: TwilioConfig | undefined =
ProjectCallSMSConfigService.toTwilioConfig(config);
const twilioConfig: TwilioConfig | undefined =
ProjectCallSMSConfigService.toTwilioConfig(config);
try {
if (!twilioConfig) {
throw new BadDataException("twilioConfig is undefined");
try {
if (!twilioConfig) {
throw new BadDataException("twilioConfig is undefined");
}
await SmsService.sendSms(
toPhone,
"This is a test SMS from OneUptime.",
{
projectId: config.projectId,
customTwilioConfig: twilioConfig,
},
);
} catch (err) {
logger.error(err);
throw new BadDataException(
"Failed to send test SMS. Please check the twilio logs for more details.",
);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
await SmsService.sendSms(toPhone, "This is a test SMS from OneUptime.", {
projectId: config.projectId,
customTwilioConfig: twilioConfig,
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"Failed to send test SMS. Please check the twilio logs for more details.",
),
);
}
return Response.sendEmptySuccessResponse(req, res);
});
},
);
export default router;

View File

@@ -11,6 +11,7 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
@@ -18,87 +19,92 @@ import ProjectSmtpConfig from "Common/Models/DatabaseModels/ProjectSmtpConfig";
const router: ExpressRouter = Express.getRouter();
router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => {
const body: JSONObject = req.body;
router.post(
"/test",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
const smtpConfigId: ObjectID = new ObjectID(body["smtpConfigId"] as string);
const smtpConfigId: ObjectID = new ObjectID(
body["smtpConfigId"] as string,
);
const config: ProjectSmtpConfig | null =
await ProjectSMTPConfigService.findOneById({
id: smtpConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
hostname: true,
port: true,
username: true,
password: true,
fromEmail: true,
fromName: true,
secure: true,
projectId: true,
},
});
const config: ProjectSmtpConfig | null =
await ProjectSMTPConfigService.findOneById({
id: smtpConfigId,
props: {
isRoot: true,
},
select: {
_id: true,
hostname: true,
port: true,
username: true,
password: true,
fromEmail: true,
fromName: true,
secure: true,
projectId: true,
},
});
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"smtp-config not found for id" + smtpConfigId.toString(),
),
);
}
if (!config) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"smtp-config not found for id" + smtpConfigId.toString(),
),
);
}
const toEmail: Email = new Email(body["toEmail"] as string);
const toEmail: Email = new Email(body["toEmail"] as string);
if (!toEmail) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toEmail is required"),
);
}
if (!toEmail) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("toEmail is required"),
);
}
const mail: EmailMessage = {
templateType: EmailTemplateType.SMTPTest,
toEmail: new Email(body["toEmail"] as string),
subject: "Test Email from OneUptime",
vars: {},
body: "",
};
const mail: EmailMessage = {
templateType: EmailTemplateType.SMTPTest,
toEmail: new Email(body["toEmail"] as string),
subject: "Test Email from OneUptime",
vars: {},
body: "",
};
const mailServer: EmailServer = {
id: config.id!,
host: config.hostname!,
port: config.port!,
username: config.username!,
password: config.password!,
fromEmail: config.fromEmail!,
fromName: config.fromName!,
secure: Boolean(config.secure),
};
const mailServer: EmailServer = {
id: config.id!,
host: config.hostname!,
port: config.port!,
username: config.username!,
password: config.password!,
fromEmail: config.fromEmail!,
fromName: config.fromName!,
secure: Boolean(config.secure),
};
try {
await MailService.send(mail, {
emailServer: mailServer,
projectId: config.projectId!,
timeout: 4000,
});
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"Cannot send email. Please check your SMTP config. If you are using Google or Gmail, please dont since it does not support machine access to their mail servers. If you are still having issues, please uncheck SSL/TLS toggle and try again. We recommend using SendGrid or Mailgun or any large volume mail provider for SMTP.",
),
);
}
try {
await MailService.send(mail, {
emailServer: mailServer,
projectId: config.projectId!,
timeout: 4000,
});
} catch (err) {
logger.error(err);
throw new BadDataException(
"Cannot send email. Please check your SMTP config. If you are using Google or Gmail, please dont since it does not support machine access to their mail servers. If you are still having issues, please uncheck SSL/TLS toggle and try again. We recommend using SendGrid or Mailgun or any large volume mail provider for SMTP.",
);
}
return Response.sendEmptySuccessResponse(req, res);
});
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
export default router;

View File

@@ -0,0 +1,171 @@
import WhatsAppService from "../Services/WhatsAppService";
import BadDataException from "Common/Types/Exception/BadDataException";
import { JSONObject } from "Common/Types/JSON";
import ObjectID from "Common/Types/ObjectID";
import Phone from "Common/Types/Phone";
import WhatsAppMessage from "Common/Types/WhatsApp/WhatsAppMessage";
import {
WhatsAppTemplateId,
WhatsAppTemplateIds,
WhatsAppTemplateLanguage,
} from "Common/Types/WhatsApp/WhatsAppTemplates";
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
const router: ExpressRouter = Express.getRouter();
const toTemplateVariables: (
rawVariables: JSONObject | undefined,
) => Record<string, string> | undefined = (
rawVariables: JSONObject | undefined,
): Record<string, string> | undefined => {
if (!rawVariables) {
return undefined;
}
const result: Record<string, string> = {};
for (const key of Object.keys(rawVariables)) {
const value: unknown = rawVariables[key];
if (value !== null && value !== undefined) {
result[key] = String(value);
}
}
return Object.keys(result).length > 0 ? result : undefined;
};
router.post(
"/send",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
const body: JSONObject = req.body as JSONObject;
if (!body["to"]) {
throw new BadDataException("`to` phone number is required");
}
const toPhone: Phone = new Phone(body["to"] as string);
const message: WhatsAppMessage = {
to: toPhone,
body: (body["body"] as string) || "",
templateKey: (body["templateKey"] as string) || undefined,
templateVariables: toTemplateVariables(
body["templateVariables"] as JSONObject | undefined,
),
templateLanguageCode:
(body["templateLanguageCode"] as string) || undefined,
};
try {
await WhatsAppService.sendWhatsApp(message, {
projectId: body["projectId"]
? new ObjectID(body["projectId"] as string)
: undefined,
isSensitive: (body["isSensitive"] as boolean) || false,
userOnCallLogTimelineId: body["userOnCallLogTimelineId"]
? new ObjectID(body["userOnCallLogTimelineId"] as string)
: undefined,
incidentId: body["incidentId"]
? new ObjectID(body["incidentId"] as string)
: undefined,
alertId: body["alertId"]
? new ObjectID(body["alertId"] as string)
: undefined,
scheduledMaintenanceId: body["scheduledMaintenanceId"]
? new ObjectID(body["scheduledMaintenanceId"] as string)
: undefined,
statusPageId: body["statusPageId"]
? new ObjectID(body["statusPageId"] as string)
: undefined,
statusPageAnnouncementId: body["statusPageAnnouncementId"]
? new ObjectID(body["statusPageAnnouncementId"] as string)
: undefined,
userId: body["userId"]
? new ObjectID(body["userId"] as string)
: undefined,
onCallPolicyId: body["onCallPolicyId"]
? new ObjectID(body["onCallPolicyId"] as string)
: undefined,
onCallPolicyEscalationRuleId: body["onCallPolicyEscalationRuleId"]
? new ObjectID(body["onCallPolicyEscalationRuleId"] as string)
: undefined,
onCallDutyPolicyExecutionLogTimelineId: body[
"onCallDutyPolicyExecutionLogTimelineId"
]
? new ObjectID(
body["onCallDutyPolicyExecutionLogTimelineId"] as string,
)
: undefined,
onCallScheduleId: body["onCallScheduleId"]
? new ObjectID(body["onCallScheduleId"] as string)
: undefined,
teamId: body["teamId"]
? new ObjectID(body["teamId"] as string)
: undefined,
});
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
router.post(
"/test",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body as JSONObject;
if (!body["toPhone"]) {
throw new BadDataException("toPhone is required");
}
const toPhone: Phone = new Phone(body["toPhone"] as string);
const templateKey: WhatsAppTemplateId =
WhatsAppTemplateIds.TestNotification;
const templateLanguageCode: string =
WhatsAppTemplateLanguage[templateKey] || "en";
const message: WhatsAppMessage = {
to: toPhone,
body: "",
templateKey,
templateVariables: undefined,
templateLanguageCode,
};
try {
await WhatsAppService.sendWhatsApp(message, {
projectId: body["projectId"]
? new ObjectID(body["projectId"] as string)
: undefined,
isSensitive: false,
});
} catch (err) {
const errorMsg: string =
err instanceof Error && err.message
? err.message
: "Failed to send test WhatsApp message.";
throw new BadDataException(errorMsg);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
export default router;

View File

@@ -12,6 +12,8 @@ import Phone from "Common/Types/Phone";
type GetGlobalSMTPConfig = () => Promise<EmailServer | null>;
export const DEFAULT_META_WHATSAPP_API_VERSION: string = "v23.0";
export const getGlobalSMTPConfig: GetGlobalSMTPConfig =
async (): Promise<EmailServer | null> => {
const globalConfig: GlobalConfig | null =
@@ -222,6 +224,83 @@ export const SMSHighRiskCostInCents: number = process.env[
? parseInt(process.env["SMS_HIGH_RISK_COST_IN_CENTS"])
: 0;
export interface MetaWhatsAppConfig {
accessToken: string;
phoneNumberId: string;
businessAccountId?: string | undefined;
appId?: string | undefined;
appSecret?: string | undefined;
apiVersion?: string | undefined;
}
type GetMetaWhatsAppConfigFunction = () => Promise<MetaWhatsAppConfig>;
export const getMetaWhatsAppConfig: GetMetaWhatsAppConfigFunction =
async (): Promise<MetaWhatsAppConfig> => {
const globalConfig: GlobalConfig | null =
await GlobalConfigService.findOneBy({
query: {
_id: ObjectID.getZeroObjectID().toString(),
},
props: {
isRoot: true,
},
select: {
metaWhatsAppAccessToken: true,
metaWhatsAppPhoneNumberId: true,
metaWhatsAppBusinessAccountId: true,
metaWhatsAppAppId: true,
metaWhatsAppAppSecret: true,
},
});
if (!globalConfig) {
throw new BadDataException("Global Config not found");
}
const accessToken: string | undefined =
globalConfig.metaWhatsAppAccessToken?.trim();
const phoneNumberId: string | undefined =
globalConfig.metaWhatsAppPhoneNumberId?.trim();
if (!accessToken) {
throw new BadDataException(
"Meta WhatsApp access token not configured. Please set it in the Admin Dashboard: " +
AdminDashboardClientURL.toString(),
);
}
if (!phoneNumberId) {
throw new BadDataException(
"Meta WhatsApp phone number ID not configured. Please set it in the Admin Dashboard: " +
AdminDashboardClientURL.toString(),
);
}
const businessAccountId: string | undefined =
globalConfig.metaWhatsAppBusinessAccountId?.trim() || undefined;
const appId: string | undefined =
globalConfig.metaWhatsAppAppId?.trim() || undefined;
const appSecret: string | undefined =
globalConfig.metaWhatsAppAppSecret?.trim() || undefined;
const apiVersion: string = DEFAULT_META_WHATSAPP_API_VERSION;
return {
accessToken,
phoneNumberId,
businessAccountId,
appId,
appSecret,
apiVersion,
};
};
export const WhatsAppTextDefaultCostInCents: number = process.env[
"WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS"
]
? parseInt(process.env["WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS"])
: 0;
export const CallHighRiskCostInCentsPerMinute: number = process.env[
"CALL_HIGH_RISK_COST_IN_CENTS_PER_MINUTE"
]

View File

@@ -2,6 +2,7 @@ import CallAPI from "./API/Call";
// API
import MailAPI from "./API/Mail";
import SmsAPI from "./API/SMS";
import WhatsAppAPI from "./API/WhatsApp";
import PushNotificationAPI from "./API/PushNotification";
import SMTPConfigAPI from "./API/SMTPConfig";
import "./Utils/Handlebars";
@@ -16,6 +17,7 @@ const NotificationFeatureSet: FeatureSet = {
app.use([`/${APP_NAME}/email`, "/email"], MailAPI);
app.use([`/${APP_NAME}/sms`, "/sms"], SmsAPI);
app.use([`/${APP_NAME}/whatsapp`, "/whatsapp"], WhatsAppAPI);
app.use([`/${APP_NAME}/push`, "/push"], PushNotificationAPI);
app.use([`/${APP_NAME}/call`, "/call"], CallAPI);
app.use([`/${APP_NAME}/smtp-config`, "/smtp-config"], SMTPConfigAPI);

View File

@@ -47,9 +47,11 @@ function extractSayMessagesFromCallRequest(callRequest: CallRequest): string {
if ((item as GatherInput).introMessage) {
sayMessages.push((item as GatherInput).introMessage);
}
// NOTE: Excluding noInputMessage and onInputCallRequest messages from summary
// as they contain system responses like "Good bye", "Invalid input", "You have acknowledged"
// which should not be included in the call summary according to user requirements
/*
* NOTE: Excluding noInputMessage and onInputCallRequest messages from summary
* as they contain system responses like "Good bye", "Invalid input", "You have acknowledged"
* which should not be included in the call summary according to user requirements
*/
}
}

View File

@@ -37,11 +37,49 @@ class TransporterPool {
private static semaphore: Map<string, number> = new Map();
private static readonly MAX_CONCURRENT_CONNECTIONS = 100;
private static resolveConnectionSettings(emailServer: EmailServer): {
portNumber: number;
wantsSecureConnection: boolean;
secureConnection: boolean;
requireTLS: boolean;
mode: "implicit-tls" | "starttls" | "plain";
} {
const portNumber: number = emailServer.port.toNumber();
const wantsSecureConnection: boolean = emailServer.secure;
const isImplicitTLSPort: boolean = portNumber === 465;
const secureConnection: boolean = isImplicitTLSPort;
const requireTLS: boolean = wantsSecureConnection && !isImplicitTLSPort;
let mode: "implicit-tls" | "starttls" | "plain" = "plain";
if (secureConnection) {
mode = "implicit-tls";
} else if (requireTLS) {
mode = "starttls";
}
return {
portNumber,
wantsSecureConnection,
secureConnection,
requireTLS,
mode,
};
}
private static getPoolKey(emailServer: EmailServer): string {
const { portNumber, mode } = this.resolveConnectionSettings(emailServer);
const username: string = emailServer.username || "noauth";
return `${emailServer.host.toString()}:${portNumber}:${username}:${mode}`;
}
public static getTransporter(
emailServer: EmailServer,
options: { timeout?: number | undefined },
): Transporter {
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
const key: string = this.getPoolKey(emailServer);
if (!this.pools.has(key)) {
const transporter: Transporter = this.createTransporter(
@@ -59,9 +97,12 @@ class TransporterPool {
emailServer: EmailServer,
options: { timeout?: number | undefined },
): Transporter {
const { portNumber, wantsSecureConnection, secureConnection, requireTLS } =
this.resolveConnectionSettings(emailServer);
let tlsOptions: tls.ConnectionOptions | undefined = undefined;
if (!emailServer.secure) {
if (!wantsSecureConnection) {
tlsOptions = {
rejectUnauthorized: false,
};
@@ -69,8 +110,9 @@ class TransporterPool {
return nodemailer.createTransport({
host: emailServer.host.toString(),
port: emailServer.port.toNumber(),
secure: emailServer.secure,
port: portNumber,
secure: secureConnection,
requireTLS,
tls: tlsOptions,
auth:
emailServer.username && emailServer.password
@@ -88,7 +130,7 @@ class TransporterPool {
public static async acquireConnection(
emailServer: EmailServer,
): Promise<void> {
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
const key: string = this.getPoolKey(emailServer);
while ((this.semaphore.get(key) || 0) >= this.MAX_CONCURRENT_CONNECTIONS) {
await new Promise<void>((resolve: () => void) => {
@@ -100,7 +142,7 @@ class TransporterPool {
}
public static releaseConnection(emailServer: EmailServer): void {
const key: string = `${emailServer.host.toString()}:${emailServer.port.toNumber()}:${emailServer.username || "noauth"}`;
const key: string = this.getPoolKey(emailServer);
const current: number = this.semaphore.get(key) || 0;
this.semaphore.set(key, Math.max(0, current - 1));
}

View File

@@ -0,0 +1,489 @@
import {
WhatsAppTextDefaultCostInCents,
getMetaWhatsAppConfig,
MetaWhatsAppConfig,
DEFAULT_META_WHATSAPP_API_VERSION,
} from "../Config";
import BadDataException from "Common/Types/Exception/BadDataException";
import ObjectID from "Common/Types/ObjectID";
import UserNotificationStatus from "Common/Types/UserNotification/UserNotificationStatus";
import WhatsAppMessage from "Common/Types/WhatsApp/WhatsAppMessage";
import WhatsAppStatus from "Common/Types/WhatsAppStatus";
import {
AuthenticationTemplates,
WhatsAppTemplateId,
} from "Common/Types/WhatsApp/WhatsAppTemplates";
import { JSONArray, JSONObject } from "Common/Types/JSON";
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import NotificationService from "Common/Server/Services/NotificationService";
import ProjectService from "Common/Server/Services/ProjectService";
import UserOnCallLogTimelineService from "Common/Server/Services/UserOnCallLogTimelineService";
import WhatsAppLogService from "Common/Server/Services/WhatsAppLogService";
import logger from "Common/Server/Utils/Logger";
import Project from "Common/Models/DatabaseModels/Project";
import WhatsAppLog from "Common/Models/DatabaseModels/WhatsAppLog";
import API from "Common/Utils/API";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import Protocol from "Common/Types/API/Protocol";
import Route from "Common/Types/API/Route";
import URL from "Common/Types/API/URL";
const SENSITIVE_MESSAGE_PLACEHOLDER: string =
"This message is sensitive and is not logged";
export default class WhatsAppService {
public static async sendWhatsApp(
message: WhatsAppMessage,
options: {
projectId?: ObjectID | undefined;
isSensitive?: boolean | undefined;
userOnCallLogTimelineId?: ObjectID | undefined;
incidentId?: ObjectID | undefined;
alertId?: ObjectID | undefined;
scheduledMaintenanceId?: ObjectID | undefined;
statusPageId?: ObjectID | undefined;
statusPageAnnouncementId?: ObjectID | undefined;
userId?: ObjectID | undefined;
onCallPolicyId?: ObjectID | undefined;
onCallPolicyEscalationRuleId?: ObjectID | undefined;
onCallDutyPolicyExecutionLogTimelineId?: ObjectID | undefined;
onCallScheduleId?: ObjectID | undefined;
teamId?: ObjectID | undefined;
} = {},
): Promise<void> {
let sendError: Error | null = null;
const whatsAppLog: WhatsAppLog = new WhatsAppLog();
try {
if (!message.to) {
throw new BadDataException(
"WhatsApp recipient phone number is required",
);
}
if (!message.body && !message.templateKey) {
throw new BadDataException(
"Either WhatsApp message body or template key must be provided",
);
}
const config: MetaWhatsAppConfig = await getMetaWhatsAppConfig();
const isSensitiveMessage: boolean = Boolean(options.isSensitive);
const messageSummary: string = isSensitiveMessage
? SENSITIVE_MESSAGE_PLACEHOLDER
: message.body ||
(message.templateKey
? `Template: ${message.templateKey}${
message.templateVariables
? " Variables: " + JSON.stringify(message.templateVariables)
: ""
}`
: "");
whatsAppLog.toNumber = message.to;
whatsAppLog.messageText = messageSummary;
whatsAppLog.whatsAppCostInUSDCents = 0;
if (options.projectId) {
whatsAppLog.projectId = options.projectId;
}
if (options.incidentId) {
whatsAppLog.incidentId = options.incidentId;
}
if (options.alertId) {
whatsAppLog.alertId = options.alertId;
}
if (options.scheduledMaintenanceId) {
whatsAppLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
}
if (options.statusPageId) {
whatsAppLog.statusPageId = options.statusPageId;
}
if (options.statusPageAnnouncementId) {
whatsAppLog.statusPageAnnouncementId = options.statusPageAnnouncementId;
}
if (options.userId) {
whatsAppLog.userId = options.userId;
}
if (options.teamId) {
whatsAppLog.teamId = options.teamId;
}
if (options.onCallPolicyId) {
whatsAppLog.onCallDutyPolicyId = options.onCallPolicyId;
}
if (options.onCallPolicyEscalationRuleId) {
whatsAppLog.onCallDutyPolicyEscalationRuleId =
options.onCallPolicyEscalationRuleId;
}
if (options.onCallScheduleId) {
whatsAppLog.onCallDutyPolicyScheduleId = options.onCallScheduleId;
}
let messageCost: number = 0;
const shouldChargeForMessage: boolean = IsBillingEnabled;
if (shouldChargeForMessage) {
messageCost = WhatsAppTextDefaultCostInCents / 100;
}
let project: Project | null = null;
if (options.projectId) {
project = await ProjectService.findOneById({
id: options.projectId,
select: {
smsOrCallCurrentBalanceInUSDCents: true,
lowCallAndSMSBalanceNotificationSentToOwners: true,
name: true,
notEnabledSmsOrCallNotificationSentToOwners: true,
},
props: {
isRoot: true,
},
});
if (!project) {
whatsAppLog.status = WhatsAppStatus.Error;
whatsAppLog.statusMessage = `Project ${options.projectId.toString()} not found.`;
logger.error(whatsAppLog.statusMessage);
await WhatsAppLogService.create({
data: whatsAppLog,
props: {
isRoot: true,
},
});
return;
}
if (shouldChargeForMessage) {
let updatedBalance: number =
project.smsOrCallCurrentBalanceInUSDCents || 0;
try {
updatedBalance = await NotificationService.rechargeIfBalanceIsLow(
project.id!,
);
} catch (err) {
logger.error(err);
}
project.smsOrCallCurrentBalanceInUSDCents = updatedBalance;
if (!project.smsOrCallCurrentBalanceInUSDCents) {
whatsAppLog.status = WhatsAppStatus.LowBalance;
whatsAppLog.statusMessage = `Project ${options.projectId.toString()} does not have enough balance for WhatsApp messages.`;
logger.error(whatsAppLog.statusMessage);
await WhatsAppLogService.create({
data: whatsAppLog,
props: {
isRoot: true,
},
});
if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
await ProjectService.updateOneById({
id: project.id!,
data: {
lowCallAndSMSBalanceNotificationSentToOwners: true,
},
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
`Low WhatsApp message balance for ${project.name || ""}`,
`We tried to send a WhatsApp message to ${message.to.toString()} with message:<br/><br/>${messageSummary}<br/><br/>The message was not sent because your project does not have enough balance for WhatsApp messages. Current balance is ${
(project.smsOrCallCurrentBalanceInUSDCents || 0) / 100
} USD. Required balance for this message is ${messageCost} USD. Please enable auto recharge or recharge manually.`,
);
}
return;
}
if (project.smsOrCallCurrentBalanceInUSDCents < messageCost * 100) {
whatsAppLog.status = WhatsAppStatus.LowBalance;
whatsAppLog.statusMessage = `Project does not have enough balance to send WhatsApp message. Current balance is ${
project.smsOrCallCurrentBalanceInUSDCents / 100
} USD. Required balance is ${messageCost} USD.`;
logger.error(whatsAppLog.statusMessage);
await WhatsAppLogService.create({
data: whatsAppLog,
props: {
isRoot: true,
},
});
if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
await ProjectService.updateOneById({
id: project.id!,
data: {
lowCallAndSMSBalanceNotificationSentToOwners: true,
},
props: {
isRoot: true,
},
});
await ProjectService.sendEmailToProjectOwners(
project.id!,
`Low WhatsApp message balance for ${project.name || ""}`,
`We tried to send a WhatsApp message to ${message.to.toString()} with message:<br/><br/>${messageSummary}<br/><br/>The message was not sent because your project does not have enough balance for WhatsApp messages. Current balance is ${
project.smsOrCallCurrentBalanceInUSDCents / 100
} USD. Required balance is ${messageCost} USD. Please enable auto recharge or recharge manually.`,
);
}
return;
}
}
}
const payload: JSONObject = {
messaging_product: "whatsapp",
recipient_type: "individual",
to: message.to.toString(),
} as JSONObject;
if (!message.templateKey) {
throw new BadDataException("WhatsApp message template key is required");
}
if (message.templateKey) {
const template: JSONObject = {
name: message.templateKey,
language: {
code: message.templateLanguageCode || "en",
},
} as JSONObject;
const components: JSONArray = [];
if (
message.templateVariables &&
Object.keys(message.templateVariables).length > 0
) {
const parameters: JSONArray = [];
for (const [key, value] of Object.entries(
message.templateVariables,
)) {
parameters.push({
type: "text",
parameter_name: key,
text: value,
} as JSONObject);
}
if (parameters.length > 0) {
components.push({
type: "body",
parameters,
} as JSONObject);
}
}
/*
* Check if this is an authentication template
* Authentication templates may have special requirements including button components
*/
const isAuthTemplate: boolean = AuthenticationTemplates.has(
message.templateKey as WhatsAppTemplateId,
);
if (isAuthTemplate) {
logger.info(
`Sending authentication template: ${message.templateKey}`,
);
/*
* Authentication templates in WhatsApp may have a button component for "Copy Code"
* If the template was created with a button, we need to provide button parameters
*/
if (message.templateVariables) {
const otpCode: string | undefined =
message.templateVariables["1"] ||
message.templateVariables["otp"] ||
message.templateVariables["code"];
if (otpCode) {
/*
* Add button component - the index should match the button position in the template
* Usually authentication templates have the button as the first (and only) button
*/
components.push({
type: "button",
sub_type: "url",
index: 0,
parameters: [
{
type: "text",
text: otpCode,
},
],
} as JSONObject);
}
}
}
if (components.length > 0) {
template["components"] = components;
}
payload["type"] = "template";
payload["template"] = template;
} else {
payload["type"] = "text";
payload["text"] = {
body: message.body || "",
} as JSONObject;
}
const apiVersion: string =
config.apiVersion?.trim() || DEFAULT_META_WHATSAPP_API_VERSION;
const url: URL = new URL(
Protocol.HTTPS,
"graph.facebook.com",
new Route(`${apiVersion}/${config.phoneNumberId}/messages`),
);
logger.debug(`WhatsApp API request: ${JSON.stringify(payload)}`);
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.post<JSONObject>({
url,
data: payload,
headers: {
Authorization: `Bearer ${config.accessToken}`,
"Content-Type": "application/json",
},
});
if (response instanceof HTTPErrorResponse) {
logger.error("Failed to send WhatsApp message.");
logger.error(response);
const responseDataAsJSONObject: JSONObject = response.data;
const responseJsonAsJSONObject: JSONObject | undefined =
(response.jsonData as JSONObject | undefined) || undefined;
// Log full error details for debugging
const errorObject: JSONObject | undefined =
(responseDataAsJSONObject["error"] as JSONObject | undefined) ||
(responseJsonAsJSONObject?.["error"] as JSONObject | undefined);
if (errorObject) {
logger.error("WhatsApp API Error Details:");
logger.error(JSON.stringify(errorObject, null, 2));
}
const detailedErrorMessage: string | undefined =
((responseDataAsJSONObject["error"] as JSONObject | undefined)?.[
"message"
] as string | undefined) ||
((responseJsonAsJSONObject?.["error"] as JSONObject | undefined)?.[
"message"
] as string | undefined);
throw new BadDataException(
detailedErrorMessage || "Failed to send WhatsApp message.",
);
}
const responseData: JSONObject = (response.jsonData || {}) as JSONObject;
let messageId: string | undefined = undefined;
const messagesArray: JSONArray | undefined =
(responseData["messages"] as JSONArray) || undefined;
if (Array.isArray(messagesArray) && messagesArray.length > 0) {
const firstMessage: JSONObject = messagesArray[0] as JSONObject;
if (firstMessage["id"]) {
messageId = firstMessage["id"] as string;
}
}
whatsAppLog.status = WhatsAppStatus.Success;
whatsAppLog.statusMessage = messageId
? `Message ID: ${messageId}`
: "WhatsApp message sent successfully";
if (shouldChargeForMessage && project) {
const deduction: number = Math.floor(messageCost * 100);
whatsAppLog.whatsAppCostInUSDCents = deduction;
project.smsOrCallCurrentBalanceInUSDCents = Math.max(
0,
Math.floor(
(project.smsOrCallCurrentBalanceInUSDCents || 0) - deduction,
),
);
await ProjectService.updateOneById({
id: project.id!,
data: {
smsOrCallCurrentBalanceInUSDCents:
project.smsOrCallCurrentBalanceInUSDCents,
notEnabledSmsOrCallNotificationSentToOwners: false,
},
props: {
isRoot: true,
},
});
}
} catch (error: any) {
logger.error("Failed to send WhatsApp message.");
logger.error(error);
whatsAppLog.whatsAppCostInUSDCents = 0;
whatsAppLog.status = WhatsAppStatus.Error;
const errorMessage: string =
error && error.message ? error.message.toString() : `${error}`;
whatsAppLog.statusMessage = errorMessage;
sendError = error;
}
if (options.projectId) {
await WhatsAppLogService.create({
data: whatsAppLog,
props: {
isRoot: true,
},
});
}
if (options.userOnCallLogTimelineId) {
await UserOnCallLogTimelineService.updateOneById({
id: options.userOnCallLogTimelineId,
data: {
status:
whatsAppLog.status === WhatsAppStatus.Success
? UserNotificationStatus.Sent
: UserNotificationStatus.Error,
statusMessage: whatsAppLog.statusMessage,
},
props: {
isRoot: true,
},
});
}
if (sendError) {
throw sendError;
}
}
}

View File

@@ -42,10 +42,10 @@ loadPartials().catch((err: Error) => {
Handlebars.registerHelper("ifCond", function (v1, v2, options) {
if (v1 === v2) {
//@ts-ignore
//@ts-expect-error - Handlebars uses dynamic this context for template helpers
return options.fn(this);
}
//@ts-ignore
//@ts-expect-error - Handlebars uses dynamic this context for template helpers
return options.inverse(this);
});
@@ -56,9 +56,9 @@ Handlebars.registerHelper("concat", (v1: any, v2: any) => {
Handlebars.registerHelper("ifNotCond", function (v1, v2, options) {
if (v1 !== v2) {
//@ts-ignore
//@ts-expect-error - Handlebars uses dynamic this context for template helpers
return options.fn(this);
}
//@ts-ignore
//@ts-expect-error - Handlebars uses dynamic this context for template helpers
return options.inverse(this);
});

View File

@@ -1,8 +1,20 @@
{
"watch": ["./","../Common/Server", "../Common/Types", "../Common/Utils", "../Common/Models"],
"ext": "ts,json,tsx,env,js,jsx,hbs",
"ext": "ts,tsx",
"ignore": [
"./node_modules/**",
"./public/**",
"./bin/**",
"./build/**",
"greenlock.d/*"
],
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
"watchOptions": {
"useFsEvents": false,
"interval": 500
},
"env": {
"TS_NODE_TRANSPILE_ONLY": "1",
"TS_NODE_FILES": "false"
},
"exec": "node -r ts-node/register/transpile-only Index.ts"
}

1
App/package-lock.json generated
View File

@@ -76,6 +76,7 @@
"crypto-js": "^4.2.0",
"dotenv": "^16.4.4",
"ejs": "^3.1.10",
"elkjs": "^0.10.0",
"esbuild": "^0.25.5",
"express": "^4.21.1",
"formik": "^2.4.6",

View File

@@ -262,6 +262,96 @@ export default class GlobalConfig extends GlobalConfigModel {
})
public twilioSecondaryPhoneNumbers?: string = undefined; // phone numbers separated by comma
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
title: "Meta WhatsApp Access Token",
description:
"Access token generated from Meta for sending WhatsApp messages.",
})
@Column({
type: ColumnType.VeryLongText,
nullable: true,
unique: true,
})
public metaWhatsAppAccessToken?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Meta WhatsApp Phone Number ID",
description: "The WhatsApp Business phone number ID from Meta.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: true,
})
public metaWhatsAppPhoneNumberId?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Meta WhatsApp Business Account ID",
description: "Business account ID associated with your WhatsApp setup.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: true,
})
public metaWhatsAppBusinessAccountId?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Meta WhatsApp App ID",
description:
"Facebook App ID used for the WhatsApp Business Platform integration.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: true,
})
public metaWhatsAppAppId?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
title: "Meta WhatsApp App Secret",
description: "Facebook App Secret for the WhatsApp Business Platform.",
})
@Column({
type: ColumnType.VeryLongText,
nullable: true,
unique: true,
})
public metaWhatsAppAppSecret?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],

View File

@@ -716,6 +716,77 @@ export default class IncidentTemplate extends BaseModel {
})
public changeMonitorStatusToId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentTemplate,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "initialIncidentStateId",
type: TableColumnType.Entity,
modelType: IncidentState,
title: "Initial Incident State",
description:
"Relation to Incident State Object. Incidents created from this template will start in this state.",
})
@ManyToOne(
() => {
return IncidentState;
},
{
eager: false,
nullable: true,
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "initialIncidentStateId" })
public initialIncidentState?: IncidentState = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditIncidentTemplate,
],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
title: "Initial Incident State ID",
description:
"Relation to Incident State Object ID. Incidents created from this template will start in this state.",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public initialIncidentStateId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -100,6 +100,7 @@ import ServiceCopilotCodeRepository from "./ServiceCopilotCodeRepository";
import ShortLink from "./ShortLink";
// SMS
import SmsLog from "./SmsLog";
import WhatsAppLog from "./WhatsAppLog";
import PushNotificationLog from "./PushNotificationLog";
import WorkspaceNotificationLog from "./WorkspaceNotificationLog";
// Status Page
@@ -123,6 +124,7 @@ import StatusPageSubscriber from "./StatusPageSubscriber";
import Team from "./Team";
import TeamMember from "./TeamMember";
import TeamPermission from "./TeamPermission";
import TeamComplianceSetting from "./TeamComplianceSetting";
import TelemetryService from "./TelemetryService";
import UsageBilling from "./TelemetryUsageBilling";
import User from "./User";
@@ -130,6 +132,7 @@ import UserCall from "./UserCall";
// Notification Methods
import UserEmail from "./UserEmail";
import UserPush from "./UserPush";
import UserWhatsApp from "./UserWhatsApp";
// User Notification Rules
import UserNotificationRule from "./UserNotificationRule";
import UserNotificationSetting from "./UserNotificationSetting";
@@ -145,7 +148,8 @@ import ServiceCatalogDependency from "./ServiceCatalogDependency";
import ServiceCatalogMonitor from "./ServiceCatalogMonitor";
import ServiceCatalogTelemetryService from "./ServiceCatalogTelemetryService";
import UserTwoFactorAuth from "./UserTwoFactorAuth";
import UserTotpAuth from "./UserTotpAuth";
import UserWebAuthn from "./UserWebAuthn";
import TelemetryIngestionKey from "./TelemetryIngestionKey";
@@ -196,6 +200,7 @@ const AllModelTypes: Array<{
Team,
TeamMember,
TeamPermission,
TeamComplianceSetting,
ApiKey,
Label,
ApiKeyPermission,
@@ -294,6 +299,7 @@ const AllModelTypes: Array<{
StatusPageOwnerUser,
SmsLog,
WhatsAppLog,
PushNotificationLog,
WorkspaceNotificationLog,
CallLog,
@@ -303,6 +309,7 @@ const AllModelTypes: Array<{
UserSms,
UserCall,
UserPush,
UserWhatsApp,
UserNotificationRule,
UserOnCallLog,
@@ -364,7 +371,8 @@ const AllModelTypes: Array<{
ProbeOwnerTeam,
ProbeOwnerUser,
UserTwoFactorAuth,
UserTotpAuth,
UserWebAuthn,
TelemetryIngestionKey,

View File

@@ -29,10 +29,17 @@ import {
ManyToOne,
} from "typeorm";
import NotificationRuleWorkspaceChannel from "../../Types/Workspace/NotificationRules/NotificationRuleWorkspaceChannel";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
@EnableDocumentation()
@AccessControlColumn("labels")
@TenantColumn("projectId")
@EnableWorkflow({
create: true,
delete: true,
update: true,
read: true,
})
@TableAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -46,7 +46,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
Permission.EditProjectOnCallDutyPolicyEscalationRule,
],
})
@CrudApiEndpoint(new Route("/on-call-duty-policy-esclation-rule"))
@CrudApiEndpoint(new Route("/on-call-duty-policy-escalation-rule"))
@Entity({
name: "OnCallDutyPolicyEscalationRule",
})

View File

@@ -47,7 +47,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
Permission.EditProjectOnCallDutyPolicyEscalationRuleSchedule,
],
})
@CrudApiEndpoint(new Route("/on-call-duty-policy-esclation-rule-schedule"))
@CrudApiEndpoint(new Route("/on-call-duty-policy-escalation-rule-schedule"))
@Entity({
name: "OnCallDutyPolicyEscalationRuleSchedule",
})

View File

@@ -47,7 +47,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
Permission.EditProjectOnCallDutyPolicyEscalationRuleTeam,
],
})
@CrudApiEndpoint(new Route("/on-call-duty-policy-esclation-rule-team"))
@CrudApiEndpoint(new Route("/on-call-duty-policy-escalation-rule-team"))
@Entity({
name: "OnCallDutyPolicyEscalationRuleTeam",
})

View File

@@ -46,7 +46,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
Permission.EditProjectOnCallDutyPolicyEscalationRuleUser,
],
})
@CrudApiEndpoint(new Route("/on-call-duty-policy-esclation-rule-user"))
@CrudApiEndpoint(new Route("/on-call-duty-policy-escalation-rule-user"))
@Entity({
name: "OnCallDutyPolicyEscalationRuleUser",
})

View File

@@ -25,6 +25,7 @@ import Permission from "../../Types/Permission";
import UserNotificationEventType from "../../Types/UserNotification/UserNotificationEventType";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import Alert from "./Alert";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
@TableBillingAccessControl({
create: PlanType.Growth,
@@ -32,6 +33,12 @@ import Alert from "./Alert";
update: PlanType.Growth,
delete: PlanType.Growth,
})
@EnableWorkflow({
create: true,
delete: true,
update: true,
read: true,
})
@EnableDocumentation()
@TenantColumn("projectId")
@TableAccessControl({

View File

@@ -29,6 +29,7 @@ import {
ManyToMany,
ManyToOne,
} from "typeorm";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
@EnableDocumentation()
@TableBillingAccessControl({
@@ -37,6 +38,12 @@ import {
update: PlanType.Growth,
delete: PlanType.Growth,
})
@EnableWorkflow({
create: true,
delete: true,
update: true,
read: true,
})
@AccessControlColumn("labels")
@TenantColumn("projectId")
@TableAccessControl({

View File

@@ -17,6 +17,7 @@ import Permission from "../../Types/Permission";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
@EnableDocumentation()
@TableBillingAccessControl({
@@ -26,6 +27,12 @@ import { PlanType } from "../../Types/Billing/SubscriptionPlan";
delete: PlanType.Growth,
})
@TenantColumn("projectId")
@EnableWorkflow({
create: true,
delete: true,
update: true,
read: true,
})
@TableAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -132,9 +132,11 @@ export default class OnCallDutyPolicyUserOverride extends BaseModel {
})
public projectId?: ObjectID = undefined;
// If this is null then it's a global override
// If this is set then it's a policy specific override
// Policy specifc override will take precedence over global override
/*
* If this is null then it's a global override
* If this is set then it's a policy specific override
* Policy specifc override will take precedence over global override
*/
@ColumnAccessControl({
create: [

View File

@@ -248,6 +248,84 @@ export default class Project extends TenantModel {
})
public paymentProviderCustomerId?: string = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProject,
Permission.UnAuthorizedSsoUser,
Permission.ProjectUser,
],
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
})
@TableColumn({
type: TableColumnType.LongText,
title: "Business Details / Billing Address",
description:
"Business legal name, address and any tax information to appear on invoices.",
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
nullable: true,
unique: false,
})
public businessDetails?: string = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProject,
Permission.UnAuthorizedSsoUser,
Permission.ProjectUser,
],
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Business Country (ISO Alpha-2)",
description:
"Two-letter ISO country code for billing address (e.g., US, GB, DE).",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: false,
})
public businessDetailsCountry?: string = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProject,
Permission.UnAuthorizedSsoUser,
Permission.ProjectUser,
],
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
})
@TableColumn({
type: TableColumnType.Email,
title: "Finance / Accounting Email",
description:
"Invoices, receipts and billing related notifications will be sent to this email in addition to project owner.",
})
@Column({
type: ColumnType.Email,
length: ColumnLength.Email,
nullable: true,
unique: false,
})
public financeAccountingEmail?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
@@ -720,6 +798,33 @@ export default class Project extends TenantModel {
})
public enableSmsNotifications?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProject,
Permission.UnAuthorizedSsoUser,
Permission.ProjectUser,
],
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
})
@TableColumn({
required: true,
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
title: "Enable WhatsApp Notifications",
description: "Enable WhatsApp notifications for this project.",
defaultValue: false,
})
@Column({
nullable: false,
default: false,
type: ColumnType.Boolean,
})
public enableWhatsAppNotifications?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [
@@ -1213,4 +1318,63 @@ export default class Project extends TenantModel {
type: ColumnType.Boolean,
})
public letCustomerSupportAccessProject?: boolean = undefined;
/*
* This is an internal field. This is used for internal analytics for example: Metabase.
* Values can be between 0 and 100.
*/
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
required: true,
type: TableColumnType.Number,
isDefaultValueColumn: true,
hideColumnInDocumentation: true,
title: "Discount Percent",
description: "Discount percentage applied to the project billing",
defaultValue: 0,
})
@Column({
type: ColumnType.Number,
nullable: false,
unique: false,
default: 0,
})
public discountPercent?: number = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProject,
Permission.UnAuthorizedSsoUser,
Permission.ProjectUser,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProject,
],
})
@TableColumn({
required: false,
type: TableColumnType.Boolean,
isDefaultValueColumn: false,
title: "Do NOT auto-add Global Probes to new monitors",
description:
"If enabled, global probes will NOT be automatically added to new monitors. Enable this only if you are using ONLY custom probes to monitor your resources.",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
unique: false,
default: false,
})
public doNotAddGlobalProbesByDefaultOnNewMonitors?: boolean = undefined;
}

View File

@@ -334,6 +334,37 @@ export default class ProjectSCIM extends BaseModel {
})
public autoDeprovisionUsers?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.CreateProjectSSO,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectSSO,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectSSO,
],
})
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
title: "Enable Push Groups",
description: "Enable push groups provisioning instead of default teams",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
default: false,
})
public enablePushGroups?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [

View File

@@ -336,8 +336,10 @@ export default class ProjectSmtpConfig extends BaseModel {
})
public deletedByUserId?: ObjectID = undefined;
// This is not required because some SMTP servers do not require authentication.
// eg: https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365#option-2-send-mail-directly-from-your-printer-or-application-to-microsoft-365-or-office-365-direct-send
/*
* This is not required because some SMTP servers do not require authentication.
* eg: https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365#option-2-send-mail-directly-from-your-printer-or-application-to-microsoft-365-or-office-365-direct-send
*/
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -1,3 +1,4 @@
import Monitor from "./Monitor";
import Project from "./Project";
import StatusPage from "./StatusPage";
import User from "./User";
@@ -198,6 +199,53 @@ export default class StatusPageAnnouncement extends BaseModel {
})
public statusPages?: Array<StatusPage> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageAnnouncement,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageAnnouncement,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageAnnouncement,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: Monitor,
title: "Monitors",
description:
"List of monitors affected by this announcement. If none are selected, all subscribers will be notified.",
})
@ManyToMany(
() => {
return Monitor;
},
{ eager: false },
)
@JoinTable({
name: "AnnouncementMonitor",
inverseJoinColumn: {
name: "monitorId",
referencedColumnName: "_id",
},
joinColumn: {
name: "announcementId",
referencedColumnName: "_id",
},
})
public monitors?: Array<Monitor> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -1,3 +1,4 @@
import Monitor from "./Monitor";
import Project from "./Project";
import StatusPage from "./StatusPage";
import User from "./User";
@@ -328,6 +329,53 @@ export default class StatusPageAnnouncementTemplate extends BaseModel {
})
public statusPages?: Array<StatusPage> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageAnnouncementTemplate,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageAnnouncementTemplate,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageAnnouncementTemplate,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: Monitor,
title: "Monitors",
description:
"List of monitors affected by this announcement template. If none are selected, all subscribers will be notified.",
})
@ManyToMany(
() => {
return Monitor;
},
{ eager: false },
)
@JoinTable({
name: "AnnouncementTemplateMonitor",
inverseJoinColumn: {
name: "monitorId",
referencedColumnName: "_id",
},
joinColumn: {
name: "announcementTemplateId",
referencedColumnName: "_id",
},
})
public monitors?: Array<Monitor> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -420,10 +420,12 @@ export default class StatusPageDomain extends BaseModel {
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
// This token is used by the Worker.
// worker pings the status page of customers - eg: status.company.com/verify-token/:id
// and the end point on Status Page project returns 200.
// when that happens the isCnameVerified is set to True and the certificate is added to Greenlock.
/*
* This token is used by the Worker.
* worker pings the status page of customers - eg: status.company.com/verify-token/:id
* and the end point on Status Page project returns 200.
* when that happens the isCnameVerified is set to True and the certificate is added to Greenlock.
*/
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -0,0 +1,403 @@
import Project from "./Project";
import Team from "./Team";
import User from "./User";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
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 { JSONObject } from "../../Types/JSON";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import ComplianceRuleType from "../../Types/Team/ComplianceRuleType";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@TableBillingAccessControl({
create: PlanType.Scale,
read: PlanType.Free,
update: PlanType.Scale,
delete: PlanType.Free,
})
@EnableDocumentation()
@TableAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
],
delete: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
})
@TenantColumn("projectId")
@CrudApiEndpoint(new Route("/team-compliance-setting"))
@Index(["teamId", "ruleType"], { unique: true })
@Entity({
name: "TeamComplianceSetting",
})
@EnableWorkflow({
create: false,
delete: false,
update: false,
read: false,
})
@TableMetadata({
tableName: "TeamComplianceSetting",
singularName: "Team Compliance Setting",
pluralName: "Team Compliance Settings",
icon: IconProp.CheckCircle,
tableDescription: "Compliance settings for your OneUptime team",
})
export default class TeamComplianceSetting extends BaseModel {
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
],
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.EditProjectTeam,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
],
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.EditProjectTeam,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "teamId",
type: TableColumnType.Entity,
modelType: Team,
title: "Team",
description: "Team this compliance setting belongs to.",
})
@ManyToOne(
() => {
return Team;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "teamId" })
public team?: Team = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
title: "Team ID",
description: "ID of Team this compliance setting belongs to.",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public teamId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
],
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.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
],
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",
modelType: 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;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
})
@TableColumn({
required: true,
type: TableColumnType.LongText,
title: "Rule Type",
description: "Type of compliance rule.",
})
@Column({
nullable: false,
type: ColumnType.LongText,
length: ColumnLength.LongText,
})
public ruleType?: ComplianceRuleType = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
})
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
title: "Enabled",
description: "Whether this compliance rule is enabled.",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
default: false,
})
public enabled?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditProjectTeam,
],
})
@TableColumn({
required: false,
type: TableColumnType.JSON,
title: "Options",
description: "Additional options for this compliance rule.",
})
@Column({
type: ColumnType.JSON,
nullable: true,
})
public options?: JSONObject = undefined;
}

View File

@@ -1,7 +1,6 @@
import File from "./File";
import UserModel from "../../Models/DatabaseModels/DatabaseBaseModel/UserModel";
import Route from "../../Types/API/Route";
import URL from "../../Types/API/URL";
import CompanySize from "../../Types/Company/CompanySize";
import JobRole from "../../Types/Company/JobRole";
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
@@ -301,51 +300,6 @@ class User extends UserModel {
})
public twoFactorAuthEnabled?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({ type: TableColumnType.ShortText })
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: false,
})
public twoFactorSecretCode?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({ type: TableColumnType.ShortURL })
@Column({
type: ColumnType.ShortURL,
length: ColumnLength.ShortURL,
nullable: true,
unique: false,
transformer: URL.getDatabaseTransformer(),
})
public twoFactorAuthUrl?: URL = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({ type: TableColumnType.Array })
@Column({
type: ColumnType.Array,
nullable: true,
unique: false,
})
public backupCodes?: Array<string> = undefined;
@ColumnAccessControl({
create: [],
read: [],

View File

@@ -6,6 +6,7 @@ import UserCall from "./UserCall";
import UserEmail from "./UserEmail";
import UserPush from "./UserPush";
import UserSMS from "./UserSMS";
import UserWhatsApp from "./UserWhatsApp";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
@@ -383,6 +384,53 @@ class UserNotificationRule extends BaseModel {
})
public userSmsId?: ObjectID = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "userWhatsAppId",
type: TableColumnType.Entity,
modelType: UserWhatsApp,
title: "User WhatsApp",
description:
"Relation to User WhatsApp Resource in which this object belongs",
})
@ManyToOne(
() => {
return UserWhatsApp;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "userWhatsAppId" })
public userWhatsApp?: UserWhatsApp = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "User WhatsApp ID",
description: "ID of User WhatsApp in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public userWhatsAppId?: ObjectID = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],

View File

@@ -273,6 +273,22 @@ class UserNotificationSetting extends BaseModel {
})
public alertBySMS?: boolean = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
default: false,
})
public alertByWhatsApp?: boolean = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],

View File

@@ -13,6 +13,7 @@ import UserNotificationRule from "./UserNotificationRule";
import UserPush from "./UserPush";
import UserOnCallLog from "./UserOnCallLog";
import UserSMS from "./UserSMS";
import UserWhatsApp from "./UserWhatsApp";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
@@ -834,6 +835,53 @@ export default class UserOnCallLogTimeline extends BaseModel {
})
public userSmsId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "userWhatsAppId",
type: TableColumnType.Entity,
modelType: UserWhatsApp,
title: "User WhatsApp",
description:
"Relation to User WhatsApp Resource in which this object belongs",
})
@ManyToOne(
() => {
return UserWhatsApp;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "userWhatsAppId" })
public userWhatsApp?: UserWhatsApp = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "User WhatsApp ID",
description: "ID of User WhatsApp in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public userWhatsAppId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],

View File

@@ -27,19 +27,19 @@ import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
delete: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@CrudApiEndpoint(new Route("/user-two-factor-auth"))
@CrudApiEndpoint(new Route("/user-totp-auth"))
@Entity({
name: "UserTwoFactorAuth",
name: "UserTotpAuth",
})
@TableMetadata({
tableName: "UserTwoFactorAuth",
singularName: "Two Factor Auth",
pluralName: "Two Factor Auth",
tableName: "UserTotpAuth",
singularName: "TOTP Auth",
pluralName: "TOTP Auth",
icon: IconProp.ShieldCheck,
tableDescription: "Two Factor Authentication for users",
tableDescription: "TOTP Authentication for users",
})
@CurrentUserCanAccessRecordBy("userId")
class UserTwoFactorAuth extends BaseModel {
class UserTotpAuth extends BaseModel {
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
@@ -48,8 +48,8 @@ class UserTwoFactorAuth extends BaseModel {
@TableColumn({
type: TableColumnType.ShortText,
canReadOnRelationQuery: true,
title: "Two Factor Auth Name",
description: "Name of the two factor authentication",
title: "TOTP Auth Name",
description: "Name of the TOTP authentication",
})
@Column({
type: ColumnType.ShortText,
@@ -67,8 +67,8 @@ class UserTwoFactorAuth extends BaseModel {
@TableColumn({
type: TableColumnType.VeryLongText,
canReadOnRelationQuery: false,
title: "Two Factor Auth Secret",
description: "Secret of the two factor authentication",
title: "TOTP Auth Secret",
description: "Secret of the TOTP authentication",
})
@Column({
type: ColumnType.VeryLongText,
@@ -85,8 +85,8 @@ class UserTwoFactorAuth extends BaseModel {
@TableColumn({
type: TableColumnType.VeryLongText,
canReadOnRelationQuery: false,
title: "Two Factor Auth OTP URL",
description: "OTP URL of the two factor authentication",
title: "TOTP Auth OTP URL",
description: "OTP URL of the TOTP authentication",
})
@Column({
type: ColumnType.VeryLongText,
@@ -106,7 +106,7 @@ class UserTwoFactorAuth extends BaseModel {
title: "Is Verified",
isDefaultValueColumn: true,
description:
"Is this two factor authentication verified and validated (has user entered the tokent to verify it)",
"Is this TOTP authentication verified and validated (has user entered the token to verify it)",
defaultValue: false,
})
@Column({
@@ -171,7 +171,7 @@ class UserTwoFactorAuth extends BaseModel {
manyToOneRelationColumn: "userId",
type: TableColumnType.Entity,
title: "User",
description: "Relation to User who owns this two factor authentication",
description: "Relation to User who owns this TOTP authentication",
})
@ManyToOne(
() => {
@@ -207,4 +207,4 @@ class UserTwoFactorAuth extends BaseModel {
public userId?: ObjectID = undefined;
}
export default UserTwoFactorAuth;
export default UserTotpAuth;

View File

@@ -0,0 +1,244 @@
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import User from "./User";
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 EnableDocumentation from "../../Types/Database/EnableDocumentation";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
@EnableDocumentation({
isMasterAdminApiDocs: true,
})
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
delete: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@CrudApiEndpoint(new Route("/user-webauthn"))
@Entity({
name: "UserWebAuthn",
})
@TableMetadata({
tableName: "UserWebAuthn",
singularName: "WebAuthn Credential",
pluralName: "WebAuthn Credentials",
icon: IconProp.ShieldCheck,
tableDescription: "WebAuthn credentials for users (security keys)",
})
@CurrentUserCanAccessRecordBy("userId")
class UserWebAuthn extends BaseModel {
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
type: TableColumnType.ShortText,
canReadOnRelationQuery: true,
title: "Credential Name",
description: "Name of the WebAuthn credential",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: false,
unique: false,
})
public name?: string = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
canReadOnRelationQuery: false,
title: "Credential ID",
description: "Unique identifier for the WebAuthn credential",
})
@Column({
type: ColumnType.VeryLongText,
nullable: false,
unique: true,
})
public credentialId?: string = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
canReadOnRelationQuery: false,
title: "Public Key",
description: "Public key of the WebAuthn credential",
})
@Column({
type: ColumnType.VeryLongText,
nullable: false,
unique: false,
})
public publicKey?: string = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
canReadOnRelationQuery: false,
title: "Counter",
description: "Counter for the WebAuthn credential",
})
@Column({
type: ColumnType.VeryLongText,
nullable: false,
unique: false,
})
public counter?: string = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.VeryLongText,
canReadOnRelationQuery: false,
title: "Transports",
description: "Transports supported by the WebAuthn credential",
})
@Column({
type: ColumnType.VeryLongText,
nullable: true,
unique: false,
})
public transports?: string = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.Boolean,
canReadOnRelationQuery: true,
title: "Is Verified",
isDefaultValueColumn: true,
description: "Is this WebAuthn credential verified and validated",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
default: false,
})
public isVerified?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: 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;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
manyToOneRelationColumn: "userId",
type: TableColumnType.Entity,
title: "User",
description: "Relation to User who owns this WebAuthn credential",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "userId" })
public user?: User = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "User ID",
description: "User ID who owns this WebAuthn credential",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public userId?: ObjectID = undefined;
}
export default UserWebAuthn;

View File

@@ -0,0 +1,288 @@
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 Phone from "../../Types/Phone";
import Text from "../../Types/Text";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@TenantColumn("projectId")
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
delete: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@CrudApiEndpoint(new Route("/user-whatsapp"))
@Entity({
name: "UserWhatsApp",
})
@TableMetadata({
tableName: "UserWhatsApp",
singularName: "WhatsApp Number",
pluralName: "WhatsApp Numbers",
icon: IconProp.WhatsApp,
tableDescription: "WhatsApp numbers used for WhatsApp notifications.",
})
@CurrentUserCanAccessRecordBy("userId")
class UserWhatsApp 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: [Permission.CurrentUser],
update: [],
})
@TableColumn({
title: "WhatsApp Number",
required: true,
unique: false,
type: TableColumnType.Phone,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.Phone,
length: ColumnLength.Phone,
unique: false,
nullable: false,
transformer: Phone.getDatabaseTransformer(),
})
public phone?: Phone = 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 WhatsApp number 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 WhatsApp number 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",
modelType: 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;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
title: "Is Verified",
description: "Is this WhatsApp number verified?",
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
default: false,
})
public isVerified?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
title: "Verification Code",
description: "Temporary Verification Code",
isDefaultValueColumn: true,
computed: true,
required: true,
type: TableColumnType.ShortText,
forceGetDefaultValueOnCreate: () => {
return Text.generateRandomNumber(6);
},
})
@Column({
type: ColumnType.ShortText,
nullable: false,
length: ColumnLength.ShortText,
})
public verificationCode?: string = undefined;
}
export default UserWhatsApp;

View File

@@ -0,0 +1,884 @@
import Project from "./Project";
import Incident from "./Incident";
import Alert from "./Alert";
import ScheduledMaintenance from "./ScheduledMaintenance";
import StatusPage from "./StatusPage";
import StatusPageAnnouncement from "./StatusPageAnnouncement";
import User from "./User";
import OnCallDutyPolicy from "./OnCallDutyPolicy";
import OnCallDutyPolicyEscalationRule from "./OnCallDutyPolicyEscalationRule";
import OnCallDutyPolicySchedule from "./OnCallDutyPolicySchedule";
import Team from "./Team";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
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 EnableDocumentation from "../../Types/Database/EnableDocumentation";
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
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 Phone from "../../Types/Phone";
import WhatsAppStatus from "../../Types/WhatsAppStatus";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@EnableDocumentation()
@TenantColumn("projectId")
@TableAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
delete: [],
update: [],
})
@CrudApiEndpoint(new Route("/whatsapp-log"))
@Entity({
name: "WhatsAppLog",
})
@EnableWorkflow({
create: true,
delete: false,
update: false,
})
@TableMetadata({
tableName: "WhatsAppLog",
singularName: "WhatsApp Log",
pluralName: "WhatsApp Logs",
icon: IconProp.WhatsApp,
tableDescription:
"Logs of all the WhatsApp messages sent out to all users and subscribers for this project.",
})
export default class WhatsAppLog extends BaseModel {
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
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: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
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: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
required: true,
type: TableColumnType.Phone,
title: "To Number",
description: "Phone Number WhatsApp message was sent to",
canReadOnRelationQuery: false,
})
@Column({
nullable: false,
type: ColumnType.Phone,
length: ColumnLength.Phone,
transformer: Phone.getDatabaseTransformer(),
})
public toNumber?: Phone = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
required: false,
type: TableColumnType.Phone,
title: "From Number",
description:
"Phone Number WhatsApp message was sent from (Business Number ID)",
canReadOnRelationQuery: false,
})
@Column({
nullable: true,
type: ColumnType.Phone,
length: ColumnLength.Phone,
transformer: Phone.getDatabaseTransformer(),
})
public fromNumber?: Phone = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.VeryLongText,
title: "Message Text",
description: "Text content of the WhatsApp message",
canReadOnRelationQuery: false,
})
@Column({
nullable: true,
type: ColumnType.VeryLongText,
})
public messageText?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.LongText,
title: "Status Message",
description: "Status Message (if any)",
canReadOnRelationQuery: false,
})
@Column({
nullable: true,
type: ColumnType.LongText,
length: ColumnLength.LongText,
})
public statusMessage?: string = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
required: true,
type: TableColumnType.ShortText,
title: "Status of the WhatsApp Message",
description: "Status of the WhatsApp message sent",
canReadOnRelationQuery: false,
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public status?: WhatsAppStatus = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
required: true,
type: TableColumnType.Number,
title: "WhatsApp Cost",
description: "WhatsApp Message Cost in USD Cents",
canReadOnRelationQuery: false,
isDefaultValueColumn: true,
defaultValue: 0,
})
@Column({
nullable: false,
default: 0,
type: ColumnType.Number,
})
public whatsAppCostInUSDCents?: number = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "incidentId",
type: TableColumnType.Entity,
modelType: Incident,
title: "Incident",
description: "Incident associated with this message (if any)",
})
@ManyToOne(
() => {
return Incident;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "incidentId" })
public incident?: Incident = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Incident ID",
description: "ID of Incident associated with this message (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public incidentId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "userId",
type: TableColumnType.Entity,
modelType: User,
title: "User",
description: "User who initiated this message (if any)",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "userId" })
public user?: User = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "User ID",
description: "ID of User who initiated this message (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public userId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "alertId",
type: TableColumnType.Entity,
modelType: Alert,
title: "Alert",
description: "Alert associated with this message (if any)",
})
@ManyToOne(
() => {
return Alert;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "alertId" })
public alert?: Alert = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Alert ID",
description: "ID of Alert associated with this message (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public alertId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "scheduledMaintenanceId",
type: TableColumnType.Entity,
modelType: ScheduledMaintenance,
title: "Scheduled Maintenance",
description: "Scheduled Maintenance associated with this message (if any)",
})
@ManyToOne(
() => {
return ScheduledMaintenance;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "scheduledMaintenanceId" })
public scheduledMaintenance?: ScheduledMaintenance = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Scheduled Maintenance ID",
description:
"ID of Scheduled Maintenance associated with this message (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public scheduledMaintenanceId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPageId",
type: TableColumnType.Entity,
modelType: StatusPage,
title: "Status Page",
description: "Status Page associated with this message (if any)",
})
@ManyToOne(
() => {
return StatusPage;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "statusPageId" })
public statusPage?: StatusPage = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Status Page ID",
description: "ID of Status Page associated with this message (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPageId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPageAnnouncementId",
type: TableColumnType.Entity,
modelType: StatusPageAnnouncement,
title: "Status Page Announcement",
description:
"Status Page Announcement associated with this message (if any)",
})
@ManyToOne(
() => {
return StatusPageAnnouncement;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "statusPageAnnouncementId" })
public statusPageAnnouncement?: StatusPageAnnouncement = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Status Page Announcement ID",
description:
"ID of Status Page Announcement associated with this message (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPageAnnouncementId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "teamId",
type: TableColumnType.Entity,
modelType: Team,
title: "Team",
description: "Team associated with this message (if any)",
})
@ManyToOne(
() => {
return Team;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "teamId" })
public team?: Team = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Team ID",
description: "ID of Team associated with this message (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public teamId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "onCallDutyPolicyId",
type: TableColumnType.Entity,
modelType: OnCallDutyPolicy,
title: "On-Call Duty Policy",
description: "On-Call Duty Policy associated with this message (if any)",
})
@ManyToOne(
() => {
return OnCallDutyPolicy;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "onCallDutyPolicyId" })
public onCallDutyPolicy?: OnCallDutyPolicy = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "On-Call Duty Policy ID",
description:
"ID of On-Call Duty Policy associated with this message (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public onCallDutyPolicyId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "onCallDutyPolicyEscalationRuleId",
type: TableColumnType.Entity,
modelType: OnCallDutyPolicyEscalationRule,
title: "On-Call Duty Policy Escalation Rule",
description:
"On-Call Duty Policy Escalation Rule associated with this message (if any)",
})
@ManyToOne(
() => {
return OnCallDutyPolicyEscalationRule;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "onCallDutyPolicyEscalationRuleId" })
public onCallDutyPolicyEscalationRule?: OnCallDutyPolicyEscalationRule =
undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "On-Call Duty Policy Escalation Rule ID",
description:
"ID of On-Call Duty Policy Escalation Rule associated with this message (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public onCallDutyPolicyEscalationRuleId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "onCallDutyPolicyScheduleId",
type: TableColumnType.Entity,
modelType: OnCallDutyPolicySchedule,
title: "On-Call Duty Policy Schedule",
description:
"On-Call Duty Policy Schedule associated with this message (if any)",
})
@ManyToOne(
() => {
return OnCallDutyPolicySchedule;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "onCallDutyPolicyScheduleId" })
public onCallDutyPolicySchedule?: OnCallDutyPolicySchedule = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadSmsLog,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "On-Call Duty Policy Schedule ID",
description:
"ID of On-Call Duty Policy Schedule associated with this message (if any)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public onCallDutyPolicyScheduleId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: 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;
}

View File

@@ -18,15 +18,43 @@ import WorkspaceType from "../../Types/Workspace/WorkspaceType";
import Permission from "../../Types/Permission";
export interface MiscData {
[key: string]: string;
[key: string]: any;
}
export interface MicrosoftTeamsTeam {
id: string;
name: string;
}
export interface SlackMiscData extends MiscData {
teamId: string;
teamName: string;
botUserId: string;
channelCache?: {
[channelName: string]: {
id: string;
name: string;
lastUpdated: string;
};
};
}
export interface MicrosoftTeamsMiscData extends MiscData {
tenantId: string;
teamId: string;
teamName: string;
botId: string;
appAccessToken?: string;
adminConsentGranted?: boolean;
lastAppTokenIssuedAt?: string;
adminConsentGrantedAt?: string;
adminConsentGrantedBy?: string;
availableTeams?: Record<string, MicrosoftTeamsTeam>;
appAccessTokenExpiresAt?: string;
}
export type WorkspaceMiscData = SlackMiscData | MicrosoftTeamsMiscData;
@TenantColumn("projectId")
@TableAccessControl({
create: [

View File

@@ -235,13 +235,27 @@ export default class BaseAPI<
): Promise<void> {
await this.onBeforeList(req, res);
const skip: PositiveNumber = req.query["skip"]
? new PositiveNumber(req.query["skip"] as string)
: new PositiveNumber(0);
/*
* Extract pagination parameters from query or body (for POST requests)
* Support both 'skip' and 'offset' parameters (offset is alias for skip)
*/
let skipValue: number = 0;
let limitValue: number = DEFAULT_LIMIT;
const limit: PositiveNumber = req.query["limit"]
? new PositiveNumber(req.query["limit"] as string)
: new PositiveNumber(DEFAULT_LIMIT);
if (req.query["skip"]) {
skipValue = parseInt(req.query["skip"] as string, 10) || 0;
} else if (req.body && req.body["skip"] !== undefined) {
skipValue = parseInt(req.body["skip"] as string, 10) || 0;
}
if (req.query["limit"]) {
limitValue = parseInt(req.query["limit"] as string, 10) || DEFAULT_LIMIT;
} else if (req.body && req.body["limit"] !== undefined) {
limitValue = parseInt(req.body["limit"] as string, 10) || DEFAULT_LIMIT;
}
const skip: PositiveNumber = new PositiveNumber(skipValue);
const limit: PositiveNumber = new PositiveNumber(limitValue);
if (limit.toNumber() > LIMIT_PER_PROJECT) {
throw new BadRequestException(

View File

@@ -0,0 +1,123 @@
import { IsBillingEnabled } from "../EnvironmentConfig";
import UserMiddleware from "../Middleware/UserAuthorization";
import BillingService from "../Services/BillingService";
import ProjectService from "../Services/ProjectService";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
OneUptimeRequest,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BadDataException from "../../Types/Exception/BadDataException";
import Permission, { UserPermission } from "../../Types/Permission";
import Project from "../../Models/DatabaseModels/Project";
import CommonAPI from "./CommonAPI";
import ObjectID from "../../Types/ObjectID";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
export default class BillingAPI {
public router: ExpressRouter;
public constructor() {
this.router = Express.getRouter();
this.router.get(
`/billing/customer-balance`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
if (!IsBillingEnabled) {
throw new BadDataException(
"Billing is not enabled for this server",
);
}
const userPermissions: Array<UserPermission> = (
await this.getPermissionsForTenant(req)
).filter((permission: UserPermission) => {
return (
permission.permission.toString() ===
Permission.ProjectOwner.toString() ||
permission.permission.toString() ===
Permission.ManageProjectBilling.toString()
);
});
if (
userPermissions.length === 0 &&
!(req as OneUptimeRequest).userAuthorization?.isMasterAdmin
) {
throw new BadDataException(
`You need ${Permission.ProjectOwner} or ${Permission.ManageProjectBilling} permission to view billing balance.`,
);
}
const project: Project | null = await ProjectService.findOneById({
id: this.getTenantId(req)!,
props: {
isRoot: true,
},
select: {
_id: true,
paymentProviderCustomerId: true,
},
});
if (!project) {
throw new BadDataException("Project not found");
}
if (!project.paymentProviderCustomerId) {
throw new BadDataException("Payment Provider customer not found");
}
const balance: number = await BillingService.getCustomerBalance(
project.paymentProviderCustomerId,
);
return Response.sendJsonObjectResponse(req, res, {
balance: balance,
});
} catch (err) {
next(err);
}
},
);
}
public async getPermissionsForTenant(
req: ExpressRequest,
): Promise<Array<UserPermission>> {
const permissions: Array<UserPermission> = [];
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
if (
props &&
props.userTenantAccessPermission &&
props.userTenantAccessPermission[props.tenantId?.toString() || ""]
) {
return (
props.userTenantAccessPermission[props.tenantId?.toString() || ""]
?.permissions || []
);
}
return permissions;
}
public getTenantId(req: ExpressRequest): ObjectID | null {
if ((req as OneUptimeRequest).tenantId) {
return (req as OneUptimeRequest).tenantId as ObjectID;
}
return null;
}
public getRouter(): ExpressRouter {
return this.router;
}
}

View File

@@ -21,20 +21,24 @@ export default class GlobalConfigAPI extends BaseAPI<
`${new this.entityType().getCrudApiPath()?.toString()}/vars`,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
// const globalConfig: GlobalConfig | null =
// await GlobalConfigService.findOneById({
// id: ObjectID.getZeroObjectID(),
// select: {
// useHttps: true,
// },
// props: {
// isRoot: true,
// },
// });
/*
* const globalConfig: GlobalConfig | null =
* await GlobalConfigService.findOneById({
* id: ObjectID.getZeroObjectID(),
* select: {
* useHttps: true,
* },
* props: {
* isRoot: true,
* },
* });
*/
return Response.sendJsonObjectResponse(req, res, {
// USE_HTTPS:
// globalConfig?.useHttps?.toString() || 'false',
/*
* USE_HTTPS:
* globalConfig?.useHttps?.toString() || 'false',
*/
});
} catch (err) {
next(err);

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,11 @@ import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
OneUptimeRequest,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BadDataException from "../../Types/Exception/BadDataException";
import Exception from "../../Types/Exception/Exception";
import JSONFunctions from "../../Types/JSONFunctions";
import ObjectID from "../../Types/ObjectID";
import Permission, { UserPermission } from "../../Types/Permission";
@@ -19,7 +19,7 @@ const router: ExpressRouter = Express.getRouter();
router.post(
"/notification/recharge",
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
let amount: number | PositiveNumber = JSONFunctions.deserializeValue(
req.body.amount,
@@ -106,8 +106,8 @@ router.post(
),
);
}
} catch (err: any) {
return Response.sendErrorResponse(req, res, err as Exception);
} catch (err) {
return next(err);
}
return Response.sendEmptySuccessResponse(req, res);

View File

@@ -25,8 +25,10 @@ export default class ProjectAPI extends BaseAPI<Project, ProjectServiceType> {
public constructor() {
super(Project, ProjectService);
/// This API lists all the projects where user is its team member.
/// This API is usually used to show project selector dropdown in the UI
/*
* This API lists all the projects where user is its team member.
* This API is usually used to show project selector dropdown in the UI
*/
this.router.post(
`${new this.entityType()
.getCrudApiPath()

View File

@@ -1,7 +1,11 @@
import ProjectSsoService, {
Service as ProjectSsoServiceType,
} from "../Services/ProjectSsoService";
import { ExpressRequest, ExpressResponse } from "../Utils/Express";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
@@ -22,43 +26,47 @@ export default class ProjectSsoAPI extends BaseAPI<
`${new this.entityType()
.getCrudApiPath()
?.toString()}/:projectId/sso-list`,
async (req: ExpressRequest, res: ExpressResponse) => {
const projectId: ObjectID = new ObjectID(
req.params["projectId"] as string,
);
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const projectId: ObjectID = new ObjectID(
req.params["projectId"] as string,
);
if (!projectId) {
return Response.sendErrorResponse(
if (!projectId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid project id."),
);
}
const sso: Array<ProjectSSO> = await this.service.findBy({
query: {
projectId: projectId,
isEnabled: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
select: {
name: true,
description: true,
_id: true,
},
props: {
isRoot: true,
},
});
return Response.sendEntityArrayResponse(
req,
res,
new BadDataException("Invalid project id."),
sso,
new PositiveNumber(sso.length),
ProjectSSO,
);
} catch (err) {
return next(err);
}
const sso: Array<ProjectSSO> = await this.service.findBy({
query: {
projectId: projectId,
isEnabled: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
select: {
name: true,
description: true,
_id: true,
},
props: {
isRoot: true,
},
});
return Response.sendEntityArrayResponse(
req,
res,
sso,
new PositiveNumber(sso.length),
ProjectSSO,
);
},
);
}

View File

@@ -30,8 +30,10 @@ export default class ResellerPlanAPI extends BaseAPI<
public constructor() {
super(ResellerPlan, ResellerPlanService);
// Reseller Plan Action API
// TODO: Refactor this API and make it partner specific.
/*
* Reseller Plan Action API
* TODO: Refactor this API and make it partner specific.
*/
this.router.post(
`${new this.entityType()
.getCrudApiPath()

View File

@@ -1,7 +1,11 @@
import ShortLinkService, {
Service as ShortLinkServiceType,
} from "../Services/ShortLinkService";
import { ExpressRequest, ExpressResponse } from "../Utils/Express";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import BadDataException from "../../Types/Exception/BadDataException";
@@ -18,34 +22,38 @@ export default class ShortLinkAPI extends BaseAPI<
`${new this.entityType()
.getCrudApiPath()
?.toString()}/redirect-to-shortlink/:id`,
async (req: ExpressRequest, res: ExpressResponse) => {
if (!req.params["id"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("id is required"),
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
if (!req.params["id"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("id is required"),
);
}
if (req.params["id"] === "status") {
return Response.sendJsonObjectResponse(req, res, {
status: "ok",
});
}
const link: ShortLink | null = await ShortLinkService.getShortLinkFor(
req.params["id"],
);
if (!link || !link.link) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("This URL is invalid or expired"),
);
}
return Response.redirect(req, res, link.link);
} catch (err) {
return next(err);
}
if (req.params["id"] === "status") {
return Response.sendJsonObjectResponse(req, res, {
status: "ok",
});
}
const link: ShortLink | null = await ShortLinkService.getShortLinkFor(
req.params["id"],
);
if (!link || !link.link) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("This URL is invalid or expired"),
);
}
return Response.redirect(req, res, link.link);
},
);
}

View File

@@ -38,6 +38,10 @@ import SlackOnCallDutyActions from "../Utils/Workspace/Slack/Actions/OnCallDutyP
import WorkspaceProjectAuthToken, {
SlackMiscData,
} from "../../Models/DatabaseModels/WorkspaceProjectAuthToken";
import UserMiddleware from "../Middleware/UserAuthorization";
import CommonAPI from "./CommonAPI";
import SlackUtil from "../Utils/Workspace/Slack/Slack";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
export default class SlackAPI {
public getRouter(): ExpressRouter {
@@ -153,13 +157,13 @@ export default class SlackAPI {
// 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,
{
await API.post({
url: URL.fromString("https://slack.com/api/oauth.v2.access"),
data: requestBody,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
);
});
if (response instanceof HTTPErrorResponse) {
throw response;
@@ -177,29 +181,31 @@ export default class SlackAPI {
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"
// }
// }
/*
* 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(
@@ -365,13 +371,13 @@ export default class SlackAPI {
// 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/openid.connect.token"),
requestBody,
{
await API.post({
url: URL.fromString("https://slack.com/api/openid.connect.token"),
data: requestBody,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
);
});
if (response instanceof HTTPErrorResponse) {
throw response;
@@ -403,31 +409,35 @@ export default class SlackAPI {
"id_token"
] as JSONObject;
// Example of Response Body
// {
// "iss": "https://slack.com",
// "sub": "U123ABC456",
// "aud": "25259531569.1115258246291",
// "exp": 1626874955,
// "iat": 1626874655,
// "auth_time": 1626874655,
// "nonce": "abcd",
// "at_hash": "abc...123",
// "https://slack.com/team_id": "T0123ABC456",
// "https://slack.com/user_id": "U123ABC456",
// "email": "alice@example.com",
// "email_verified": true,
// "date_email_verified": 1622128723,
// "locale": "en-US",
// "name": "Alice",
// "given_name": "",
// "family_name": "",
// "https://slack.com/team_image_230": "https://secure.gravatar.com/avatar/bc.png",
// "https://slack.com/team_image_default": true
// }
/*
* Example of Response Body
* {
* "iss": "https://slack.com",
* "sub": "U123ABC456",
* "aud": "25259531569.1115258246291",
* "exp": 1626874955,
* "iat": 1626874655,
* "auth_time": 1626874655,
* "nonce": "abcd",
* "at_hash": "abc...123",
* "https://slack.com/team_id": "T0123ABC456",
* "https://slack.com/user_id": "U123ABC456",
* "email": "alice@example.com",
* "email_verified": true,
* "date_email_verified": 1622128723,
* "locale": "en-US",
* "name": "Alice",
* "given_name": "",
* "family_name": "",
* "https://slack.com/team_image_230": "https://secure.gravatar.com/avatar/bc.png",
* "https://slack.com/team_image_default": true
* }
*/
// check if the team id matches the project id.
// get project auth.
/*
* check if the team id matches the project id.
* get project auth.
*/
const projectAuth: WorkspaceProjectAuthToken | null =
await WorkspaceProjectAuthTokenService.findOneBy({
@@ -656,6 +666,71 @@ export default class SlackAPI {
},
);
// Fetch and cache all Slack channels for current tenant's project
router.get(
"/slack/get-all-channels",
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
if (!props.tenantId) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException("ProjectId (tenant) not found in request"),
);
}
// Get Slack project auth
const projectAuth: WorkspaceProjectAuthToken | null =
await WorkspaceProjectAuthTokenService.getProjectAuth({
projectId: props.tenantId,
workspaceType: WorkspaceType.Slack,
});
if (!projectAuth || !projectAuth.authToken) {
return Response.sendErrorResponse(
req,
res,
new BadRequestException(
"Slack is not connected for this project. Please connect Slack first.",
),
);
}
// Fetch all channels (also updates cache under miscData.channelCache)
let updatedProjectAuth: WorkspaceProjectAuthToken | null = projectAuth;
if (!(projectAuth.miscData as SlackMiscData)?.channelCache) {
await SlackUtil.getAllWorkspaceChannels({
authToken: projectAuth.authToken,
projectId: props.tenantId,
});
// Re-fetch to return the latest cached object
updatedProjectAuth =
await WorkspaceProjectAuthTokenService.getProjectAuth({
projectId: props.tenantId,
workspaceType: WorkspaceType.Slack,
});
}
const channelCache: {
[channelName: string]: {
id: string;
name: string;
lastUpdated: string;
};
} =
((updatedProjectAuth?.miscData as SlackMiscData | undefined) || {})
?.channelCache || {};
return Response.sendJsonObjectResponse(req, res, channelCache as any);
},
);
// options load endpoint.
router.post(

View File

@@ -181,6 +181,7 @@ export default class StatusPageAPI extends BaseAPI<
return Response.sendJsonObjectResponse(req, res, {
title: statusPage.pageTitle || statusPage.name,
description: statusPage.pageDescription,
_id: statusPage._id?.toString(),
});
},
);
@@ -734,9 +735,11 @@ export default class StatusPageAPI extends BaseAPI<
req: req,
});
// 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.
/*
* 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();
@@ -1100,7 +1103,7 @@ export default class StatusPageAPI extends BaseAPI<
},
select: select,
sort: {
createdAt: SortOrder.Ascending,
createdAt: SortOrder.Descending,
},
skip: 0,
@@ -2076,6 +2079,10 @@ export default class StatusPageAPI extends BaseAPI<
_id: true,
showAnnouncementAt: true,
endAnnouncementAt: true,
monitors: {
_id: true,
name: true,
},
},
skip: 0,
limit: LIMIT_PER_PROJECT,
@@ -2096,6 +2103,7 @@ export default class StatusPageAPI extends BaseAPI<
displayTooltip: true,
displayDescription: true,
displayName: true,
monitorGroupId: true,
monitor: {
_id: true,
currentMonitorStatusId: true,
@@ -2109,6 +2117,65 @@ export default class StatusPageAPI extends BaseAPI<
},
});
const monitorGroupIds: Array<ObjectID> = statusPageResources
.map((resource: StatusPageResource) => {
return resource.monitorGroupId!;
})
.filter((id: ObjectID) => {
return Boolean(id); // remove nulls
});
// get monitors in the group.
const monitorsInGroup: Dictionary<Array<ObjectID>> = {};
// get monitor status charts.
const monitorsOnStatusPage: Array<ObjectID> = statusPageResources
.map((monitor: StatusPageResource) => {
return monitor.monitorId!;
})
.filter((id: ObjectID) => {
return Boolean(id); // remove nulls
});
for (const monitorGroupId of monitorGroupIds) {
// get monitors in the group.
const groupResources: Array<MonitorGroupResource> =
await MonitorGroupResourceService.findBy({
query: {
monitorGroupId: monitorGroupId,
},
select: {
monitorId: true,
},
props: {
isRoot: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
});
const monitorsInGroupIds: Array<ObjectID> = groupResources
.map((resource: MonitorGroupResource) => {
return resource.monitorId!;
})
.filter((id: ObjectID) => {
return Boolean(id); // remove nulls
});
for (const monitorId of monitorsInGroupIds) {
if (
!monitorsOnStatusPage.find((item: ObjectID) => {
return item.toString() === monitorId.toString();
})
) {
monitorsOnStatusPage.push(monitorId);
}
}
monitorsInGroup[monitorGroupId.toString()] = monitorsInGroupIds;
}
const response: JSONObject = {
announcements: BaseModel.toJSONArray(
announcements,
@@ -2118,6 +2185,7 @@ export default class StatusPageAPI extends BaseAPI<
statusPageResources,
StatusPageResource,
),
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
};
return response;

View File

@@ -0,0 +1,106 @@
import UserMiddleware from "../Middleware/UserAuthorization";
import TeamComplianceService, {
TeamComplianceStatus,
UserComplianceStatus,
} from "../Services/TeamComplianceService";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import CommonAPI from "./CommonAPI";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
import BadDataException from "../../Types/Exception/BadDataException";
import ObjectID from "../../Types/ObjectID";
import Team from "../../Models/DatabaseModels/Team";
import TeamService, {
Service as TeamServiceType,
} from "../Services/TeamService";
import ComplianceRuleType from "../../Types/Team/ComplianceRuleType";
export default class TeamComplianceAPI extends BaseAPI<Team, TeamServiceType> {
public constructor() {
super(Team, TeamService);
// Get team compliance status
this.router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/compliance-status/:teamId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const databaseProps: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
const projectId: ObjectID = databaseProps.tenantId as ObjectID;
const teamId: ObjectID = new ObjectID(req.params["teamId"] as string);
if (!projectId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid projectId."),
);
}
if (!teamId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid teamId."),
);
}
const complianceStatus: TeamComplianceStatus =
await TeamComplianceService.getTeamComplianceStatus(
teamId,
projectId,
);
// Convert ObjectIDs to strings for JSON response
const responseData: {
teamId: string;
teamName: string;
complianceSettings: Array<{
ruleType: ComplianceRuleType;
enabled: boolean;
}>;
userComplianceStatuses: Array<{
userId: string;
userName: string;
userEmail: string;
userProfilePictureId: string | undefined;
isCompliant: boolean;
nonCompliantRules: Array<{
ruleType: ComplianceRuleType;
reason: string;
}>;
}>;
} = {
teamId: complianceStatus.teamId.toString(),
teamName: complianceStatus.teamName,
complianceSettings: complianceStatus.complianceSettings,
userComplianceStatuses: complianceStatus.userComplianceStatuses.map(
(user: UserComplianceStatus) => {
return {
userId: user.userId.toString(),
userName: user.userName,
userEmail: user.userEmail,
userProfilePictureId: user.userProfilePictureId?.toString(),
isCompliant: user.isCompliant,
nonCompliantRules: user.nonCompliantRules,
};
},
),
};
return Response.sendJsonObjectResponse(req, res, responseData);
} catch (e) {
next(e);
}
},
);
}
}

View File

@@ -5,6 +5,7 @@ import UserCallService, {
import {
ExpressRequest,
ExpressResponse,
NextFunction,
OneUptimeRequest,
} from "../Utils/Express";
import Response from "../Utils/Response";
@@ -23,97 +24,105 @@ export default class UserCallAPI extends BaseAPI<
this.router.post(
`/user-call/verify`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
req = req as OneUptimeRequest;
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
req = req as OneUptimeRequest;
if (!req.body.itemId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
if (!req.body.itemId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
}
if (!req.body.code) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code"),
);
}
// Check if the code matches and verify the phone number.
const item: UserSMS | null = await this.service.findOneById({
id: req.body["itemId"],
props: {
isRoot: true,
},
select: {
userId: true,
verificationCode: true,
},
});
if (!item) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Item not found"),
);
}
//check user id
if (
item.userId?.toString() !==
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid user ID"),
);
}
if (item.verificationCode !== req.body["code"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code"),
);
}
await this.service.updateOneById({
id: item.id!,
props: {
isRoot: true,
},
data: {
isVerified: true,
},
});
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
if (!req.body.code) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code"),
);
}
// Check if the code matches and verify the phone number.
const item: UserSMS | null = await this.service.findOneById({
id: req.body["itemId"],
props: {
isRoot: true,
},
select: {
userId: true,
verificationCode: true,
},
});
if (!item) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Item not found"),
);
}
//check user id
if (
item.userId?.toString() !==
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid user ID"),
);
}
if (item.verificationCode !== req.body["code"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code"),
);
}
await this.service.updateOneById({
id: item.id!,
props: {
isRoot: true,
},
data: {
isVerified: true,
},
});
return Response.sendEmptySuccessResponse(req, res);
},
);
this.router.post(
`/user-call/resend-verification-code`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
req = req as OneUptimeRequest;
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
req = req as OneUptimeRequest;
if (!req.body.itemId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
if (!req.body.itemId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
}
await this.service.resendVerificationCode(req.body.itemId);
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
await this.service.resendVerificationCode(req.body.itemId);
return Response.sendEmptySuccessResponse(req, res);
},
);
}

View File

@@ -5,6 +5,7 @@ import UserEmailService, {
import {
ExpressRequest,
ExpressResponse,
NextFunction,
OneUptimeRequest,
} from "../Utils/Express";
import Response from "../Utils/Response";
@@ -22,77 +23,81 @@ export default class UserEmailAPI extends BaseAPI<
this.router.post(
`${new this.entityType().getCrudApiPath()?.toString()}/verify`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
req = req as OneUptimeRequest;
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
req = req as OneUptimeRequest;
if (!req.body.itemId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
if (!req.body.itemId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
}
if (!req.body.code) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code"),
);
}
// Check if the code matches and verify the email.
const item: UserEmail | null = await this.service.findOneById({
id: req.body["itemId"],
props: {
isRoot: true,
},
select: {
userId: true,
verificationCode: true,
},
});
if (!item) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Item not found"),
);
}
//check user id
if (
item.userId?.toString() !==
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid user ID"),
);
}
if (item.verificationCode !== req.body["code"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code"),
);
}
await this.service.updateOneById({
id: item.id!,
props: {
isRoot: true,
},
data: {
isVerified: true,
},
});
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
if (!req.body.code) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code"),
);
}
// Check if the code matches and verify the email.
const item: UserEmail | null = await this.service.findOneById({
id: req.body["itemId"],
props: {
isRoot: true,
},
select: {
userId: true,
verificationCode: true,
},
});
if (!item) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Item not found"),
);
}
//check user id
if (
item.userId?.toString() !==
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid user ID"),
);
}
if (item.verificationCode !== req.body["code"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code"),
);
}
await this.service.updateOneById({
id: item.id!,
props: {
isRoot: true,
},
data: {
isVerified: true,
},
});
return Response.sendEmptySuccessResponse(req, res);
},
);
@@ -101,20 +106,24 @@ export default class UserEmailAPI extends BaseAPI<
.getCrudApiPath()
?.toString()}/resend-verification-code`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
req = req as OneUptimeRequest;
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
req = req as OneUptimeRequest;
if (!req.body.itemId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
if (!req.body.itemId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
}
await this.service.resendVerificationCode(req.body.itemId);
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
await this.service.resendVerificationCode(req.body.itemId);
return Response.sendEmptySuccessResponse(req, res);
},
);
}

View File

@@ -6,6 +6,7 @@ import UserOnCallLogTimelineService, {
import {
ExpressRequest,
ExpressResponse,
NextFunction,
OneUptimeRequest,
} from "../Utils/Express";
import Response from "../Utils/Response";
@@ -34,47 +35,228 @@ export default class UserNotificationLogTimelineAPI extends BaseAPI<
.getCrudApiPath()
?.toString()}/call/gather-input/:itemId`,
NotificationMiddleware.isValidCallNotificationRequest,
async (req: ExpressRequest, res: ExpressResponse) => {
req = req as OneUptimeRequest;
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
req = req as OneUptimeRequest;
if (!req.params["itemId"]) {
return Response.sendErrorResponse(
if (!req.params["itemId"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
}
const token: JSONObject = (req as any).callTokenData;
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
const timelineItem: UserOnCallLogTimeline | null =
await this.service.findOneById({
id: itemId,
select: {
_id: true,
projectId: true,
triggeredByIncidentId: true,
triggeredByAlertId: true,
},
props: {
isRoot: true,
},
});
if (!timelineItem) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item Id"),
);
}
// check digits.
if (req.body["Digits"] === "1") {
// then ack incident
await this.service.updateOneById({
id: itemId,
data: {
acknowledgedAt: OneUptimeDate.getCurrentDate(),
isAcknowledged: true,
status: UserNotificationStatus.Acknowledged,
statusMessage: "Notification Acknowledged",
},
props: {
isRoot: true,
},
});
}
return NotificationMiddleware.sendResponse(req, res, token as any);
} catch (error) {
return next(error);
}
},
);
/*
* We have this ack page to show the user a confirmation page before acknowledging the notification.
* this is because email clients automatically make a get request to the url in the email and ack the notification automatically which is not what we want.
* so we need to create this page for the user to confirm that they want to acknowledge the notification.
*/
this.router.get(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/acknowledge-page/:itemId`,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
req = req as OneUptimeRequest;
if (!req.params["itemId"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Item ID is required"),
);
}
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
const timelineItem: UserOnCallLogTimeline | null =
await this.service.findOneById({
id: itemId,
select: {
_id: true,
projectId: true,
triggeredByIncidentId: true,
triggeredByIncident: {
title: true,
description: true,
},
triggeredByAlertId: true,
triggeredByAlert: {
title: true,
description: true,
},
},
props: {
isRoot: true,
},
});
if (!timelineItem) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item Id"),
);
}
const notificationType: string = timelineItem.triggeredByIncidentId
? "Incident"
: "Alert";
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
return Response.render(
req,
res,
new BadDataException("Invalid item ID"),
);
}
const token: JSONObject = (req as any).callTokenData;
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
const timelineItem: UserOnCallLogTimeline | null =
await this.service.findOneById({
id: itemId,
select: {
_id: true,
projectId: true,
triggeredByIncidentId: true,
triggeredByAlertId: true,
"/usr/src/Common/Server/Views/AcknowledgeUserOnCallNotification.ejs",
{
title: `Acknowledge ${notificationType} - ${timelineItem.triggeredByIncident?.title || timelineItem.triggeredByAlert?.title}`,
message: `Do you want to acknowledge this ${notificationType}?`,
acknowledgeText: `Acknowledge ${notificationType}`,
acknowledgeUrl: new URL(
httpProtocol,
host,
new Route(AppApiRoute.toString())
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
.addRoute("/acknowledge/" + itemId.toString()),
).toString(),
},
props: {
isRoot: true,
},
});
if (!timelineItem) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item Id"),
);
} catch (error) {
return next(error);
}
},
);
// check digits.
// This is the link that actually acknowledges the notification.
this.router.get(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/acknowledge/:itemId`,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
req = req as OneUptimeRequest;
if (!req.params["itemId"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Item ID is required"),
);
}
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
const timelineItem: UserOnCallLogTimeline | null =
await this.service.findOneById({
id: itemId,
select: {
_id: true,
projectId: true,
triggeredByIncidentId: true,
triggeredByAlertId: true,
triggeredByAlert: {
title: true,
},
triggeredByIncident: {
title: true,
},
acknowledgedAt: true,
isAcknowledged: true,
},
props: {
isRoot: true,
},
});
if (!timelineItem) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item Id"),
);
}
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
if (timelineItem.isAcknowledged) {
// already acknowledged. Then show already acknowledged page with view details button.
const viewDetailsUrl: URL = new URL(
httpProtocol,
host,
DashboardRoute.addRoute(
`/${timelineItem.projectId?.toString()}/${timelineItem.triggeredByIncidentId ? "incidents" : "alerts"}/${timelineItem.triggeredByIncidentId ? timelineItem.triggeredByIncidentId!.toString() : timelineItem.triggeredByAlertId!.toString()}`,
),
);
return Response.render(
req,
res,
"/usr/src/Common/Server/Views/ViewMessage.ejs",
{
title: `Notification Already Acknowledged - ${timelineItem.triggeredByIncident?.title || timelineItem.triggeredByAlert?.title}`,
message: `This notification has already been acknowledged.`,
viewDetailsText: `View ${timelineItem.triggeredByIncidentId ? "Incident" : "Alert"}`,
viewDetailsUrl: viewDetailsUrl.toString(),
},
);
}
if (req.body["Digits"] === "1") {
// then ack incident
await this.service.updateOneById({
id: itemId,
data: {
@@ -87,212 +269,45 @@ export default class UserNotificationLogTimelineAPI extends BaseAPI<
isRoot: true,
},
});
}
return NotificationMiddleware.sendResponse(req, res, token as any);
},
);
// redirect to dashboard to incidents page.
// We have this ack page to show the user a confirmation page before acknowledging the notification.
// this is because email clients automatically make a get request to the url in the email and ack the notification automatically which is not what we want.
// so we need to create this page for the user to confirm that they want to acknowledge the notification.
this.router.get(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/acknowledge-page/:itemId`,
async (req: ExpressRequest, res: ExpressResponse) => {
req = req as OneUptimeRequest;
if (timelineItem.triggeredByIncidentId) {
return Response.redirect(
req,
res,
new URL(
httpProtocol,
host,
DashboardRoute.addRoute(
`/${timelineItem.projectId?.toString()}/incidents/${timelineItem.triggeredByIncidentId!.toString()}`,
),
),
);
}
if (!req.params["itemId"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Item ID is required"),
);
}
if (timelineItem.triggeredByAlertId) {
return Response.redirect(
req,
res,
new URL(
httpProtocol,
host,
DashboardRoute.addRoute(
`/${timelineItem.projectId?.toString()}/alerts/${timelineItem.triggeredByAlertId!.toString()}`,
),
),
);
}
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
const timelineItem: UserOnCallLogTimeline | null =
await this.service.findOneById({
id: itemId,
select: {
_id: true,
projectId: true,
triggeredByIncidentId: true,
triggeredByIncident: {
title: true,
description: true,
},
triggeredByAlertId: true,
triggeredByAlert: {
title: true,
description: true,
},
},
props: {
isRoot: true,
},
});
if (!timelineItem) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item Id"),
);
} catch (error) {
return next(error);
}
const notificationType: string = timelineItem.triggeredByIncidentId
? "Incident"
: "Alert";
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
return Response.render(
req,
res,
"/usr/src/Common/Server/Views/AcknowledgeUserOnCallNotification.ejs",
{
title: `Acknowledge ${notificationType} - ${timelineItem.triggeredByIncident?.title || timelineItem.triggeredByAlert?.title}`,
message: `Do you want to acknowledge this ${notificationType}?`,
acknowledgeText: `Acknowledge ${notificationType}`,
acknowledgeUrl: new URL(
httpProtocol,
host,
new Route(AppApiRoute.toString())
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
.addRoute("/acknowledge/" + itemId.toString()),
).toString(),
},
);
},
);
// This is the link that actually acknowledges the notification.
this.router.get(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/acknowledge/:itemId`,
async (req: ExpressRequest, res: ExpressResponse) => {
req = req as OneUptimeRequest;
if (!req.params["itemId"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Item ID is required"),
);
}
const itemId: ObjectID = new ObjectID(req.params["itemId"]);
const timelineItem: UserOnCallLogTimeline | null =
await this.service.findOneById({
id: itemId,
select: {
_id: true,
projectId: true,
triggeredByIncidentId: true,
triggeredByAlertId: true,
triggeredByAlert: {
title: true,
},
triggeredByIncident: {
title: true,
},
acknowledgedAt: true,
isAcknowledged: true,
},
props: {
isRoot: true,
},
});
if (!timelineItem) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item Id"),
);
}
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
if (timelineItem.isAcknowledged) {
// already acknowledged. Then show already acknowledged page with view details button.
const viewDetailsUrl: URL = new URL(
httpProtocol,
host,
DashboardRoute.addRoute(
`/${timelineItem.projectId?.toString()}/${timelineItem.triggeredByIncidentId ? "incidents" : "alerts"}/${timelineItem.triggeredByIncidentId ? timelineItem.triggeredByIncidentId!.toString() : timelineItem.triggeredByAlertId!.toString()}`,
),
);
return Response.render(
req,
res,
"/usr/src/Common/Server/Views/ViewMessage.ejs",
{
title: `Notification Already Acknowledged - ${timelineItem.triggeredByIncident?.title || timelineItem.triggeredByAlert?.title}`,
message: `This notification has already been acknowledged.`,
viewDetailsText: `View ${timelineItem.triggeredByIncidentId ? "Incident" : "Alert"}`,
viewDetailsUrl: viewDetailsUrl.toString(),
},
);
}
await this.service.updateOneById({
id: itemId,
data: {
acknowledgedAt: OneUptimeDate.getCurrentDate(),
isAcknowledged: true,
status: UserNotificationStatus.Acknowledged,
statusMessage: "Notification Acknowledged",
},
props: {
isRoot: true,
},
});
// redirect to dashboard to incidents page.
if (timelineItem.triggeredByIncidentId) {
return Response.redirect(
req,
res,
new URL(
httpProtocol,
host,
DashboardRoute.addRoute(
`/${timelineItem.projectId?.toString()}/incidents/${timelineItem.triggeredByIncidentId!.toString()}`,
),
),
);
}
if (timelineItem.triggeredByAlertId) {
return Response.redirect(
req,
res,
new URL(
httpProtocol,
host,
DashboardRoute.addRoute(
`/${timelineItem.projectId?.toString()}/alerts/${timelineItem.triggeredByAlertId!.toString()}`,
),
),
);
}
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item Id"),
);
},
);
}

View File

@@ -108,104 +108,104 @@ export default class UserPushAPI extends BaseAPI<
this.router.post(
`/user-push/:deviceId/test-notification`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
req = req as OneUptimeRequest;
if (!req.params["deviceId"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device ID is required"),
);
}
// Get the device
const device: UserPush | null = await this.service.findOneById({
id: new ObjectID(req.params["deviceId"]),
props: {
isRoot: true,
},
select: {
userId: true,
deviceName: true,
deviceToken: true,
deviceType: true,
isVerified: true,
projectId: true,
},
});
if (!device) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device not found"),
);
}
// Check if the device belongs to the current user
if (
device.userId?.toString() !==
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Unauthorized access to device"),
);
}
if (!device.isVerified) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device is not verified"),
);
}
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
// Send test notification
const testMessage: PushNotificationMessage =
PushNotificationUtil.createGenericNotification({
title: "Test Notification from OneUptime",
body: "This is a test notification to verify your device is working correctly.",
clickAction: "/dashboard",
tag: "test-notification",
requireInteraction: false,
});
req = req as OneUptimeRequest;
await PushNotificationService.sendPushNotification(
{
devices: [
{
token: device.deviceToken!,
...(device.deviceName && {
name: device.deviceName,
}),
},
],
message: testMessage,
deviceType: device.deviceType!,
if (!req.params["deviceId"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device ID is required"),
);
}
// Get the device
const device: UserPush | null = await this.service.findOneById({
id: new ObjectID(req.params["deviceId"]),
props: {
isRoot: true,
},
{
isSensitive: false,
projectId: device.projectId!,
userId: device.userId!,
select: {
userId: true,
deviceName: true,
deviceToken: true,
deviceType: true,
isVerified: true,
projectId: true,
},
);
});
if (!device) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device not found"),
);
}
// Check if the device belongs to the current user
if (
device.userId?.toString() !==
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Unauthorized access to device"),
);
}
if (!device.isVerified) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device is not verified"),
);
}
try {
// Send test notification
const testMessage: PushNotificationMessage =
PushNotificationUtil.createGenericNotification({
title: "Test Notification from OneUptime",
body: "This is a test notification to verify your device is working correctly.",
clickAction: "/dashboard",
tag: "test-notification",
requireInteraction: false,
});
await PushNotificationService.sendPushNotification(
{
devices: [
{
token: device.deviceToken!,
...(device.deviceName && {
name: device.deviceName,
}),
},
],
message: testMessage,
deviceType: device.deviceType!,
},
{
isSensitive: false,
projectId: device.projectId!,
userId: device.userId!,
},
);
} catch (error: any) {
throw new BadDataException(
`Failed to send test notification: ${error.message}`,
);
}
return Response.sendJsonObjectResponse(req, res, {
success: true,
message: "Test notification sent successfully",
});
} catch (error: any) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
`Failed to send test notification: ${error.message}`,
),
);
} catch (error) {
return next(error);
}
},
);
@@ -213,100 +213,108 @@ export default class UserPushAPI extends BaseAPI<
this.router.post(
`/user-push/:deviceId/verify`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
req = req as OneUptimeRequest;
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
req = req as OneUptimeRequest;
if (!req.params["deviceId"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device ID is required"),
);
if (!req.params["deviceId"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device ID is required"),
);
}
const device: UserPush | null = await this.service.findOneById({
id: new ObjectID(req.params["deviceId"]),
props: {
isRoot: true,
},
select: {
userId: true,
},
});
if (!device) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device not found"),
);
}
// Check if the device belongs to the current user
if (
device.userId?.toString() !==
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Unauthorized access to device"),
);
}
await this.service.verifyDevice(device._id!.toString());
return Response.sendEmptySuccessResponse(req, res);
} catch (error) {
return next(error);
}
const device: UserPush | null = await this.service.findOneById({
id: new ObjectID(req.params["deviceId"]),
props: {
isRoot: true,
},
select: {
userId: true,
},
});
if (!device) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device not found"),
);
}
// Check if the device belongs to the current user
if (
device.userId?.toString() !==
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Unauthorized access to device"),
);
}
await this.service.verifyDevice(device._id!.toString());
return Response.sendEmptySuccessResponse(req, res);
},
);
this.router.post(
`/user-push/:deviceId/unverify`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
req = req as OneUptimeRequest;
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
req = req as OneUptimeRequest;
if (!req.params["deviceId"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device ID is required"),
);
if (!req.params["deviceId"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device ID is required"),
);
}
const device: UserPush | null = await this.service.findOneById({
id: new ObjectID(req.params["deviceId"]),
props: {
isRoot: true,
},
select: {
userId: true,
},
});
if (!device) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device not found"),
);
}
// Check if the device belongs to the current user
if (
device.userId?.toString() !==
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Unauthorized access to device"),
);
}
await this.service.unverifyDevice(device._id!.toString());
return Response.sendEmptySuccessResponse(req, res);
} catch (error) {
return next(error);
}
const device: UserPush | null = await this.service.findOneById({
id: new ObjectID(req.params["deviceId"]),
props: {
isRoot: true,
},
select: {
userId: true,
},
});
if (!device) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Device not found"),
);
}
// Check if the device belongs to the current user
if (
device.userId?.toString() !==
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Unauthorized access to device"),
);
}
await this.service.unverifyDevice(device._id!.toString());
return Response.sendEmptySuccessResponse(req, res);
},
);
}

View File

@@ -5,6 +5,7 @@ import UserSMSService, {
import {
ExpressRequest,
ExpressResponse,
NextFunction,
OneUptimeRequest,
} from "../Utils/Express";
import Response from "../Utils/Response";
@@ -19,97 +20,105 @@ export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
this.router.post(
`/user-sms/verify`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
req = req as OneUptimeRequest;
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
req = req as OneUptimeRequest;
if (!req.body.itemId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
if (!req.body.itemId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
}
if (!req.body.code) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code"),
);
}
// Check if the code matches and verify the phone number.
const item: UserSMS | null = await this.service.findOneById({
id: req.body["itemId"],
props: {
isRoot: true,
},
select: {
userId: true,
verificationCode: true,
},
});
if (!item) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Item not found"),
);
}
//check user id
if (
item.userId?.toString() !==
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid user ID"),
);
}
if (item.verificationCode !== req.body["code"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code"),
);
}
await this.service.updateOneById({
id: item.id!,
props: {
isRoot: true,
},
data: {
isVerified: true,
},
});
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
if (!req.body.code) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code"),
);
}
// Check if the code matches and verify the phone number.
const item: UserSMS | null = await this.service.findOneById({
id: req.body["itemId"],
props: {
isRoot: true,
},
select: {
userId: true,
verificationCode: true,
},
});
if (!item) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Item not found"),
);
}
//check user id
if (
item.userId?.toString() !==
(req as OneUptimeRequest)?.userAuthorization?.userId?.toString()
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid user ID"),
);
}
if (item.verificationCode !== req.body["code"]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid code"),
);
}
await this.service.updateOneById({
id: item.id!,
props: {
isRoot: true,
},
data: {
isVerified: true,
},
});
return Response.sendEmptySuccessResponse(req, res);
},
);
this.router.post(
`/user-sms/resend-verification-code`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
req = req as OneUptimeRequest;
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
req = req as OneUptimeRequest;
if (!req.body.itemId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
if (!req.body.itemId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid item ID"),
);
}
await this.service.resendVerificationCode(req.body.itemId);
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
await this.service.resendVerificationCode(req.body.itemId);
return Response.sendEmptySuccessResponse(req, res);
},
);
}

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