@oneuptime/common 12.0.13 → 12.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +14 -0
- package/Models/DatabaseModels/UserCall.ts +94 -3
- package/Models/DatabaseModels/UserEmail.ts +94 -3
- package/Models/DatabaseModels/UserIncomingCallNumber.ts +94 -3
- package/Models/DatabaseModels/UserPush.ts +37 -0
- package/Models/DatabaseModels/UserSMS.ts +94 -3
- package/Models/DatabaseModels/UserWhatsApp.ts +93 -2
- package/Server/API/BaseAPI.ts +79 -14
- package/Server/API/UserCallAPI.ts +53 -45
- package/Server/API/UserEmailAPI.ts +53 -44
- package/Server/API/UserIncomingCallNumberAPI.ts +48 -38
- package/Server/API/UserNotificationMethodAdminAPI.ts +481 -0
- package/Server/API/UserPushAPI.ts +126 -2
- package/Server/API/UserSmsAPI.ts +53 -44
- package/Server/API/UserWhatsAppAPI.ts +53 -50
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.ts +154 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.ts +45 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/VerificationCodeRateLimit.ts +586 -0
- package/Server/Services/AnalyticsDatabaseService.ts +169 -0
- package/Server/Services/DatabaseService.ts +101 -0
- package/Server/Services/LogAggregationService.ts +5 -0
- package/Server/Services/PushNotificationService.ts +127 -12
- package/Server/Services/TeamMemberService.ts +53 -13
- package/Server/Services/UserCallService.ts +55 -24
- package/Server/Services/UserEmailService.ts +83 -17
- package/Server/Services/UserIncomingCallNumberService.ts +49 -85
- package/Server/Services/UserNotificationMethodAdminService.ts +1527 -0
- package/Server/Services/UserNotificationRuleService.ts +26 -0
- package/Server/Services/UserPushService.ts +89 -0
- package/Server/Services/UserSmsService.ts +50 -17
- package/Server/Services/UserWhatsAppService.ts +48 -15
- package/Server/Types/Database/QueryHelper.ts +30 -0
- package/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.ts +59 -0
- package/Server/Utils/ChannelVerification.ts +510 -0
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +13 -16
- package/Server/Utils/Monitor/MonitorAlert.ts +24 -21
- package/Server/Utils/Monitor/MonitorIncident.ts +21 -18
- package/Server/Utils/Monitor/MonitorResourceContext.ts +107 -0
- package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +442 -0
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +3 -3
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +96 -28
- package/Server/Utils/TotpAuth.ts +137 -9
- package/Server/Utils/UserRegistrationToken.ts +182 -0
- package/Server/Utils/VerificationCode.ts +134 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +173 -94
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +1151 -0
- package/Tests/App/Dashboard/AdminUserOnCallPages.test.tsx +905 -0
- package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +338 -0
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +1 -1
- package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +28 -0
- package/Tests/Models/UserPushCriticalAlertColumn.test.ts +172 -0
- package/Tests/Server/API/BaseAPIGetListParallel.test.ts +519 -0
- package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +543 -0
- package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +108 -18
- package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +963 -0
- package/Tests/Server/API/UserPushCriticalAlertsApi.test.ts +114 -0
- package/Tests/Server/API/UserSmsApi.test.ts +107 -14
- package/Tests/Server/API/UserTotpAuthAPI.test.ts +427 -0
- package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +315 -0
- package/Tests/Server/Middleware/VerificationCodeRateLimit.test.ts +775 -0
- package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +10 -62
- package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +33 -13
- package/Tests/Server/Services/AnalyticsDatabaseSortKeyBoundary.test.ts +658 -0
- package/Tests/Server/Services/CriticalOnCallAlertDelivery.test.ts +458 -0
- package/Tests/Server/Services/CriticalPushAlertPayload.test.ts +301 -0
- package/Tests/Server/Services/DatabaseServiceAtomicIncrement.test.ts +143 -0
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1 -0
- package/Tests/Server/Services/LogAggregationScanMemory.test.ts +421 -0
- package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
- package/Tests/Server/Services/NotificationChannelCodeIssuance.test.ts +401 -0
- package/Tests/Server/Services/PushNotificationDeliveryOptions.test.ts +204 -0
- package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +21 -1
- package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +290 -0
- package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +948 -0
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1 -0
- package/Tests/Server/Services/UserPushCriticalAlertToggle.test.ts +255 -0
- package/Tests/Server/Services/UserTotpAuthEnrolment.test.ts +218 -0
- package/Tests/Server/TestingUtils/AuthenticatorApp.ts +248 -0
- package/Tests/Server/Types/Database/Permissions/NotificationChannelVerificationColumns.test.ts +135 -0
- package/Tests/Server/Types/Database/Permissions/OnCallScheduleRelationSelect.test.ts +504 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAnyOf.test.ts +91 -0
- package/Tests/Server/Types/Workflow/Components/JsonToText.test.ts +384 -0
- package/Tests/Server/Types/Workflow/Components/MergeJson.test.ts +403 -0
- package/Tests/Server/Utils/Browser.test.ts +162 -0
- package/Tests/Server/Utils/ChannelVerification.test.ts +689 -0
- package/Tests/Server/Utils/Express.test.ts +371 -0
- package/Tests/Server/Utils/Monitor/Criteria/CustomCodeMonitorCriteria.test.ts +707 -0
- package/Tests/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.test.ts +698 -0
- package/Tests/Server/Utils/Monitor/Criteria/IncomingEmailBodyCriteria.test.ts +243 -0
- package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestBodyCriteria.test.ts +278 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +160 -20
- package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +13 -8
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +162 -18
- package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +349 -0
- package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +690 -0
- package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +7 -2
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +92 -18
- package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +267 -0
- package/Tests/Server/Utils/Telemetry/OneuptimeLabel.test.ts +439 -0
- package/Tests/Server/Utils/TotpAuth.test.ts +719 -0
- package/Tests/Server/Utils/UserRegistrationToken.test.ts +322 -0
- package/Tests/Server/Utils/VerificationCode.test.ts +296 -0
- package/Tests/Types/AutoRemediation/AutoRemediationSuggestionStatus.test.ts +192 -0
- package/Tests/Types/Call/CallRequest.test.ts +189 -0
- package/Tests/Types/Dashboard/DashboardSize.test.ts +226 -0
- package/Tests/Types/Incident/IncidentSlaStatus.test.ts +191 -0
- package/Tests/Types/Metrics/MetricDashboardMetricType.test.ts +278 -0
- package/Tests/Types/Metrics/MetricPipelineRuleType.test.ts +517 -0
- package/Tests/Types/Monitor/IncomingMonitorDefaultCriteria.test.ts +470 -0
- package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +6 -4
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +262 -5
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
- package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +905 -0
- package/Tests/Types/Service/ServiceLanguage.test.ts +255 -0
- package/Tests/Types/TextRandomGeneration.test.ts +211 -0
- package/Tests/UI/Components/Button.test.tsx +175 -1
- package/Tests/UI/Components/CodeBlockLanguages.test.tsx +236 -0
- package/Tests/UI/Components/CommandPalette/CommandPalette.test.tsx +568 -0
- package/Tests/UI/Components/CommandPalette/CommandPaletteKeyboard.test.tsx +239 -0
- package/Tests/UI/Components/CommandPalette/CommandPaletteProviders.test.tsx +400 -0
- package/Tests/UI/Components/CommandPalette/PaletteFilter.test.ts +234 -0
- package/Tests/UI/Components/CommandPalette/RecentCommands.test.ts +97 -0
- package/Tests/UI/Components/CountModelSideMenuItemGuard.test.tsx +198 -0
- package/Tests/UI/Components/FeedItemSafeMode.test.tsx +1 -1
- package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +267 -0
- package/Tests/UI/Components/List.test.tsx +9 -2
- package/Tests/UI/Components/ListLoadingStates.test.tsx +173 -0
- package/Tests/UI/Components/MarkdownLazy.test.tsx +113 -0
- package/Tests/UI/Components/MarkdownMermaidRetry.test.tsx +110 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +8 -0
- package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +692 -0
- package/Tests/UI/Components/MoreMenuMotion.test.tsx +118 -0
- package/Tests/UI/Components/NavBarCommandKGating.test.tsx +178 -0
- package/Tests/UI/Components/OrderedStatesList.test.tsx +19 -2
- package/Tests/UI/Components/SideOverMotion.test.tsx +109 -0
- package/Tests/UI/Components/Skeleton.test.tsx +71 -0
- package/Tests/UI/Components/TableLoadingStates.test.tsx +279 -0
- package/Tests/UI/Components/Tabs.test.tsx +24 -0
- package/Tests/UI/Components/Toast.test.tsx +130 -7
- package/Tests/UI/Components/ToastStacking.test.tsx +198 -0
- package/Tests/UI/Utils/Dropdown.test.ts +423 -0
- package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +122 -0
- package/Tests/UI/Utils/ModelListCache.test.ts +338 -0
- package/Tests/UI/Utils/ProjectListCacheInvalidation.test.ts +225 -0
- package/Tests/Utils/API.test.ts +16 -2
- package/Tests/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.test.ts +347 -0
- package/Tests/Utils/Dashboard/Components/DashboardListSharedArgs.test.ts +259 -0
- package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +466 -0
- package/Tests/Utils/Rum/UrlScrubberSegments.test.ts +362 -0
- package/Types/Email/EmailTemplateType.ts +1 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +41 -20
- package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +131 -8
- package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +48 -8
- package/Types/Monitor/ServiceAlertTemplates.ts +1352 -0
- package/Types/Permission.ts +35 -0
- package/Types/PushNotification/AndroidNotificationChannel.ts +26 -0
- package/Types/PushNotification/PushNotificationMessage.ts +15 -0
- package/Types/PushNotification/PushNotificationRequest.ts +8 -16
- package/Types/Service/ServiceLanguage.ts +144 -0
- package/Types/Text.ts +111 -19
- package/UI/Components/Button/Button.tsx +27 -14
- package/UI/Components/CodeBlock/CodeBlock.tsx +54 -7
- package/UI/Components/CodeBlock/LanguageRegistry.ts +103 -0
- package/UI/Components/CommandPalette/CommandPalette.tsx +790 -0
- package/UI/Components/CommandPalette/PaletteFilter.ts +196 -0
- package/UI/Components/CommandPalette/PaletteRow.tsx +173 -0
- package/UI/Components/CommandPalette/RecentCommands.ts +57 -0
- package/UI/Components/CommandPalette/Types.ts +49 -0
- package/UI/Components/CommandPalette/UseProviderSearch.ts +129 -0
- package/UI/Components/Dropdown/Dropdown.tsx +6 -1
- package/UI/Components/Feed/FeedItem.tsx +1 -1
- package/UI/Components/List/List.tsx +37 -5
- package/UI/Components/List/ListSkeleton.tsx +54 -0
- package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +6 -1
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +71 -26
- package/UI/Components/Modal/Modal.tsx +2 -2
- package/UI/Components/ModelTable/BaseModelTable.tsx +1 -1
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +11 -5
- package/UI/Components/MoreMenu/MoreMenu.tsx +39 -1
- package/UI/Components/Navbar/NavBar.tsx +14 -1
- package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +50 -5
- package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +40 -1
- package/UI/Components/SideOver/SideOver.tsx +37 -2
- package/UI/Components/Skeleton/Skeleton.tsx +53 -0
- package/UI/Components/Table/Table.tsx +74 -44
- package/UI/Components/Table/TableSkeletonRows.tsx +141 -0
- package/UI/Components/Tabs/Tab.tsx +1 -1
- package/UI/Components/Toast/Toast.tsx +118 -53
- package/UI/Components/Toast/ToastInit.tsx +82 -32
- package/UI/Styles/Theme.css +40 -0
- package/UI/Utils/ModelAPI/ModelAPI.ts +20 -10
- package/UI/Utils/ModelListCache.ts +188 -0
- package/UI/Utils/Project.ts +29 -0
- package/Utils/API.ts +6 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +14 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserCall.js +101 -3
- package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserEmail.js +101 -3
- package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js +101 -3
- package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserPush.js +38 -0
- package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSMS.js +101 -3
- package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js +100 -2
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +69 -13
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/UserCallAPI.js +42 -38
- package/build/dist/Server/API/UserCallAPI.js.map +1 -1
- package/build/dist/Server/API/UserEmailAPI.js +42 -38
- package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js +37 -32
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
- package/build/dist/Server/API/UserNotificationMethodAdminAPI.js +341 -0
- package/build/dist/Server/API/UserNotificationMethodAdminAPI.js.map +1 -0
- package/build/dist/Server/API/UserPushAPI.js +95 -2
- package/build/dist/Server/API/UserPushAPI.js.map +1 -1
- package/build/dist/Server/API/UserSmsAPI.js +42 -38
- package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
- package/build/dist/Server/API/UserWhatsAppAPI.js +42 -40
- package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js +89 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/VerificationCodeRateLimit.js +393 -0
- package/build/dist/Server/Middleware/VerificationCodeRateLimit.js.map +1 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +136 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +74 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +5 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/PushNotificationService.js +69 -30
- package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +43 -7
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/UserCallService.js +54 -22
- package/build/dist/Server/Services/UserCallService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +78 -17
- package/build/dist/Server/Services/UserEmailService.js.map +1 -1
- package/build/dist/Server/Services/UserIncomingCallNumberService.js +45 -74
- package/build/dist/Server/Services/UserIncomingCallNumberService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1117 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js +20 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserPushService.js +77 -0
- package/build/dist/Server/Services/UserPushService.js.map +1 -1
- package/build/dist/Server/Services/UserSmsService.js +50 -17
- package/build/dist/Server/Services/UserSmsService.js.map +1 -1
- package/build/dist/Server/Services/UserWhatsAppService.js +48 -15
- package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +27 -0
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js +46 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js.map +1 -1
- package/build/dist/Server/Utils/ChannelVerification.js +348 -0
- package/build/dist/Server/Utils/ChannelVerification.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +13 -12
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +20 -17
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +17 -14
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +95 -0
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +313 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +3 -3
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +67 -26
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
- package/build/dist/Server/Utils/TotpAuth.js +127 -8
- package/build/dist/Server/Utils/TotpAuth.js.map +1 -1
- package/build/dist/Server/Utils/UserRegistrationToken.js +170 -0
- package/build/dist/Server/Utils/UserRegistrationToken.js.map +1 -0
- package/build/dist/Server/Utils/VerificationCode.js +120 -0
- package/build/dist/Server/Utils/VerificationCode.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +42 -21
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +77 -5
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +18 -6
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
- package/build/dist/Types/Monitor/ServiceAlertTemplates.js +1028 -0
- package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -0
- package/build/dist/Types/Permission.js +33 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/PushNotification/AndroidNotificationChannel.js +27 -0
- package/build/dist/Types/PushNotification/AndroidNotificationChannel.js.map +1 -0
- package/build/dist/Types/Service/ServiceLanguage.js +96 -0
- package/build/dist/Types/Service/ServiceLanguage.js.map +1 -0
- package/build/dist/Types/Text.js +82 -17
- package/build/dist/Types/Text.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +22 -14
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/CodeBlock/CodeBlock.js +35 -3
- package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
- package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js +57 -0
- package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js +439 -0
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/PaletteFilter.js +135 -0
- package/build/dist/UI/Components/CommandPalette/PaletteFilter.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/PaletteRow.js +83 -0
- package/build/dist/UI/Components/CommandPalette/PaletteRow.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/RecentCommands.js +47 -0
- package/build/dist/UI/Components/CommandPalette/RecentCommands.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/Types.js +2 -0
- package/build/dist/UI/Components/CommandPalette/Types.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js +88 -0
- package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js.map +1 -0
- package/build/dist/UI/Components/Dropdown/Dropdown.js +6 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/Feed/FeedItem.js +1 -1
- package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
- package/build/dist/UI/Components/List/List.js +19 -5
- package/build/dist/UI/Components/List/List.js.map +1 -1
- package/build/dist/UI/Components/List/ListSkeleton.js +21 -0
- package/build/dist/UI/Components/List/ListSkeleton.js.map +1 -0
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +52 -25
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +2 -2
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +9 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js +32 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +5 -1
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +29 -5
- package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +23 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideOver/SideOver.js +23 -2
- package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
- package/build/dist/UI/Components/Skeleton/Skeleton.js +23 -0
- package/build/dist/UI/Components/Skeleton/Skeleton.js.map +1 -0
- package/build/dist/UI/Components/Table/Table.js +35 -24
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableSkeletonRows.js +52 -0
- package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -0
- package/build/dist/UI/Components/Tabs/Tab.js +1 -1
- package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
- package/build/dist/UI/Components/Toast/Toast.js +77 -23
- package/build/dist/UI/Components/Toast/Toast.js.map +1 -1
- package/build/dist/UI/Components/Toast/ToastInit.js +37 -15
- package/build/dist/UI/Components/Toast/ToastInit.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +18 -9
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/ModelListCache.js +126 -0
- package/build/dist/UI/Utils/ModelListCache.js.map +1 -0
- package/build/dist/UI/Utils/Project.js +24 -1
- package/build/dist/UI/Utils/Project.js.map +1 -1
- package/build/dist/Utils/API.js +6 -1
- package/build/dist/Utils/API.js.map +1 -1
- package/jest.config.json +1 -1
- package/package.json +2 -3
- package/Server/Utils/Monitor/MonitorClusterContext.ts +0 -182
- package/Tests/__mocks__/otpauth.js +0 -30
- package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js +0 -141
- package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js.map +0 -1
|
@@ -0,0 +1,1527 @@
|
|
|
1
|
+
import DatabaseConfig from "../DatabaseConfig";
|
|
2
|
+
import BaseService from "./BaseService";
|
|
3
|
+
import MailService from "./MailService";
|
|
4
|
+
import OnCallReadinessService, {
|
|
5
|
+
MaskedIdentifierKind,
|
|
6
|
+
ReadinessMethodType,
|
|
7
|
+
maskIdentifier,
|
|
8
|
+
} from "./OnCallReadinessService";
|
|
9
|
+
import ProjectService from "./ProjectService";
|
|
10
|
+
import TeamMemberService from "./TeamMemberService";
|
|
11
|
+
import UserCallService from "./UserCallService";
|
|
12
|
+
import UserEmailService from "./UserEmailService";
|
|
13
|
+
import UserNotificationRuleService, {
|
|
14
|
+
NotificationDeletionImpact,
|
|
15
|
+
} from "./UserNotificationRuleService";
|
|
16
|
+
import UserPushService from "./UserPushService";
|
|
17
|
+
import UserService from "./UserService";
|
|
18
|
+
import UserSmsService from "./UserSmsService";
|
|
19
|
+
import UserTelegramService from "./UserTelegramService";
|
|
20
|
+
import UserWebhookService from "./UserWebhookService";
|
|
21
|
+
import UserWhatsAppService from "./UserWhatsAppService";
|
|
22
|
+
import type AuditLogServiceType from "./AuditLogService";
|
|
23
|
+
import logger, { LogAttributes } from "../Utils/Logger";
|
|
24
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
25
|
+
import URL from "../../Types/API/URL";
|
|
26
|
+
import AuditLogAction from "../../Types/AuditLog/AuditLogAction";
|
|
27
|
+
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
28
|
+
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
29
|
+
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
30
|
+
import Dictionary from "../../Types/Dictionary";
|
|
31
|
+
import Email from "../../Types/Email";
|
|
32
|
+
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
|
|
33
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
34
|
+
import ObjectID from "../../Types/ObjectID";
|
|
35
|
+
import Phone from "../../Types/Phone";
|
|
36
|
+
import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
37
|
+
import Project from "../../Models/DatabaseModels/Project";
|
|
38
|
+
import TeamMember from "../../Models/DatabaseModels/TeamMember";
|
|
39
|
+
import User from "../../Models/DatabaseModels/User";
|
|
40
|
+
import UserCall from "../../Models/DatabaseModels/UserCall";
|
|
41
|
+
import UserEmail from "../../Models/DatabaseModels/UserEmail";
|
|
42
|
+
import UserPush from "../../Models/DatabaseModels/UserPush";
|
|
43
|
+
import UserSMS from "../../Models/DatabaseModels/UserSMS";
|
|
44
|
+
import UserTelegram from "../../Models/DatabaseModels/UserTelegram";
|
|
45
|
+
import UserWebhook from "../../Models/DatabaseModels/UserWebhook";
|
|
46
|
+
import UserWhatsApp from "../../Models/DatabaseModels/UserWhatsApp";
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* ADDING A NOTIFICATION METHOD TO SOMEBODY ELSE'S ACCOUNT.
|
|
50
|
+
*
|
|
51
|
+
* The readiness work that came before this one could tell a project owner that
|
|
52
|
+
* a responder was unreachable, and the admin rules page let them repair that
|
|
53
|
+
* responder's RULES. Neither could do anything about the case that produces
|
|
54
|
+
* most unreachable responders in practice: somebody who has no notification
|
|
55
|
+
* method at all. A rule is a pointer; with nothing to point at, an owner
|
|
56
|
+
* looking at a brand-new engineer's page could only send them a reminder and
|
|
57
|
+
* hope. This service is what closes that last gap.
|
|
58
|
+
*
|
|
59
|
+
* It is deliberately NOT "widen the seven method models so admins can write
|
|
60
|
+
* them". That was tried, and the essay at the top of UserEmail.ts is the record
|
|
61
|
+
* of how it failed: those models are scoped to their owner by
|
|
62
|
+
* TenantPermission.isAccessGrantedOnlyByCurrentUser, which holds exactly while
|
|
63
|
+
* their table access control lists Permission.CurrentUser and nothing else, and
|
|
64
|
+
* every attempt to keep the raw columns fenced off once that stamp stopped
|
|
65
|
+
* being applied was walked past by something — nested relation selects, `query`
|
|
66
|
+
* filters, sort columns appended after the guard had run. The models are still
|
|
67
|
+
* CurrentUser-only. Nothing in this file changes that, and an administrator's
|
|
68
|
+
* every direct read or write of UserEmail, UserSMS, UserCall, UserWhatsApp,
|
|
69
|
+
* UserTelegram, UserPush and UserWebhook is still refused.
|
|
70
|
+
*
|
|
71
|
+
* What this file does instead is a NARROW, SERVER-SIDE, AUDITED capability
|
|
72
|
+
* that runs as root on the admin's behalf and never hands them the row:
|
|
73
|
+
*
|
|
74
|
+
* - LIST returns masked identifiers only, through the same maskIdentifier
|
|
75
|
+
* the readiness payload uses. There is no unmask-for-anybody branch, and
|
|
76
|
+
* the raw column never enters a response body.
|
|
77
|
+
*
|
|
78
|
+
* - ADD writes the row with the target user's id and isVerified left at its
|
|
79
|
+
* default of false, which makes the method service send a verification code
|
|
80
|
+
* TO THE DEVICE. That is the whole safety property, and it is worth being
|
|
81
|
+
* precise about: an administrator can type a phone number in, but they
|
|
82
|
+
* cannot make it live. /user-email/verify, /user-sms/verify and their
|
|
83
|
+
* siblings all compare the row's userId against the SIGNED-IN caller and
|
|
84
|
+
* refuse anybody else (UserEmailAPI.ts and friends), so the code that was
|
|
85
|
+
* just sent is only usable by the person holding the device. An admin who
|
|
86
|
+
* types their own number in has created a row that will never be verified,
|
|
87
|
+
* will never be selected by the fallback, and shows up on the owner's own
|
|
88
|
+
* notification methods page as an unverified method they did not add — on
|
|
89
|
+
* top of the mail this service sends them.
|
|
90
|
+
*
|
|
91
|
+
* - REMOVE is allowed on all seven channels, because offboarding a stale
|
|
92
|
+
* device is a real administrative job and the owner may well be gone. It is
|
|
93
|
+
* the destructive direction, so it is the one with a preview attached:
|
|
94
|
+
* UserNotificationRuleService.getNotificationMethodDeletionImpact says how
|
|
95
|
+
* many rules would go with it and whether the person is reachable
|
|
96
|
+
* afterwards. That method was written for this caller and had none until
|
|
97
|
+
* now.
|
|
98
|
+
*
|
|
99
|
+
* - ADD is allowed on FOUR channels — Email, SMS, Call, WhatsApp — and the
|
|
100
|
+
* omissions are not oversights. Push is a device token minted by a browser
|
|
101
|
+
* or a phone at registration time; there is nothing an admin could type.
|
|
102
|
+
* Telegram needs the account holder to message the bot before a chat id
|
|
103
|
+
* exists. Webhook is the one that is deliberately refused rather than
|
|
104
|
+
* merely impractical: UserWebhook has no isVerified column at all, so an
|
|
105
|
+
* admin-created webhook would be live the moment it was written, which is
|
|
106
|
+
* exactly the silent-redirect this whole design is built to prevent.
|
|
107
|
+
*
|
|
108
|
+
* EVERY WRITE TELLS THE OWNER. Same reasoning as the rules path: an admin
|
|
109
|
+
* adding a colleague's work phone and an attacker adding their own are the same
|
|
110
|
+
* request from the server's point of view, and the only party who can always
|
|
111
|
+
* tell them apart is the person whose account it is.
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A method as an administrator is allowed to see it: enough to recognise, never
|
|
116
|
+
* enough to reach.
|
|
117
|
+
*
|
|
118
|
+
* `methodId` is a foreign key, not a secret — it is already stored in plain
|
|
119
|
+
* sight on every rule the owner has created, and it is what lets an admin point
|
|
120
|
+
* a rule at a method (or remove one) without reading the row behind it.
|
|
121
|
+
*
|
|
122
|
+
* Nothing else belongs on this interface. Every field added here is a field
|
|
123
|
+
* that ships to every administrator of the project.
|
|
124
|
+
*/
|
|
125
|
+
export interface AdminNotificationMethodView {
|
|
126
|
+
methodId: ObjectID;
|
|
127
|
+
methodType: ReadinessMethodType;
|
|
128
|
+
/** Already masked by maskIdentifier. Never the raw value, for any caller. */
|
|
129
|
+
maskedIdentifier: string;
|
|
130
|
+
isVerified: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Whether an administrator could have created this kind of method at all.
|
|
133
|
+
* Drives the page's copy, so a responder with only a webhook is told why the
|
|
134
|
+
* "Add" control does not offer one rather than being left to guess.
|
|
135
|
+
*/
|
|
136
|
+
isAdminAddable: boolean;
|
|
137
|
+
createdAt?: Date | undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The four channels an administrator may CREATE. See the header for why the
|
|
142
|
+
* other three are absent; this enum is the single place that decides, and both
|
|
143
|
+
* the API's validation and the service's own dispatch read it.
|
|
144
|
+
*/
|
|
145
|
+
export enum AdminAddableChannel {
|
|
146
|
+
Email = "Email",
|
|
147
|
+
SMS = "SMS",
|
|
148
|
+
Call = "Call",
|
|
149
|
+
WhatsApp = "WhatsApp",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A compact deletion preview, for the confirmation an admin sees before
|
|
154
|
+
* removing somebody else's method.
|
|
155
|
+
*
|
|
156
|
+
* It is a projection of NotificationDeletionImpact rather than that whole
|
|
157
|
+
* structure: the full one carries per-cell severity names, and an admin
|
|
158
|
+
* confirming a removal needs the counts and the one sentence about whether this
|
|
159
|
+
* is the deletion that makes a person unreachable.
|
|
160
|
+
*/
|
|
161
|
+
export interface AdminNotificationMethodDeletionPreview {
|
|
162
|
+
rulesDeletedCount: number;
|
|
163
|
+
coverageLostCount: number;
|
|
164
|
+
verifiedMethodCountAfterDeletion: number;
|
|
165
|
+
reachability: string;
|
|
166
|
+
isFallbackEnabled: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* TRUE means the read behind these numbers hit its ceiling, so they are
|
|
169
|
+
* floors rather than totals. Carried through rather than swallowed: a
|
|
170
|
+
* confirmation that undercounts silently is worse than one that says it is
|
|
171
|
+
* unsure.
|
|
172
|
+
*/
|
|
173
|
+
isTruncated: boolean;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* One channel, and everything this service needs to do to it. Built per call
|
|
178
|
+
* rather than hoisted to module scope for the same reason
|
|
179
|
+
* UserNotificationRuleAdminService builds its descriptors per call: the seven
|
|
180
|
+
* method services import UserNotificationRuleService, which imports this
|
|
181
|
+
* file's siblings, and a module-level constant would capture `default` while
|
|
182
|
+
* one of those modules was still mid-evaluation and freeze `undefined` into the
|
|
183
|
+
* table.
|
|
184
|
+
*/
|
|
185
|
+
interface ChannelDescriptor {
|
|
186
|
+
methodType: ReadinessMethodType;
|
|
187
|
+
isAdminAddable: boolean;
|
|
188
|
+
/*
|
|
189
|
+
* The model class behind this channel, used for ONE thing: naming the table
|
|
190
|
+
* an audit entry is about. Never instantiated with a real row's values — see
|
|
191
|
+
* writeAuditLog for why the entry is built from a blank instance.
|
|
192
|
+
*/
|
|
193
|
+
modelType: { new (): BaseModel };
|
|
194
|
+
/** Reads every one of this user's rows on this channel, already masked. */
|
|
195
|
+
list: (data: {
|
|
196
|
+
projectId: ObjectID;
|
|
197
|
+
userId: ObjectID;
|
|
198
|
+
}) => Promise<Array<AdminNotificationMethodView>>;
|
|
199
|
+
/** Confirms one row exists AND belongs to this user in this project. */
|
|
200
|
+
findOwnedRow: (data: {
|
|
201
|
+
methodId: ObjectID;
|
|
202
|
+
projectId: ObjectID;
|
|
203
|
+
userId: ObjectID;
|
|
204
|
+
}) => Promise<AdminNotificationMethodView | null>;
|
|
205
|
+
deleteRow: (data: { methodId: ObjectID }) => Promise<void>;
|
|
206
|
+
/** Absent on the three channels an administrator may not create. */
|
|
207
|
+
create?:
|
|
208
|
+
| ((data: {
|
|
209
|
+
projectId: ObjectID;
|
|
210
|
+
userId: ObjectID;
|
|
211
|
+
value: string;
|
|
212
|
+
}) => Promise<AdminNotificationMethodView>)
|
|
213
|
+
| undefined;
|
|
214
|
+
/** Absent on Webhook, which has no verification concept at all. */
|
|
215
|
+
resendVerificationCode?: ((methodId: ObjectID) => Promise<void>) | undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export class UserNotificationMethodAdminService extends BaseService {
|
|
219
|
+
/*
|
|
220
|
+
* The seven channels. Every one of them is listable and deletable; four are
|
|
221
|
+
* creatable. A channel missing from this table is a channel this service
|
|
222
|
+
* cannot see at all, which is why the list is exhaustive rather than
|
|
223
|
+
* restricted to the interesting ones — an admin looking at a responder whose
|
|
224
|
+
* only method is a webhook must be told the webhook is there, or they will
|
|
225
|
+
* conclude the person has nothing and go and add a phone number that
|
|
226
|
+
* duplicates a working channel.
|
|
227
|
+
*/
|
|
228
|
+
private getChannelDescriptors(): Array<ChannelDescriptor> {
|
|
229
|
+
return [
|
|
230
|
+
{
|
|
231
|
+
methodType: ReadinessMethodType.Email,
|
|
232
|
+
isAdminAddable: true,
|
|
233
|
+
modelType: UserEmail,
|
|
234
|
+
list: async (data: {
|
|
235
|
+
projectId: ObjectID;
|
|
236
|
+
userId: ObjectID;
|
|
237
|
+
}): Promise<Array<AdminNotificationMethodView>> => {
|
|
238
|
+
const rows: Array<UserEmail> = await UserEmailService.findBy({
|
|
239
|
+
query: { projectId: data.projectId, userId: data.userId },
|
|
240
|
+
select: {
|
|
241
|
+
_id: true,
|
|
242
|
+
email: true,
|
|
243
|
+
isVerified: true,
|
|
244
|
+
createdAt: true,
|
|
245
|
+
},
|
|
246
|
+
sort: { createdAt: SortOrder.Ascending },
|
|
247
|
+
skip: 0,
|
|
248
|
+
limit: LIMIT_PER_PROJECT,
|
|
249
|
+
props: { isRoot: true },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return rows.map((row: UserEmail): AdminNotificationMethodView => {
|
|
253
|
+
return this.toView({
|
|
254
|
+
row: row,
|
|
255
|
+
methodType: ReadinessMethodType.Email,
|
|
256
|
+
isAdminAddable: true,
|
|
257
|
+
identifier: row.email?.toString(),
|
|
258
|
+
kind: MaskedIdentifierKind.Email,
|
|
259
|
+
isVerified: row.isVerified,
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
findOwnedRow: async (data: {
|
|
264
|
+
methodId: ObjectID;
|
|
265
|
+
projectId: ObjectID;
|
|
266
|
+
userId: ObjectID;
|
|
267
|
+
}): Promise<AdminNotificationMethodView | null> => {
|
|
268
|
+
const row: UserEmail | null = await UserEmailService.findOneBy({
|
|
269
|
+
query: {
|
|
270
|
+
_id: data.methodId,
|
|
271
|
+
projectId: data.projectId,
|
|
272
|
+
userId: data.userId,
|
|
273
|
+
},
|
|
274
|
+
select: { _id: true, email: true, isVerified: true },
|
|
275
|
+
props: { isRoot: true },
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return row
|
|
279
|
+
? this.toView({
|
|
280
|
+
row: row,
|
|
281
|
+
methodType: ReadinessMethodType.Email,
|
|
282
|
+
isAdminAddable: true,
|
|
283
|
+
identifier: row.email?.toString(),
|
|
284
|
+
kind: MaskedIdentifierKind.Email,
|
|
285
|
+
isVerified: row.isVerified,
|
|
286
|
+
})
|
|
287
|
+
: null;
|
|
288
|
+
},
|
|
289
|
+
deleteRow: async (data: { methodId: ObjectID }): Promise<void> => {
|
|
290
|
+
await UserEmailService.deleteOneById({
|
|
291
|
+
id: data.methodId,
|
|
292
|
+
props: { isRoot: true },
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
create: async (data: {
|
|
296
|
+
projectId: ObjectID;
|
|
297
|
+
userId: ObjectID;
|
|
298
|
+
value: string;
|
|
299
|
+
}): Promise<AdminNotificationMethodView> => {
|
|
300
|
+
const model: UserEmail = new UserEmail();
|
|
301
|
+
model.projectId = data.projectId;
|
|
302
|
+
model.userId = data.userId;
|
|
303
|
+
model.email = new Email(data.value);
|
|
304
|
+
|
|
305
|
+
const created: UserEmail = await UserEmailService.create({
|
|
306
|
+
data: model,
|
|
307
|
+
props: { isRoot: true },
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return this.toView({
|
|
311
|
+
row: created,
|
|
312
|
+
methodType: ReadinessMethodType.Email,
|
|
313
|
+
isAdminAddable: true,
|
|
314
|
+
identifier: created.email?.toString(),
|
|
315
|
+
kind: MaskedIdentifierKind.Email,
|
|
316
|
+
isVerified: created.isVerified,
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
resendVerificationCode: async (methodId: ObjectID): Promise<void> => {
|
|
320
|
+
await UserEmailService.resendVerificationCode(methodId);
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
methodType: ReadinessMethodType.SMS,
|
|
325
|
+
isAdminAddable: true,
|
|
326
|
+
modelType: UserSMS,
|
|
327
|
+
list: async (data: {
|
|
328
|
+
projectId: ObjectID;
|
|
329
|
+
userId: ObjectID;
|
|
330
|
+
}): Promise<Array<AdminNotificationMethodView>> => {
|
|
331
|
+
const rows: Array<UserSMS> = await UserSmsService.findBy({
|
|
332
|
+
query: { projectId: data.projectId, userId: data.userId },
|
|
333
|
+
select: {
|
|
334
|
+
_id: true,
|
|
335
|
+
phone: true,
|
|
336
|
+
isVerified: true,
|
|
337
|
+
createdAt: true,
|
|
338
|
+
},
|
|
339
|
+
sort: { createdAt: SortOrder.Ascending },
|
|
340
|
+
skip: 0,
|
|
341
|
+
limit: LIMIT_PER_PROJECT,
|
|
342
|
+
props: { isRoot: true },
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return rows.map((row: UserSMS): AdminNotificationMethodView => {
|
|
346
|
+
return this.toView({
|
|
347
|
+
row: row,
|
|
348
|
+
methodType: ReadinessMethodType.SMS,
|
|
349
|
+
isAdminAddable: true,
|
|
350
|
+
identifier: row.phone?.toString(),
|
|
351
|
+
kind: MaskedIdentifierKind.Phone,
|
|
352
|
+
isVerified: row.isVerified,
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
},
|
|
356
|
+
findOwnedRow: async (data: {
|
|
357
|
+
methodId: ObjectID;
|
|
358
|
+
projectId: ObjectID;
|
|
359
|
+
userId: ObjectID;
|
|
360
|
+
}): Promise<AdminNotificationMethodView | null> => {
|
|
361
|
+
const row: UserSMS | null = await UserSmsService.findOneBy({
|
|
362
|
+
query: {
|
|
363
|
+
_id: data.methodId,
|
|
364
|
+
projectId: data.projectId,
|
|
365
|
+
userId: data.userId,
|
|
366
|
+
},
|
|
367
|
+
select: { _id: true, phone: true, isVerified: true },
|
|
368
|
+
props: { isRoot: true },
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return row
|
|
372
|
+
? this.toView({
|
|
373
|
+
row: row,
|
|
374
|
+
methodType: ReadinessMethodType.SMS,
|
|
375
|
+
isAdminAddable: true,
|
|
376
|
+
identifier: row.phone?.toString(),
|
|
377
|
+
kind: MaskedIdentifierKind.Phone,
|
|
378
|
+
isVerified: row.isVerified,
|
|
379
|
+
})
|
|
380
|
+
: null;
|
|
381
|
+
},
|
|
382
|
+
deleteRow: async (data: { methodId: ObjectID }): Promise<void> => {
|
|
383
|
+
await UserSmsService.deleteOneById({
|
|
384
|
+
id: data.methodId,
|
|
385
|
+
props: { isRoot: true },
|
|
386
|
+
});
|
|
387
|
+
},
|
|
388
|
+
create: async (data: {
|
|
389
|
+
projectId: ObjectID;
|
|
390
|
+
userId: ObjectID;
|
|
391
|
+
value: string;
|
|
392
|
+
}): Promise<AdminNotificationMethodView> => {
|
|
393
|
+
const model: UserSMS = new UserSMS();
|
|
394
|
+
model.projectId = data.projectId;
|
|
395
|
+
model.userId = data.userId;
|
|
396
|
+
model.phone = new Phone(data.value);
|
|
397
|
+
|
|
398
|
+
const created: UserSMS = await UserSmsService.create({
|
|
399
|
+
data: model,
|
|
400
|
+
props: { isRoot: true },
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
return this.toView({
|
|
404
|
+
row: created,
|
|
405
|
+
methodType: ReadinessMethodType.SMS,
|
|
406
|
+
isAdminAddable: true,
|
|
407
|
+
identifier: created.phone?.toString(),
|
|
408
|
+
kind: MaskedIdentifierKind.Phone,
|
|
409
|
+
isVerified: created.isVerified,
|
|
410
|
+
});
|
|
411
|
+
},
|
|
412
|
+
resendVerificationCode: async (methodId: ObjectID): Promise<void> => {
|
|
413
|
+
await UserSmsService.resendVerificationCode(methodId);
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
methodType: ReadinessMethodType.Call,
|
|
418
|
+
isAdminAddable: true,
|
|
419
|
+
modelType: UserCall,
|
|
420
|
+
list: async (data: {
|
|
421
|
+
projectId: ObjectID;
|
|
422
|
+
userId: ObjectID;
|
|
423
|
+
}): Promise<Array<AdminNotificationMethodView>> => {
|
|
424
|
+
const rows: Array<UserCall> = await UserCallService.findBy({
|
|
425
|
+
query: { projectId: data.projectId, userId: data.userId },
|
|
426
|
+
select: {
|
|
427
|
+
_id: true,
|
|
428
|
+
phone: true,
|
|
429
|
+
isVerified: true,
|
|
430
|
+
createdAt: true,
|
|
431
|
+
},
|
|
432
|
+
sort: { createdAt: SortOrder.Ascending },
|
|
433
|
+
skip: 0,
|
|
434
|
+
limit: LIMIT_PER_PROJECT,
|
|
435
|
+
props: { isRoot: true },
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return rows.map((row: UserCall): AdminNotificationMethodView => {
|
|
439
|
+
return this.toView({
|
|
440
|
+
row: row,
|
|
441
|
+
methodType: ReadinessMethodType.Call,
|
|
442
|
+
isAdminAddable: true,
|
|
443
|
+
identifier: row.phone?.toString(),
|
|
444
|
+
kind: MaskedIdentifierKind.Phone,
|
|
445
|
+
isVerified: row.isVerified,
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
},
|
|
449
|
+
findOwnedRow: async (data: {
|
|
450
|
+
methodId: ObjectID;
|
|
451
|
+
projectId: ObjectID;
|
|
452
|
+
userId: ObjectID;
|
|
453
|
+
}): Promise<AdminNotificationMethodView | null> => {
|
|
454
|
+
const row: UserCall | null = await UserCallService.findOneBy({
|
|
455
|
+
query: {
|
|
456
|
+
_id: data.methodId,
|
|
457
|
+
projectId: data.projectId,
|
|
458
|
+
userId: data.userId,
|
|
459
|
+
},
|
|
460
|
+
select: { _id: true, phone: true, isVerified: true },
|
|
461
|
+
props: { isRoot: true },
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return row
|
|
465
|
+
? this.toView({
|
|
466
|
+
row: row,
|
|
467
|
+
methodType: ReadinessMethodType.Call,
|
|
468
|
+
isAdminAddable: true,
|
|
469
|
+
identifier: row.phone?.toString(),
|
|
470
|
+
kind: MaskedIdentifierKind.Phone,
|
|
471
|
+
isVerified: row.isVerified,
|
|
472
|
+
})
|
|
473
|
+
: null;
|
|
474
|
+
},
|
|
475
|
+
deleteRow: async (data: { methodId: ObjectID }): Promise<void> => {
|
|
476
|
+
await UserCallService.deleteOneById({
|
|
477
|
+
id: data.methodId,
|
|
478
|
+
props: { isRoot: true },
|
|
479
|
+
});
|
|
480
|
+
},
|
|
481
|
+
create: async (data: {
|
|
482
|
+
projectId: ObjectID;
|
|
483
|
+
userId: ObjectID;
|
|
484
|
+
value: string;
|
|
485
|
+
}): Promise<AdminNotificationMethodView> => {
|
|
486
|
+
const model: UserCall = new UserCall();
|
|
487
|
+
model.projectId = data.projectId;
|
|
488
|
+
model.userId = data.userId;
|
|
489
|
+
model.phone = new Phone(data.value);
|
|
490
|
+
|
|
491
|
+
const created: UserCall = await UserCallService.create({
|
|
492
|
+
data: model,
|
|
493
|
+
props: { isRoot: true },
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
return this.toView({
|
|
497
|
+
row: created,
|
|
498
|
+
methodType: ReadinessMethodType.Call,
|
|
499
|
+
isAdminAddable: true,
|
|
500
|
+
identifier: created.phone?.toString(),
|
|
501
|
+
kind: MaskedIdentifierKind.Phone,
|
|
502
|
+
isVerified: created.isVerified,
|
|
503
|
+
});
|
|
504
|
+
},
|
|
505
|
+
resendVerificationCode: async (methodId: ObjectID): Promise<void> => {
|
|
506
|
+
await UserCallService.resendVerificationCode(methodId);
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
methodType: ReadinessMethodType.WhatsApp,
|
|
511
|
+
isAdminAddable: true,
|
|
512
|
+
modelType: UserWhatsApp,
|
|
513
|
+
list: async (data: {
|
|
514
|
+
projectId: ObjectID;
|
|
515
|
+
userId: ObjectID;
|
|
516
|
+
}): Promise<Array<AdminNotificationMethodView>> => {
|
|
517
|
+
const rows: Array<UserWhatsApp> = await UserWhatsAppService.findBy({
|
|
518
|
+
query: { projectId: data.projectId, userId: data.userId },
|
|
519
|
+
select: {
|
|
520
|
+
_id: true,
|
|
521
|
+
phone: true,
|
|
522
|
+
isVerified: true,
|
|
523
|
+
createdAt: true,
|
|
524
|
+
},
|
|
525
|
+
sort: { createdAt: SortOrder.Ascending },
|
|
526
|
+
skip: 0,
|
|
527
|
+
limit: LIMIT_PER_PROJECT,
|
|
528
|
+
props: { isRoot: true },
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
return rows.map((row: UserWhatsApp): AdminNotificationMethodView => {
|
|
532
|
+
return this.toView({
|
|
533
|
+
row: row,
|
|
534
|
+
methodType: ReadinessMethodType.WhatsApp,
|
|
535
|
+
isAdminAddable: true,
|
|
536
|
+
identifier: row.phone?.toString(),
|
|
537
|
+
kind: MaskedIdentifierKind.Phone,
|
|
538
|
+
isVerified: row.isVerified,
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
},
|
|
542
|
+
findOwnedRow: async (data: {
|
|
543
|
+
methodId: ObjectID;
|
|
544
|
+
projectId: ObjectID;
|
|
545
|
+
userId: ObjectID;
|
|
546
|
+
}): Promise<AdminNotificationMethodView | null> => {
|
|
547
|
+
const row: UserWhatsApp | null = await UserWhatsAppService.findOneBy({
|
|
548
|
+
query: {
|
|
549
|
+
_id: data.methodId,
|
|
550
|
+
projectId: data.projectId,
|
|
551
|
+
userId: data.userId,
|
|
552
|
+
},
|
|
553
|
+
select: { _id: true, phone: true, isVerified: true },
|
|
554
|
+
props: { isRoot: true },
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
return row
|
|
558
|
+
? this.toView({
|
|
559
|
+
row: row,
|
|
560
|
+
methodType: ReadinessMethodType.WhatsApp,
|
|
561
|
+
isAdminAddable: true,
|
|
562
|
+
identifier: row.phone?.toString(),
|
|
563
|
+
kind: MaskedIdentifierKind.Phone,
|
|
564
|
+
isVerified: row.isVerified,
|
|
565
|
+
})
|
|
566
|
+
: null;
|
|
567
|
+
},
|
|
568
|
+
deleteRow: async (data: { methodId: ObjectID }): Promise<void> => {
|
|
569
|
+
await UserWhatsAppService.deleteOneById({
|
|
570
|
+
id: data.methodId,
|
|
571
|
+
props: { isRoot: true },
|
|
572
|
+
});
|
|
573
|
+
},
|
|
574
|
+
create: async (data: {
|
|
575
|
+
projectId: ObjectID;
|
|
576
|
+
userId: ObjectID;
|
|
577
|
+
value: string;
|
|
578
|
+
}): Promise<AdminNotificationMethodView> => {
|
|
579
|
+
const model: UserWhatsApp = new UserWhatsApp();
|
|
580
|
+
model.projectId = data.projectId;
|
|
581
|
+
model.userId = data.userId;
|
|
582
|
+
model.phone = new Phone(data.value);
|
|
583
|
+
|
|
584
|
+
const created: UserWhatsApp = await UserWhatsAppService.create({
|
|
585
|
+
data: model,
|
|
586
|
+
props: { isRoot: true },
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
return this.toView({
|
|
590
|
+
row: created,
|
|
591
|
+
methodType: ReadinessMethodType.WhatsApp,
|
|
592
|
+
isAdminAddable: true,
|
|
593
|
+
identifier: created.phone?.toString(),
|
|
594
|
+
kind: MaskedIdentifierKind.Phone,
|
|
595
|
+
isVerified: created.isVerified,
|
|
596
|
+
});
|
|
597
|
+
},
|
|
598
|
+
resendVerificationCode: async (methodId: ObjectID): Promise<void> => {
|
|
599
|
+
await UserWhatsAppService.resendVerificationCode(methodId);
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
/*
|
|
603
|
+
* The three read-and-remove-only channels. They carry no `create`, which
|
|
604
|
+
* is what the API's "this channel cannot be added by an administrator"
|
|
605
|
+
* refusal is derived from — the refusal is a consequence of the table
|
|
606
|
+
* rather than a second list that could drift out of step with it.
|
|
607
|
+
*/
|
|
608
|
+
{
|
|
609
|
+
methodType: ReadinessMethodType.Push,
|
|
610
|
+
isAdminAddable: false,
|
|
611
|
+
modelType: UserPush,
|
|
612
|
+
list: async (data: {
|
|
613
|
+
projectId: ObjectID;
|
|
614
|
+
userId: ObjectID;
|
|
615
|
+
}): Promise<Array<AdminNotificationMethodView>> => {
|
|
616
|
+
const rows: Array<UserPush> = await UserPushService.findBy({
|
|
617
|
+
query: { projectId: data.projectId, userId: data.userId },
|
|
618
|
+
select: {
|
|
619
|
+
_id: true,
|
|
620
|
+
deviceName: true,
|
|
621
|
+
isVerified: true,
|
|
622
|
+
createdAt: true,
|
|
623
|
+
},
|
|
624
|
+
sort: { createdAt: SortOrder.Ascending },
|
|
625
|
+
skip: 0,
|
|
626
|
+
limit: LIMIT_PER_PROJECT,
|
|
627
|
+
props: { isRoot: true },
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
/*
|
|
631
|
+
* `deviceName` and never `deviceToken`. The token is the addressable
|
|
632
|
+
* secret behind a push method — anyone holding it can push to that
|
|
633
|
+
* device — and this select is the only thing standing between it and
|
|
634
|
+
* a response body.
|
|
635
|
+
*/
|
|
636
|
+
return rows.map((row: UserPush): AdminNotificationMethodView => {
|
|
637
|
+
return this.toView({
|
|
638
|
+
row: row,
|
|
639
|
+
methodType: ReadinessMethodType.Push,
|
|
640
|
+
isAdminAddable: false,
|
|
641
|
+
identifier: row.deviceName,
|
|
642
|
+
kind: MaskedIdentifierKind.Handle,
|
|
643
|
+
isVerified: row.isVerified,
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
},
|
|
647
|
+
findOwnedRow: async (data: {
|
|
648
|
+
methodId: ObjectID;
|
|
649
|
+
projectId: ObjectID;
|
|
650
|
+
userId: ObjectID;
|
|
651
|
+
}): Promise<AdminNotificationMethodView | null> => {
|
|
652
|
+
const row: UserPush | null = await UserPushService.findOneBy({
|
|
653
|
+
query: {
|
|
654
|
+
_id: data.methodId,
|
|
655
|
+
projectId: data.projectId,
|
|
656
|
+
userId: data.userId,
|
|
657
|
+
},
|
|
658
|
+
select: { _id: true, deviceName: true, isVerified: true },
|
|
659
|
+
props: { isRoot: true },
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
return row
|
|
663
|
+
? this.toView({
|
|
664
|
+
row: row,
|
|
665
|
+
methodType: ReadinessMethodType.Push,
|
|
666
|
+
isAdminAddable: false,
|
|
667
|
+
identifier: row.deviceName,
|
|
668
|
+
kind: MaskedIdentifierKind.Handle,
|
|
669
|
+
isVerified: row.isVerified,
|
|
670
|
+
})
|
|
671
|
+
: null;
|
|
672
|
+
},
|
|
673
|
+
deleteRow: async (data: { methodId: ObjectID }): Promise<void> => {
|
|
674
|
+
await UserPushService.deleteOneById({
|
|
675
|
+
id: data.methodId,
|
|
676
|
+
props: { isRoot: true },
|
|
677
|
+
});
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
methodType: ReadinessMethodType.Telegram,
|
|
682
|
+
isAdminAddable: false,
|
|
683
|
+
modelType: UserTelegram,
|
|
684
|
+
list: async (data: {
|
|
685
|
+
projectId: ObjectID;
|
|
686
|
+
userId: ObjectID;
|
|
687
|
+
}): Promise<Array<AdminNotificationMethodView>> => {
|
|
688
|
+
const rows: Array<UserTelegram> = await UserTelegramService.findBy({
|
|
689
|
+
query: { projectId: data.projectId, userId: data.userId },
|
|
690
|
+
/*
|
|
691
|
+
* The handle only — never telegramChatId, which is the addressable
|
|
692
|
+
* target a bot sends to. Same rule OnCallReadinessService follows.
|
|
693
|
+
*/
|
|
694
|
+
select: {
|
|
695
|
+
_id: true,
|
|
696
|
+
telegramUserHandle: true,
|
|
697
|
+
isVerified: true,
|
|
698
|
+
createdAt: true,
|
|
699
|
+
},
|
|
700
|
+
sort: { createdAt: SortOrder.Ascending },
|
|
701
|
+
skip: 0,
|
|
702
|
+
limit: LIMIT_PER_PROJECT,
|
|
703
|
+
props: { isRoot: true },
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
return rows.map((row: UserTelegram): AdminNotificationMethodView => {
|
|
707
|
+
return this.toView({
|
|
708
|
+
row: row,
|
|
709
|
+
methodType: ReadinessMethodType.Telegram,
|
|
710
|
+
isAdminAddable: false,
|
|
711
|
+
identifier: row.telegramUserHandle,
|
|
712
|
+
kind: MaskedIdentifierKind.Handle,
|
|
713
|
+
isVerified: row.isVerified,
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
},
|
|
717
|
+
findOwnedRow: async (data: {
|
|
718
|
+
methodId: ObjectID;
|
|
719
|
+
projectId: ObjectID;
|
|
720
|
+
userId: ObjectID;
|
|
721
|
+
}): Promise<AdminNotificationMethodView | null> => {
|
|
722
|
+
const row: UserTelegram | null = await UserTelegramService.findOneBy({
|
|
723
|
+
query: {
|
|
724
|
+
_id: data.methodId,
|
|
725
|
+
projectId: data.projectId,
|
|
726
|
+
userId: data.userId,
|
|
727
|
+
},
|
|
728
|
+
select: {
|
|
729
|
+
_id: true,
|
|
730
|
+
telegramUserHandle: true,
|
|
731
|
+
isVerified: true,
|
|
732
|
+
},
|
|
733
|
+
props: { isRoot: true },
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
return row
|
|
737
|
+
? this.toView({
|
|
738
|
+
row: row,
|
|
739
|
+
methodType: ReadinessMethodType.Telegram,
|
|
740
|
+
isAdminAddable: false,
|
|
741
|
+
identifier: row.telegramUserHandle,
|
|
742
|
+
kind: MaskedIdentifierKind.Handle,
|
|
743
|
+
isVerified: row.isVerified,
|
|
744
|
+
})
|
|
745
|
+
: null;
|
|
746
|
+
},
|
|
747
|
+
deleteRow: async (data: { methodId: ObjectID }): Promise<void> => {
|
|
748
|
+
await UserTelegramService.deleteOneById({
|
|
749
|
+
id: data.methodId,
|
|
750
|
+
props: { isRoot: true },
|
|
751
|
+
});
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
methodType: ReadinessMethodType.Webhook,
|
|
756
|
+
isAdminAddable: false,
|
|
757
|
+
modelType: UserWebhook,
|
|
758
|
+
list: async (data: {
|
|
759
|
+
projectId: ObjectID;
|
|
760
|
+
userId: ObjectID;
|
|
761
|
+
}): Promise<Array<AdminNotificationMethodView>> => {
|
|
762
|
+
const rows: Array<UserWebhook> = await UserWebhookService.findBy({
|
|
763
|
+
query: { projectId: data.projectId, userId: data.userId },
|
|
764
|
+
/*
|
|
765
|
+
* `name` ONLY. UserWebhook.webhookUrl is a bearer credential —
|
|
766
|
+
* anyone holding a Slack/Discord/Teams hook url can post as the
|
|
767
|
+
* integration — so it never leaves the server on this path.
|
|
768
|
+
*/
|
|
769
|
+
select: { _id: true, name: true, createdAt: true },
|
|
770
|
+
sort: { createdAt: SortOrder.Ascending },
|
|
771
|
+
skip: 0,
|
|
772
|
+
limit: LIMIT_PER_PROJECT,
|
|
773
|
+
props: { isRoot: true },
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
return rows.map((row: UserWebhook): AdminNotificationMethodView => {
|
|
777
|
+
return this.toView({
|
|
778
|
+
row: row,
|
|
779
|
+
methodType: ReadinessMethodType.Webhook,
|
|
780
|
+
isAdminAddable: false,
|
|
781
|
+
identifier: row.name,
|
|
782
|
+
kind: MaskedIdentifierKind.Handle,
|
|
783
|
+
/*
|
|
784
|
+
* UserWebhook has no isVerified column: a webhook is live from
|
|
785
|
+
* the moment it is written. Reported as verified because that is
|
|
786
|
+
* what it behaves like, and reporting it as unverified would put
|
|
787
|
+
* a "needs setup" badge on a channel that is already delivering.
|
|
788
|
+
*/
|
|
789
|
+
isVerified: true,
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
},
|
|
793
|
+
findOwnedRow: async (data: {
|
|
794
|
+
methodId: ObjectID;
|
|
795
|
+
projectId: ObjectID;
|
|
796
|
+
userId: ObjectID;
|
|
797
|
+
}): Promise<AdminNotificationMethodView | null> => {
|
|
798
|
+
const row: UserWebhook | null = await UserWebhookService.findOneBy({
|
|
799
|
+
query: {
|
|
800
|
+
_id: data.methodId,
|
|
801
|
+
projectId: data.projectId,
|
|
802
|
+
userId: data.userId,
|
|
803
|
+
},
|
|
804
|
+
select: { _id: true, name: true },
|
|
805
|
+
props: { isRoot: true },
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
return row
|
|
809
|
+
? this.toView({
|
|
810
|
+
row: row,
|
|
811
|
+
methodType: ReadinessMethodType.Webhook,
|
|
812
|
+
isAdminAddable: false,
|
|
813
|
+
identifier: row.name,
|
|
814
|
+
kind: MaskedIdentifierKind.Handle,
|
|
815
|
+
isVerified: true,
|
|
816
|
+
})
|
|
817
|
+
: null;
|
|
818
|
+
},
|
|
819
|
+
deleteRow: async (data: { methodId: ObjectID }): Promise<void> => {
|
|
820
|
+
await UserWebhookService.deleteOneById({
|
|
821
|
+
id: data.methodId,
|
|
822
|
+
props: { isRoot: true },
|
|
823
|
+
});
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
];
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/*
|
|
830
|
+
* The ONLY place a raw identifier is turned into something this service will
|
|
831
|
+
* return. Every descriptor above routes through it, so there is no code path
|
|
832
|
+
* on which an unmasked value can reach a view object by accident — reaching
|
|
833
|
+
* one would mean deliberately constructing the interface by hand.
|
|
834
|
+
*/
|
|
835
|
+
private toView(data: {
|
|
836
|
+
row: BaseModel;
|
|
837
|
+
methodType: ReadinessMethodType;
|
|
838
|
+
isAdminAddable: boolean;
|
|
839
|
+
identifier: string | undefined;
|
|
840
|
+
kind: MaskedIdentifierKind;
|
|
841
|
+
isVerified: boolean | undefined;
|
|
842
|
+
}): AdminNotificationMethodView {
|
|
843
|
+
return {
|
|
844
|
+
methodId: data.row.id!,
|
|
845
|
+
methodType: data.methodType,
|
|
846
|
+
maskedIdentifier: maskIdentifier(data.identifier, data.kind),
|
|
847
|
+
isVerified: Boolean(data.isVerified),
|
|
848
|
+
isAdminAddable: data.isAdminAddable,
|
|
849
|
+
createdAt: data.row.createdAt,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private getChannelDescriptor(
|
|
854
|
+
methodType: string,
|
|
855
|
+
): ChannelDescriptor | undefined {
|
|
856
|
+
return this.getChannelDescriptors().find(
|
|
857
|
+
(descriptor: ChannelDescriptor): boolean => {
|
|
858
|
+
return descriptor.methodType === methodType;
|
|
859
|
+
},
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Every notification method this user has in this project, masked.
|
|
865
|
+
*
|
|
866
|
+
* Ordered by channel and then by age, so the list an admin reads is stable
|
|
867
|
+
* between visits — a list that reshuffles is a list somebody misreads when
|
|
868
|
+
* they come back to remove the row they saw a moment ago.
|
|
869
|
+
*/
|
|
870
|
+
@CaptureSpan()
|
|
871
|
+
public async listMethodsForUser(data: {
|
|
872
|
+
projectId: ObjectID;
|
|
873
|
+
userId: ObjectID;
|
|
874
|
+
}): Promise<Array<AdminNotificationMethodView>> {
|
|
875
|
+
const methods: Array<AdminNotificationMethodView> = [];
|
|
876
|
+
|
|
877
|
+
for (const descriptor of this.getChannelDescriptors()) {
|
|
878
|
+
methods.push(
|
|
879
|
+
...(await descriptor.list({
|
|
880
|
+
projectId: data.projectId,
|
|
881
|
+
userId: data.userId,
|
|
882
|
+
})),
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return methods;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Membership, read straight from TeamMember with root props and no cache.
|
|
891
|
+
*
|
|
892
|
+
* Holding an administrative permission is a claim about a PROJECT, so it can
|
|
893
|
+
* only ever authorise writing for users of that project. Without this, one
|
|
894
|
+
* throwaway project where the caller is an admin would license adding a phone
|
|
895
|
+
* number to any user id in the installation.
|
|
896
|
+
*
|
|
897
|
+
* TeamMemberService.getTeamIdsForUser would answer the same question with one
|
|
898
|
+
* fewer query, but it memoises for 60 seconds — and a security decision that
|
|
899
|
+
* keeps saying "yes" for a minute after somebody was removed from the project
|
|
900
|
+
* is not a security decision.
|
|
901
|
+
*
|
|
902
|
+
* `hasAcceptedInvitation` is part of the test for the same reason
|
|
903
|
+
* UserNotificationRuleAdminService includes it: a pending invitation is an
|
|
904
|
+
* admin-created row for an arbitrary email address, so treating one as
|
|
905
|
+
* membership would hand back most of what this guard takes away.
|
|
906
|
+
*/
|
|
907
|
+
@CaptureSpan()
|
|
908
|
+
public async assertTargetUserIsProjectMember(data: {
|
|
909
|
+
targetUserId: ObjectID;
|
|
910
|
+
projectId: ObjectID;
|
|
911
|
+
}): Promise<void> {
|
|
912
|
+
const membership: TeamMember | null = await TeamMemberService.findOneBy({
|
|
913
|
+
query: {
|
|
914
|
+
userId: data.targetUserId,
|
|
915
|
+
projectId: data.projectId,
|
|
916
|
+
hasAcceptedInvitation: true,
|
|
917
|
+
},
|
|
918
|
+
select: { _id: true },
|
|
919
|
+
props: { isRoot: true },
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
if (!membership) {
|
|
923
|
+
throw new BadDataException(
|
|
924
|
+
"This user is not a member of this project, so their notification methods cannot be managed here.",
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Add a notification method to another member's account.
|
|
931
|
+
*
|
|
932
|
+
* The row is written with the TARGET's userId and with isVerified untouched,
|
|
933
|
+
* which is what makes the method service mail or message a verification code
|
|
934
|
+
* to the address itself. Nothing here can mark it verified, and neither can
|
|
935
|
+
* the administrator afterwards: the verify endpoints compare the row's owner
|
|
936
|
+
* against the signed-in caller.
|
|
937
|
+
*
|
|
938
|
+
* `value` is validated by the type it becomes — Email and Phone both throw
|
|
939
|
+
* BadDataException from their constructors — so a malformed number is
|
|
940
|
+
* refused before any row is written rather than stored and discovered later
|
|
941
|
+
* by a page that fails to deliver.
|
|
942
|
+
*/
|
|
943
|
+
@CaptureSpan()
|
|
944
|
+
public async addMethodForUser(data: {
|
|
945
|
+
projectId: ObjectID;
|
|
946
|
+
targetUserId: ObjectID;
|
|
947
|
+
actorUserId: ObjectID | undefined;
|
|
948
|
+
methodType: string;
|
|
949
|
+
value: string;
|
|
950
|
+
props: DatabaseCommonInteractionProps;
|
|
951
|
+
}): Promise<AdminNotificationMethodView> {
|
|
952
|
+
await this.assertTargetUserIsProjectMember({
|
|
953
|
+
targetUserId: data.targetUserId,
|
|
954
|
+
projectId: data.projectId,
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
const descriptor: ChannelDescriptor | undefined = this.getChannelDescriptor(
|
|
958
|
+
data.methodType,
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
if (!descriptor || !descriptor.create) {
|
|
962
|
+
/*
|
|
963
|
+
* One sentence for "no such channel" and for "that channel exists but an
|
|
964
|
+
* administrator may not create it", because the two are the same answer
|
|
965
|
+
* to the caller and naming which one it was would describe the shape of
|
|
966
|
+
* the feature to somebody probing it.
|
|
967
|
+
*/
|
|
968
|
+
throw new BadDataException(
|
|
969
|
+
`A project administrator can only add Email, SMS, Call and WhatsApp notification methods for another user. Push, Telegram and webhook methods have to be added by the person who owns the device.`,
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const trimmed: string = (data.value || "").trim();
|
|
974
|
+
|
|
975
|
+
if (!trimmed) {
|
|
976
|
+
throw new BadDataException(
|
|
977
|
+
`A ${descriptor.methodType} notification method needs a value.`,
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/*
|
|
982
|
+
* Adding the same address twice is refused rather than deduplicated. Two
|
|
983
|
+
* identical rows are not merely untidy: they show up as two entries an
|
|
984
|
+
* admin has to tell apart on a masked list, each with its own verification
|
|
985
|
+
* state, and a rule pointed at the unverified one looks correct and pages
|
|
986
|
+
* nobody.
|
|
987
|
+
*/
|
|
988
|
+
const existing: Array<AdminNotificationMethodView> = await descriptor.list({
|
|
989
|
+
projectId: data.projectId,
|
|
990
|
+
userId: data.targetUserId,
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
const candidateMask: string = maskIdentifier(
|
|
994
|
+
trimmed,
|
|
995
|
+
descriptor.methodType === ReadinessMethodType.Email
|
|
996
|
+
? MaskedIdentifierKind.Email
|
|
997
|
+
: MaskedIdentifierKind.Phone,
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
/*
|
|
1001
|
+
* Compared on the MASK, deliberately. The raw values are on the server and
|
|
1002
|
+
* could be compared directly, but a duplicate check that reports "this
|
|
1003
|
+
* already exists" for a value the admin cannot see is only useful if the
|
|
1004
|
+
* thing it matched is the thing they are looking at on screen — and what
|
|
1005
|
+
* they are looking at is the mask.
|
|
1006
|
+
*/
|
|
1007
|
+
const isDuplicate: boolean = existing.some(
|
|
1008
|
+
(method: AdminNotificationMethodView): boolean => {
|
|
1009
|
+
return method.maskedIdentifier === candidateMask;
|
|
1010
|
+
},
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
if (isDuplicate) {
|
|
1014
|
+
throw new BadDataException(
|
|
1015
|
+
`This user already has a ${descriptor.methodType} notification method that looks like ${candidateMask}.`,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const created: AdminNotificationMethodView = await descriptor.create({
|
|
1020
|
+
projectId: data.projectId,
|
|
1021
|
+
userId: data.targetUserId,
|
|
1022
|
+
value: trimmed,
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
await this.recordAdminMethodChange({
|
|
1026
|
+
action: AuditLogAction.Create,
|
|
1027
|
+
actorUserId: data.actorUserId,
|
|
1028
|
+
ownerUserId: data.targetUserId,
|
|
1029
|
+
projectId: data.projectId,
|
|
1030
|
+
methodType: descriptor.methodType,
|
|
1031
|
+
modelType: descriptor.modelType,
|
|
1032
|
+
methodId: created.methodId,
|
|
1033
|
+
isVerified: created.isVerified,
|
|
1034
|
+
maskedIdentifier: created.maskedIdentifier,
|
|
1035
|
+
props: data.props,
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
return created;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* What removing this method would cost the person who owns it.
|
|
1043
|
+
*
|
|
1044
|
+
* Every method foreign key on UserNotificationRule is onDelete: "CASCADE",
|
|
1045
|
+
* and each method service deletes the same rows itself in onBeforeDelete, so
|
|
1046
|
+
* removing one phone number takes every rule that pointed at it with it. An
|
|
1047
|
+
* administrator has even less reason to expect that than the owner does —
|
|
1048
|
+
* they are usually tidying up a number they know is dead — so the numbers are
|
|
1049
|
+
* put in front of them before they confirm.
|
|
1050
|
+
*
|
|
1051
|
+
* The row is resolved through the ownership check first, so a caller cannot
|
|
1052
|
+
* use this preview to learn anything about a method id belonging to another
|
|
1053
|
+
* user or another project.
|
|
1054
|
+
*/
|
|
1055
|
+
@CaptureSpan()
|
|
1056
|
+
public async getDeletionPreview(data: {
|
|
1057
|
+
projectId: ObjectID;
|
|
1058
|
+
targetUserId: ObjectID;
|
|
1059
|
+
methodType: string;
|
|
1060
|
+
methodId: ObjectID;
|
|
1061
|
+
}): Promise<AdminNotificationMethodDeletionPreview> {
|
|
1062
|
+
const descriptor: ChannelDescriptor =
|
|
1063
|
+
await this.resolveOwnedMethodDescriptor(data);
|
|
1064
|
+
|
|
1065
|
+
const impact: NotificationDeletionImpact =
|
|
1066
|
+
await UserNotificationRuleService.getNotificationMethodDeletionImpact({
|
|
1067
|
+
projectId: data.projectId,
|
|
1068
|
+
methodType: descriptor.methodType,
|
|
1069
|
+
methodId: data.methodId,
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
return {
|
|
1073
|
+
rulesDeletedCount: impact.rulesDeletedCount,
|
|
1074
|
+
coverageLostCount: impact.coverageLost.length,
|
|
1075
|
+
verifiedMethodCountAfterDeletion: impact.verifiedMethodCountAfterDeletion,
|
|
1076
|
+
reachability: impact.reachability,
|
|
1077
|
+
isFallbackEnabled: impact.isFallbackEnabled,
|
|
1078
|
+
isTruncated: impact.isTruncated,
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Remove one of another member's notification methods.
|
|
1084
|
+
*
|
|
1085
|
+
* Allowed on all seven channels: a device that has been handed back, a number
|
|
1086
|
+
* that has been reassigned or a webhook pointing at a decommissioned endpoint
|
|
1087
|
+
* are all things an administrator has to be able to clear up, frequently
|
|
1088
|
+
* without the owner around to do it.
|
|
1089
|
+
*
|
|
1090
|
+
* It is also the one operation here that can make somebody LESS reachable, so
|
|
1091
|
+
* it is the one that always mails them — including when it leaves them with
|
|
1092
|
+
* nothing.
|
|
1093
|
+
*/
|
|
1094
|
+
@CaptureSpan()
|
|
1095
|
+
public async deleteMethodForUser(data: {
|
|
1096
|
+
projectId: ObjectID;
|
|
1097
|
+
targetUserId: ObjectID;
|
|
1098
|
+
actorUserId: ObjectID | undefined;
|
|
1099
|
+
methodType: string;
|
|
1100
|
+
methodId: ObjectID;
|
|
1101
|
+
props: DatabaseCommonInteractionProps;
|
|
1102
|
+
}): Promise<void> {
|
|
1103
|
+
await this.assertTargetUserIsProjectMember({
|
|
1104
|
+
targetUserId: data.targetUserId,
|
|
1105
|
+
projectId: data.projectId,
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
const descriptor: ChannelDescriptor =
|
|
1109
|
+
await this.resolveOwnedMethodDescriptor(data);
|
|
1110
|
+
|
|
1111
|
+
/*
|
|
1112
|
+
* Read before the delete, because afterwards there is nothing to read. The
|
|
1113
|
+
* mask is the whole description of what the project just lost, and it is
|
|
1114
|
+
* the only thing the owner's mail can name.
|
|
1115
|
+
*/
|
|
1116
|
+
const before: AdminNotificationMethodView | null =
|
|
1117
|
+
await descriptor.findOwnedRow({
|
|
1118
|
+
methodId: data.methodId,
|
|
1119
|
+
projectId: data.projectId,
|
|
1120
|
+
userId: data.targetUserId,
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
await descriptor.deleteRow({ methodId: data.methodId });
|
|
1124
|
+
|
|
1125
|
+
await this.recordAdminMethodChange({
|
|
1126
|
+
action: AuditLogAction.Delete,
|
|
1127
|
+
actorUserId: data.actorUserId,
|
|
1128
|
+
ownerUserId: data.targetUserId,
|
|
1129
|
+
projectId: data.projectId,
|
|
1130
|
+
methodType: descriptor.methodType,
|
|
1131
|
+
modelType: descriptor.modelType,
|
|
1132
|
+
methodId: data.methodId,
|
|
1133
|
+
isVerified: Boolean(before?.isVerified),
|
|
1134
|
+
maskedIdentifier: before?.maskedIdentifier || "",
|
|
1135
|
+
props: data.props,
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Send the verification code again, to the device.
|
|
1141
|
+
*
|
|
1142
|
+
* This is the administrator's most useful lever after adding a method, and it
|
|
1143
|
+
* discloses nothing: the code goes to the address on the row, which is the
|
|
1144
|
+
* one place the admin cannot read. It is how "I added your work mobile,
|
|
1145
|
+
* please confirm it" becomes something they can nudge rather than re-type.
|
|
1146
|
+
*/
|
|
1147
|
+
@CaptureSpan()
|
|
1148
|
+
public async resendVerificationCodeForUser(data: {
|
|
1149
|
+
projectId: ObjectID;
|
|
1150
|
+
targetUserId: ObjectID;
|
|
1151
|
+
methodType: string;
|
|
1152
|
+
methodId: ObjectID;
|
|
1153
|
+
}): Promise<void> {
|
|
1154
|
+
await this.assertTargetUserIsProjectMember({
|
|
1155
|
+
targetUserId: data.targetUserId,
|
|
1156
|
+
projectId: data.projectId,
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
const descriptor: ChannelDescriptor =
|
|
1160
|
+
await this.resolveOwnedMethodDescriptor(data);
|
|
1161
|
+
|
|
1162
|
+
if (!descriptor.resendVerificationCode) {
|
|
1163
|
+
throw new BadDataException(
|
|
1164
|
+
`${descriptor.methodType} notification methods do not use a verification code.`,
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
await descriptor.resendVerificationCode(data.methodId);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/*
|
|
1172
|
+
* "Does this method exist, is it on a channel we know, and does it belong to
|
|
1173
|
+
* THIS user in THIS project?" — asked once, by every write path.
|
|
1174
|
+
*
|
|
1175
|
+
* A row that does not exist and a row belonging to somebody else get the same
|
|
1176
|
+
* refusal. In both cases the caller named a row they have no business naming,
|
|
1177
|
+
* and a distinguishable answer turns this into a probe for which method ids
|
|
1178
|
+
* are real.
|
|
1179
|
+
*/
|
|
1180
|
+
private async resolveOwnedMethodDescriptor(data: {
|
|
1181
|
+
projectId: ObjectID;
|
|
1182
|
+
targetUserId: ObjectID;
|
|
1183
|
+
methodType: string;
|
|
1184
|
+
methodId: ObjectID;
|
|
1185
|
+
}): Promise<ChannelDescriptor> {
|
|
1186
|
+
const descriptor: ChannelDescriptor | undefined = this.getChannelDescriptor(
|
|
1187
|
+
data.methodType,
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
if (!descriptor) {
|
|
1191
|
+
throw new BadDataException(
|
|
1192
|
+
"This notification method could not be found for this user.",
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const row: AdminNotificationMethodView | null =
|
|
1197
|
+
await descriptor.findOwnedRow({
|
|
1198
|
+
methodId: data.methodId,
|
|
1199
|
+
projectId: data.projectId,
|
|
1200
|
+
userId: data.targetUserId,
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
if (!row) {
|
|
1204
|
+
throw new BadDataException(
|
|
1205
|
+
"This notification method could not be found for this user.",
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
return descriptor;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/*
|
|
1213
|
+
* The trail, and the mail. Nothing in here may break the write: the row is
|
|
1214
|
+
* already committed by the time this runs, so throwing now would report a
|
|
1215
|
+
* failure for a change that happened and the caller would be entitled to
|
|
1216
|
+
* believe it did not.
|
|
1217
|
+
*/
|
|
1218
|
+
private async recordAdminMethodChange(data: {
|
|
1219
|
+
action: AuditLogAction;
|
|
1220
|
+
actorUserId: ObjectID | undefined;
|
|
1221
|
+
ownerUserId: ObjectID;
|
|
1222
|
+
projectId: ObjectID;
|
|
1223
|
+
methodType: ReadinessMethodType;
|
|
1224
|
+
modelType: { new (): BaseModel };
|
|
1225
|
+
methodId: ObjectID | null | undefined;
|
|
1226
|
+
isVerified: boolean;
|
|
1227
|
+
maskedIdentifier: string;
|
|
1228
|
+
props: DatabaseCommonInteractionProps;
|
|
1229
|
+
}): Promise<void> {
|
|
1230
|
+
try {
|
|
1231
|
+
/*
|
|
1232
|
+
* The dependable half. AuditLogService writes only on enterprise builds
|
|
1233
|
+
* with audit logs switched on for the project, and "an administrator
|
|
1234
|
+
* added a phone number to somebody's account" is exactly the event an
|
|
1235
|
+
* operator needs to be able to find on the other installs too.
|
|
1236
|
+
*/
|
|
1237
|
+
logger.info(
|
|
1238
|
+
`Notification method ${data.action.toLowerCase()}d for user ${data.ownerUserId.toString()} by ${
|
|
1239
|
+
data.actorUserId ? data.actorUserId.toString() : "an unknown actor"
|
|
1240
|
+
}.`,
|
|
1241
|
+
{
|
|
1242
|
+
projectId: data.projectId.toString(),
|
|
1243
|
+
userId: data.ownerUserId.toString(),
|
|
1244
|
+
actorUserId: data.actorUserId?.toString(),
|
|
1245
|
+
methodType: data.methodType,
|
|
1246
|
+
action: data.action,
|
|
1247
|
+
} as LogAttributes,
|
|
1248
|
+
);
|
|
1249
|
+
|
|
1250
|
+
await this.writeAuditLog({
|
|
1251
|
+
action: data.action,
|
|
1252
|
+
modelType: data.modelType,
|
|
1253
|
+
methodId: data.methodId,
|
|
1254
|
+
ownerUserId: data.ownerUserId,
|
|
1255
|
+
projectId: data.projectId,
|
|
1256
|
+
isVerified: data.isVerified,
|
|
1257
|
+
props: data.props,
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
/*
|
|
1261
|
+
* The owner is not mailed about their own action. A person managing their
|
|
1262
|
+
* own methods from this page — an owner looking at their own row — has
|
|
1263
|
+
* just done the thing the mail would be warning them about.
|
|
1264
|
+
*/
|
|
1265
|
+
if (
|
|
1266
|
+
data.actorUserId &&
|
|
1267
|
+
data.actorUserId.toString() === data.ownerUserId.toString()
|
|
1268
|
+
) {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/*
|
|
1273
|
+
* The readiness cache holds this user's method list for 60 seconds, and
|
|
1274
|
+
* the page the admin is looking at re-reads readiness the moment this
|
|
1275
|
+
* returns. Left alone it would redraw the responder exactly as they were
|
|
1276
|
+
* before the change, which reads as "the add did not work".
|
|
1277
|
+
*/
|
|
1278
|
+
OnCallReadinessService.clearCache();
|
|
1279
|
+
|
|
1280
|
+
/*
|
|
1281
|
+
* Fire-and-forget by construction: the owner's mail involves three more
|
|
1282
|
+
* reads and an SMTP round trip, none of which the writing request should
|
|
1283
|
+
* wait on and none of which may surface as a failed write.
|
|
1284
|
+
*/
|
|
1285
|
+
this.notifyOwnerOfAdminChange(data).catch((error: Error): void => {
|
|
1286
|
+
logger.error(
|
|
1287
|
+
`UserNotificationMethodAdminService: failed to tell user ${data.ownerUserId.toString()} that an administrator changed their notification methods.`,
|
|
1288
|
+
);
|
|
1289
|
+
logger.error(error);
|
|
1290
|
+
});
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
logger.error(
|
|
1293
|
+
"UserNotificationMethodAdminService: failed to record an administrative notification method change.",
|
|
1294
|
+
);
|
|
1295
|
+
logger.error(error);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* The AuditLog row, written from a BLANK model rather than from the row that
|
|
1301
|
+
* changed. That is the whole subtlety of this method.
|
|
1302
|
+
*
|
|
1303
|
+
* AuditLogService snapshots every column it is handed and redacts only the
|
|
1304
|
+
* ones whose column-level `read` list is EMPTY. On UserEmail the address is
|
|
1305
|
+
* `read: [Permission.CurrentUser]`, not empty — so handing it the real row
|
|
1306
|
+
* would write the raw email address, phone number or webhook name into an
|
|
1307
|
+
* analytics table that every project administrator can query. For a removal
|
|
1308
|
+
* that address is one the acting admin never saw, which would make the audit
|
|
1309
|
+
* trail leak precisely what the page it records was built to hide.
|
|
1310
|
+
*
|
|
1311
|
+
* So the snapshot carries the three facts that make the entry useful and
|
|
1312
|
+
* nothing else: which project, whose account, and whether the method was
|
|
1313
|
+
* live. `buildSnapshotChanges` skips undefined columns, so everything else
|
|
1314
|
+
* simply is not in the row. WHAT was added or removed is in the message
|
|
1315
|
+
* logged above it, masked; WHO did it comes from `props` by way of
|
|
1316
|
+
* resolveActor.
|
|
1317
|
+
*
|
|
1318
|
+
* AuditLogService is required lazily for the same reason DatabaseService
|
|
1319
|
+
* requires it lazily: it depends on ProjectService and UserService, both of
|
|
1320
|
+
* which extend DatabaseService, so a top-level import can leave the base
|
|
1321
|
+
* class undefined at class-extension time.
|
|
1322
|
+
*/
|
|
1323
|
+
private async writeAuditLog(data: {
|
|
1324
|
+
action: AuditLogAction;
|
|
1325
|
+
modelType: { new (): BaseModel };
|
|
1326
|
+
methodId: ObjectID | null | undefined;
|
|
1327
|
+
ownerUserId: ObjectID;
|
|
1328
|
+
projectId: ObjectID;
|
|
1329
|
+
isVerified: boolean;
|
|
1330
|
+
props: DatabaseCommonInteractionProps;
|
|
1331
|
+
}): Promise<void> {
|
|
1332
|
+
const auditLogService: typeof AuditLogServiceType =
|
|
1333
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
1334
|
+
require("./AuditLogService").default;
|
|
1335
|
+
|
|
1336
|
+
if (!auditLogService) {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const snapshot: BaseModel = new data.modelType();
|
|
1341
|
+
|
|
1342
|
+
/*
|
|
1343
|
+
* Assigned through an index rather than through typed properties because
|
|
1344
|
+
* `snapshot` is a BaseModel here — the three columns are declared on each
|
|
1345
|
+
* of the seven concrete classes, not on the base, and casting to one of
|
|
1346
|
+
* them would be claiming a type this method deliberately does not know.
|
|
1347
|
+
*/
|
|
1348
|
+
const columns: Record<string, unknown> = snapshot as unknown as Record<
|
|
1349
|
+
string,
|
|
1350
|
+
unknown
|
|
1351
|
+
>;
|
|
1352
|
+
|
|
1353
|
+
if (data.methodId) {
|
|
1354
|
+
columns["_id"] = data.methodId.toString();
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
columns["projectId"] = data.projectId;
|
|
1358
|
+
columns["userId"] = data.ownerUserId;
|
|
1359
|
+
columns["isVerified"] = data.isVerified;
|
|
1360
|
+
|
|
1361
|
+
if (data.action === AuditLogAction.Create) {
|
|
1362
|
+
await auditLogService.recordCreate({
|
|
1363
|
+
model: new data.modelType(),
|
|
1364
|
+
createdItem: snapshot,
|
|
1365
|
+
props: data.props,
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/*
|
|
1372
|
+
* A deleted method cannot be re-read, so this snapshot is the whole of what
|
|
1373
|
+
* the project has left of it. It is also the reason the id is set above: an
|
|
1374
|
+
* entry with no resourceId cannot be tied back to the rules that went with
|
|
1375
|
+
* it.
|
|
1376
|
+
*/
|
|
1377
|
+
if (data.methodId) {
|
|
1378
|
+
await auditLogService.recordDelete({
|
|
1379
|
+
model: new data.modelType(),
|
|
1380
|
+
deletedItem: snapshot,
|
|
1381
|
+
itemId: data.methodId,
|
|
1382
|
+
props: data.props,
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/*
|
|
1388
|
+
* Tell the person whose account just changed.
|
|
1389
|
+
*
|
|
1390
|
+
* This is what makes the capability safe to hand out rather than merely
|
|
1391
|
+
* possible. An admin adding a colleague's work mobile and an attacker adding
|
|
1392
|
+
* a number of their own are the same request from the server's side; the only
|
|
1393
|
+
* party who always knows which it was is the account holder.
|
|
1394
|
+
*/
|
|
1395
|
+
private async notifyOwnerOfAdminChange(data: {
|
|
1396
|
+
action: AuditLogAction;
|
|
1397
|
+
actorUserId: ObjectID | undefined;
|
|
1398
|
+
ownerUserId: ObjectID;
|
|
1399
|
+
projectId: ObjectID;
|
|
1400
|
+
methodType: ReadinessMethodType;
|
|
1401
|
+
maskedIdentifier: string;
|
|
1402
|
+
}): Promise<void> {
|
|
1403
|
+
const owner: User | null = await UserService.findOneById({
|
|
1404
|
+
id: data.ownerUserId,
|
|
1405
|
+
select: { _id: true, name: true, email: true },
|
|
1406
|
+
props: { isRoot: true },
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
if (!owner || !owner.email) {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
let actorDescription: string = "A project administrator";
|
|
1414
|
+
|
|
1415
|
+
if (data.actorUserId) {
|
|
1416
|
+
const actor: User | null = await UserService.findOneById({
|
|
1417
|
+
id: data.actorUserId,
|
|
1418
|
+
select: { _id: true, name: true, email: true },
|
|
1419
|
+
props: { isRoot: true },
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
/*
|
|
1423
|
+
* Named by email as well as by name, deliberately. Display names are not
|
|
1424
|
+
* unique and are user-editable, so "Alex added a phone number" is not
|
|
1425
|
+
* something the reader can act on; an address is.
|
|
1426
|
+
*/
|
|
1427
|
+
actorDescription = actor
|
|
1428
|
+
? `${actor.name?.toString() || "A project administrator"} (${
|
|
1429
|
+
actor.email?.toString() || data.actorUserId.toString()
|
|
1430
|
+
})`
|
|
1431
|
+
: `A project administrator (${data.actorUserId.toString()})`;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
let projectName: string = "your project";
|
|
1435
|
+
|
|
1436
|
+
const project: Project | null = await ProjectService.findOneById({
|
|
1437
|
+
id: data.projectId,
|
|
1438
|
+
select: { _id: true, name: true },
|
|
1439
|
+
props: { isRoot: true },
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
if (project?.name) {
|
|
1443
|
+
projectName = project.name;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
const isAdd: boolean = data.action === AuditLogAction.Create;
|
|
1447
|
+
|
|
1448
|
+
const subject: string = isAdd
|
|
1449
|
+
? "A notification method was added to your account"
|
|
1450
|
+
: "A notification method was removed from your account";
|
|
1451
|
+
|
|
1452
|
+
/*
|
|
1453
|
+
* The two consequences are genuinely different and are worth separate
|
|
1454
|
+
* sentences. An addition is inert until the owner verifies it, and saying
|
|
1455
|
+
* so is what turns "somebody touched my account" into an instruction. A
|
|
1456
|
+
* removal has already happened and may have taken rules with it, which is
|
|
1457
|
+
* the only one of the two that can leave a responder silently unreachable.
|
|
1458
|
+
*/
|
|
1459
|
+
const consequence: string = isAdd
|
|
1460
|
+
? "It cannot be used to notify you until you verify it, and only you can do that — a verification code has been sent to the address or device itself. If you were not expecting this, do not verify it, remove it from your notification methods, and tell your project owners."
|
|
1461
|
+
: "Any notification rules that used it were removed with it, so you may no longer be paged the way you were. Check your notification methods and rules now, and tell your project owners if you were not expecting this.";
|
|
1462
|
+
|
|
1463
|
+
const message: string = `${this.escapeHtml(actorDescription)} ${
|
|
1464
|
+
isAdd ? "added" : "removed"
|
|
1465
|
+
} a ${this.escapeHtml(data.methodType)} notification method (${this.escapeHtml(
|
|
1466
|
+
data.maskedIdentifier,
|
|
1467
|
+
)}) ${isAdd ? "to" : "from"} your account in ${this.escapeHtml(
|
|
1468
|
+
projectName,
|
|
1469
|
+
)}.<br/><br/>${consequence}`;
|
|
1470
|
+
|
|
1471
|
+
const settingsLink: string = (
|
|
1472
|
+
await this.getNotificationMethodsLinkInDashboard(data.projectId)
|
|
1473
|
+
).toString();
|
|
1474
|
+
|
|
1475
|
+
const vars: Dictionary<string> = {
|
|
1476
|
+
subject: subject,
|
|
1477
|
+
message: `${message}<br/><br/><a href="${this.escapeHtml(
|
|
1478
|
+
settingsLink,
|
|
1479
|
+
)}">Review your notification methods</a>`,
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
await MailService.sendMail(
|
|
1483
|
+
{
|
|
1484
|
+
toEmail: owner.email,
|
|
1485
|
+
templateType: EmailTemplateType.SimpleMessage,
|
|
1486
|
+
vars: vars,
|
|
1487
|
+
subject: subject,
|
|
1488
|
+
},
|
|
1489
|
+
{
|
|
1490
|
+
projectId: data.projectId,
|
|
1491
|
+
userId: owner.id!,
|
|
1492
|
+
},
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
private async getNotificationMethodsLinkInDashboard(
|
|
1497
|
+
projectId: ObjectID,
|
|
1498
|
+
): Promise<URL> {
|
|
1499
|
+
const dashboardUrl: URL = await DatabaseConfig.getDashboardUrl();
|
|
1500
|
+
|
|
1501
|
+
/*
|
|
1502
|
+
* Spelled out rather than imported: Common/Server cannot reach the
|
|
1503
|
+
* Dashboard's RouteMap, the same reason
|
|
1504
|
+
* UserNotificationRuleAdminService.getNotificationRulesLinkInDashboard
|
|
1505
|
+
* duplicates its path segments.
|
|
1506
|
+
*/
|
|
1507
|
+
return URL.fromString(dashboardUrl.toString()).addRoute(
|
|
1508
|
+
`/${projectId.toString()}/user-settings/notification-methods`,
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/*
|
|
1513
|
+
* SimpleMessage.hbs renders `message` through a triple-stache InfoBlock
|
|
1514
|
+
* partial, so nothing between here and the recipient's inbox escapes
|
|
1515
|
+
* anything. Project names and user names are attacker-influenced free text.
|
|
1516
|
+
*/
|
|
1517
|
+
private escapeHtml(value: string): string {
|
|
1518
|
+
return value
|
|
1519
|
+
.replace(/&/g, "&")
|
|
1520
|
+
.replace(/</g, "<")
|
|
1521
|
+
.replace(/>/g, ">")
|
|
1522
|
+
.replace(/"/g, """)
|
|
1523
|
+
.replace(/'/g, "'");
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
export default new UserNotificationMethodAdminService();
|