@oneuptime/common 12.0.29 → 12.0.31
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/AnalyticsModels/SecurityEvent.ts +48 -16
- package/Models/AnalyticsModels/ThreatIntelIndicator.ts +10 -5
- package/Models/DatabaseModels/AlertSeverity.ts +44 -0
- package/Models/DatabaseModels/DetectionRule.ts +16 -5
- package/Models/DatabaseModels/GoogleSecOpsConnection.ts +14 -5
- package/Models/DatabaseModels/IncidentSeverity.ts +44 -0
- package/Models/DatabaseModels/Index.ts +13 -0
- package/Models/DatabaseModels/NetworkDevice.ts +113 -3
- package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +88 -0
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +22 -0
- package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +499 -0
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
- package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
- package/Models/DatabaseModels/ThreatIntelFeed.ts +16 -5
- package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
- package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
- package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
- package/Server/API/GlobalConfigAPI.ts +362 -183
- package/Server/API/OnCallCalendarAPI.ts +2194 -0
- package/Server/API/TelemetryAPI.ts +42 -4
- package/Server/EnvironmentConfig.ts +250 -0
- package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790500000000-AddNetworkDeviceOidTemplate.ts +59 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790600000000-AddAutoImportRuleOidTemplate.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/MultipartFormData.ts +118 -32
- package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
- package/Server/Services/AlertEpisodeLabelRuleEngineService.ts +9 -1
- package/Server/Services/AlertEpisodeOnCallRuleEngineService.ts +9 -1
- package/Server/Services/AlertEpisodeOwnerRuleEngineService.ts +9 -1
- package/Server/Services/AlertEpisodePrivacyRuleEngineService.ts +9 -1
- package/Server/Services/AlertGroupingEngineService.ts +9 -1
- package/Server/Services/AlertLabelRuleEngineService.ts +9 -1
- package/Server/Services/AlertOnCallRuleEngineService.ts +9 -1
- package/Server/Services/AlertOwnerRuleEngineService.ts +9 -1
- package/Server/Services/AlertPrivacyRuleEngineService.ts +9 -1
- package/Server/Services/AlertReminderRuleService.ts +9 -1
- package/Server/Services/AutoRemediationRuleEngineService.ts +9 -1
- package/Server/Services/CephClusterLabelRuleEngineService.ts +9 -1
- package/Server/Services/CephClusterOwnerRuleEngineService.ts +9 -1
- package/Server/Services/CloudResourceLabelRuleEngineService.ts +9 -1
- package/Server/Services/CloudResourceOwnerRuleEngineService.ts +9 -1
- package/Server/Services/DashboardLabelRuleEngineService.ts +9 -1
- package/Server/Services/DashboardOwnerRuleEngineService.ts +9 -1
- package/Server/Services/DockerHostLabelRuleEngineService.ts +9 -1
- package/Server/Services/DockerHostOwnerRuleEngineService.ts +9 -1
- package/Server/Services/DockerSwarmClusterLabelRuleEngineService.ts +9 -1
- package/Server/Services/DockerSwarmClusterOwnerRuleEngineService.ts +9 -1
- package/Server/Services/HostLabelRuleEngineService.ts +9 -1
- package/Server/Services/HostOwnerRuleEngineService.ts +9 -1
- package/Server/Services/IncidentEpisodeLabelRuleEngineService.ts +9 -1
- package/Server/Services/IncidentEpisodeOnCallRuleEngineService.ts +9 -1
- package/Server/Services/IncidentEpisodeOwnerRuleEngineService.ts +9 -1
- package/Server/Services/IncidentEpisodePrivacyRuleEngineService.ts +9 -1
- package/Server/Services/IncidentGroupingEngineService.ts +9 -1
- package/Server/Services/IncidentLabelRuleEngineService.ts +9 -1
- package/Server/Services/IncidentOnCallRuleEngineService.ts +9 -1
- package/Server/Services/IncidentOwnerRuleEngineService.ts +9 -1
- package/Server/Services/IncidentPrivacyRuleEngineService.ts +9 -1
- package/Server/Services/IncidentReminderRuleService.ts +9 -1
- package/Server/Services/IncidentSlaRuleService.ts +9 -1
- package/Server/Services/IncomingCallPolicyLabelRuleEngineService.ts +9 -1
- package/Server/Services/IncomingCallPolicyOwnerRuleEngineService.ts +9 -1
- package/Server/Services/Index.ts +14 -0
- package/Server/Services/IoTFleetLabelRuleEngineService.ts +9 -1
- package/Server/Services/IoTFleetOwnerRuleEngineService.ts +9 -1
- package/Server/Services/KubernetesClusterLabelRuleEngineService.ts +9 -1
- package/Server/Services/KubernetesClusterOwnerRuleEngineService.ts +9 -1
- package/Server/Services/KubernetesResourceService.ts +5 -1
- package/Server/Services/MonitorLabelRuleEngineService.ts +9 -1
- package/Server/Services/MonitorOwnerRuleEngineService.ts +9 -1
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +35 -0
- package/Server/Services/NetworkDeviceAutoImportRuleService.ts +96 -0
- package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +9 -1
- package/Server/Services/NetworkDeviceOidTemplateService.ts +201 -0
- package/Server/Services/NetworkDeviceOwnerRuleEngineService.ts +9 -1
- package/Server/Services/NetworkDeviceService.ts +209 -1
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
- package/Server/Services/OnCallDutyPolicyLabelRuleEngineService.ts +9 -1
- package/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.ts +9 -1
- package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
- package/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.ts +9 -1
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
- package/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.ts +9 -1
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
- package/Server/Services/PodmanHostLabelRuleEngineService.ts +9 -1
- package/Server/Services/PodmanHostOwnerRuleEngineService.ts +9 -1
- package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
- package/Server/Services/ProxmoxClusterLabelRuleEngineService.ts +9 -1
- package/Server/Services/ProxmoxClusterOwnerRuleEngineService.ts +9 -1
- package/Server/Services/RumApplicationLabelRuleEngineService.ts +9 -1
- package/Server/Services/RumApplicationOwnerRuleEngineService.ts +9 -1
- package/Server/Services/RunbookLabelRuleEngineService.ts +9 -1
- package/Server/Services/RunbookOwnerRuleEngineService.ts +9 -1
- package/Server/Services/RunbookRuleEngineService.ts +9 -1
- package/Server/Services/ScheduledMaintenanceLabelRuleEngineService.ts +9 -1
- package/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.ts +9 -1
- package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +9 -1
- package/Server/Services/ServerlessFunctionLabelRuleEngineService.ts +9 -1
- package/Server/Services/ServerlessFunctionOwnerRuleEngineService.ts +9 -1
- package/Server/Services/ServiceLabelRuleEngineService.ts +9 -1
- package/Server/Services/ServiceOwnerRuleEngineService.ts +9 -1
- package/Server/Services/StatusPageLabelRuleEngineService.ts +9 -1
- package/Server/Services/StatusPageMonitorRuleEngineService.ts +7 -0
- package/Server/Services/StatusPageOwnerRuleEngineService.ts +9 -1
- package/Server/Services/TeamMemberService.ts +489 -1
- package/Server/Services/TeamService.ts +50 -0
- package/Server/Services/TelemetrySourceMapService.ts +266 -23
- package/Server/Services/UserNotificationSettingService.ts +44 -1
- package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
- package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
- package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
- package/Server/Services/UserService.ts +27 -0
- package/Server/Services/WorkflowLabelRuleEngineService.ts +9 -1
- package/Server/Services/WorkflowOwnerRuleEngineService.ts +9 -1
- package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +186 -0
- package/Server/Utils/Monitor/CapturedMetricAttributeUtil.ts +245 -0
- package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +21 -2
- package/Server/Utils/Monitor/MonitorMetricUtil.ts +55 -21
- package/Server/Utils/Monitor/MonitorTemplateUtil.ts +32 -0
- package/Server/Utils/Monitor/NetworkDeviceMetricUtil.ts +15 -2
- package/Server/Utils/Monitor/NetworkDeviceWalkUtil.ts +18 -1
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +10 -0
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +34 -0
- package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
- package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
- package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
- package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
- package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
- package/Server/Utils/Response.ts +134 -0
- package/Server/Utils/Rules/RuleEngineRuleRead.ts +48 -0
- package/Server/Utils/StartServer.ts +10 -0
- package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
- package/Server/Utils/Telemetry/ExceptionSanitizer.ts +14 -1
- package/Server/Utils/Telemetry/LogExceptionExtractor.ts +14 -2
- package/Server/Utils/Telemetry/SourceMapResolver.ts +16 -5
- package/Server/Utils/Telemetry/StackTraceParser.ts +399 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +125 -0
- package/Tests/App/Dashboard/HeaderImageAssets.test.tsx +93 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
- package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +3 -3
- package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
- package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
- package/Tests/App/Dashboard/UserElementEmail.test.tsx +12 -4
- package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +1 -1
- package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +14 -0
- package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +469 -0
- package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
- package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
- package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
- package/Tests/Models/SecurityDomainAccessControl.test.ts +429 -0
- package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
- package/Tests/Server/API/GlobalConfigLicense.test.ts +657 -0
- package/Tests/Server/API/Helpers.ts +8 -0
- package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
- package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +84 -6
- package/Tests/Server/API/TelemetryResolveStackTraceAPI.test.ts +805 -0
- package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
- package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
- package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +394 -0
- package/Tests/Server/Middleware/IdentityRateLimit.test.ts +1144 -0
- package/Tests/Server/Middleware/MultipartFormData.test.ts +618 -3
- package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
- package/Tests/Server/Services/MonitorLabelRuleEngineScale.test.ts +520 -0
- package/Tests/Server/Services/NetworkDeviceOidTemplateService.test.ts +332 -0
- package/Tests/Server/Services/NetworkDeviceOidTemplateWriteGuards.test.ts +409 -0
- package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
- package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
- package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
- package/Tests/Server/Services/RuleEngineRuleFetchLimit.test.ts +239 -0
- package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
- package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
- package/Tests/Server/Services/TelemetrySourceMapService.test.ts +1210 -2
- package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
- package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
- package/Tests/Server/Services/UserServiceSeatLimit.test.ts +182 -0
- package/Tests/Server/Types/AnalyticsDatabase/SecurityEventModelPermission.test.ts +255 -0
- package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
- package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +212 -0
- package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +413 -0
- package/Tests/Server/Utils/CodeRepository.test.ts +689 -0
- package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +495 -0
- package/Tests/Server/Utils/ExceptionSanitizer.test.ts +77 -0
- package/Tests/Server/Utils/Monitor/CapturedMetricAttributeUtil.test.ts +284 -0
- package/Tests/Server/Utils/Monitor/MonitorCustomMetricAttributes.test.ts +503 -0
- package/Tests/Server/Utils/Monitor/MonitorTemplatePrototypePollution.test.ts +106 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceWalkLogWritePath.test.ts +33 -4
- package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +14 -4
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +44 -0
- package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
- package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
- package/Tests/Server/Utils/OpenAPISpec.test.ts +536 -0
- package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
- package/Tests/Server/Utils/Rules/RuleEngineRuleRead.test.ts +115 -0
- package/Tests/Server/Utils/StackTraceParser.test.ts +610 -0
- package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
- package/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.ts +0 -0
- package/Tests/Server/Utils/Telemetry/SourceMapLimits.test.ts +656 -0
- package/Tests/Server/Utils/Telemetry/SourceMapResolver.test.ts +100 -0
- package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
- package/Tests/Types/Monitor/SnmpMonitor/SnmpOidListUtil.test.ts +454 -0
- package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
- package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
- package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
- package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
- package/Tests/Types/SecurityRolePermissions.test.ts +193 -0
- package/Tests/UI/Components/Charts/AnnotationLayout.test.ts +773 -0
- package/Tests/UI/Components/Charts/ChartAnnotationRail.test.tsx +789 -0
- package/Tests/UI/Components/Charts/ChartBucketIdentity.test.ts +254 -0
- package/Tests/UI/Components/Charts/TimeAnnotation.test.ts +471 -0
- package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +405 -0
- package/Tests/UI/Components/ErrorMessageRefreshControl.test.tsx +83 -0
- package/Tests/UI/Components/MarkdownConverters.test.ts +686 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableDeleteAndEmptyStates.test.tsx +341 -0
- package/Tests/UI/Components/OrderedStatesList.test.tsx +5 -5
- package/Tests/UI/Components/PendingProjectInvitations.test.tsx +1 -1
- package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +195 -0
- package/Tests/UI/Components/XAxis.test.ts +270 -5
- package/Tests/UI/Components/XAxisDaylightSaving.test.ts +113 -0
- package/Tests/UI/Styles/ChartFocusRing.test.ts +153 -0
- package/Tests/UI/Styles/ChartFocusRingCoverage.test.tsx +377 -0
- package/Tests/UI/Styles/ThemeStylesheet.ts +162 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/Utils/EnterpriseLicenseSeats.test.ts +507 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +951 -1
- package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +238 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +1056 -0
- package/Tests/Utils/NetworkDiscovery/ReverseDnsNameUtil.test.ts +882 -0
- package/Types/Calendar/ICalendar.ts +442 -0
- package/Types/Date.ts +39 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/Monitor/SnmpMonitor/SnmpOidListUtil.ts +333 -0
- package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
- package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
- package/Types/OnCallDutyPolicy/Layer.ts +190 -35
- package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
- package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
- package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
- package/Types/Permission.ts +96 -0
- package/Types/Rum/SessionReplay.ts +21 -0
- package/Types/Telemetry/SourceMap.ts +8 -0
- package/UI/Components/Charts/ChartLibrary/Annotations/AnnotationHoverCard.tsx +176 -0
- package/UI/Components/Charts/ChartLibrary/Annotations/AnnotationLayout.ts +745 -0
- package/UI/Components/Charts/ChartLibrary/Annotations/ChartAnnotationLayer.tsx +576 -0
- package/UI/Components/Charts/ChartLibrary/Annotations/UseChartAnnotations.tsx +204 -0
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +491 -544
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +324 -372
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +487 -540
- package/UI/Components/Charts/ChartLibrary/Types/FormattedReferenceRegion.ts +7 -3
- package/UI/Components/Charts/ChartLibrary/Types/FormattedTimeReferenceLine.ts +14 -3
- package/UI/Components/Charts/Types/ChartEventKind.ts +13 -0
- package/UI/Components/Charts/Types/ReferenceRegionProps.ts +6 -0
- package/UI/Components/Charts/Types/TimeReferenceLineProps.ts +20 -3
- package/UI/Components/Charts/Utils/TimeAnnotation.ts +69 -35
- package/UI/Components/Charts/Utils/XAxis.ts +125 -18
- package/UI/Components/EditionLabel/EditionLabel.tsx +280 -26
- package/UI/Components/ErrorMessage/ErrorMessage.tsx +21 -10
- package/UI/Components/Feed/FeedItem.tsx +4 -5
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -2
- package/UI/Components/Header/UserProfile/UserProfile.tsx +1 -2
- package/UI/Components/Image/Image.tsx +20 -3
- package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
- package/UI/Components/MemberRoleAssignment/MemberRoleAssignment.tsx +1 -2
- package/UI/Components/ModelTable/BaseModelTable.tsx +150 -24
- package/UI/Components/Table/TableHeader.tsx +64 -48
- package/UI/Components/Table/TableRow.tsx +13 -3
- package/UI/Styles/Theme.css +38 -0
- package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +271 -0
- package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +6 -0
- package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +141 -10
- package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +55 -3
- package/Utils/NetworkDiscovery/ReverseDnsNameUtil.ts +151 -0
- package/Utils/Rules/RuleEngineLimits.ts +32 -0
- package/build/dist/Models/AnalyticsModels/SecurityEvent.js +48 -16
- package/build/dist/Models/AnalyticsModels/SecurityEvent.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ThreatIntelIndicator.js +10 -5
- package/build/dist/Models/AnalyticsModels/ThreatIntelIndicator.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertSeverity.js +46 -1
- package/build/dist/Models/DatabaseModels/AlertSeverity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DetectionRule.js +16 -5
- package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js +14 -5
- package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentSeverity.js +46 -1
- package/build/dist/Models/DatabaseModels/IncidentSeverity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +13 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +115 -3
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +88 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +519 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ThreatIntelFeed.js +16 -5
- package/build/dist/Models/DatabaseModels/ThreatIntelFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
- package/build/dist/Server/API/GlobalConfigAPI.js +293 -176
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
- package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
- package/build/dist/Server/API/TelemetryAPI.js +31 -3
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +185 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790500000000-AddNetworkDeviceOidTemplate.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790500000000-AddNetworkDeviceOidTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790600000000-AddAutoImportRuleOidTemplate.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790600000000-AddAutoImportRuleOidTemplate.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/MultipartFormData.js +93 -28
- package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
- package/build/dist/Server/Services/AlertEpisodeLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertEpisodeLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeOnCallRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertEpisodeOnCallRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertEpisodeOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodePrivacyRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertEpisodePrivacyRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertGroupingEngineService.js +8 -1
- package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertOnCallRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertOnCallRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertPrivacyRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AlertPrivacyRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AlertReminderRuleService.js +8 -1
- package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +8 -1
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/DashboardLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/DashboardLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/DashboardOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/DashboardOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/HostLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/HostLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/HostOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/HostOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentEpisodeLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeOnCallRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentEpisodeOnCallRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentEpisodeOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePrivacyRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentEpisodePrivacyRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentGroupingEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentOnCallRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentOnCallRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentPrivacyRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncidentPrivacyRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentReminderRuleService.js +8 -1
- package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncidentSlaRuleService.js +8 -1
- package/build/dist/Server/Services/IncidentSlaRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +14 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +5 -1
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/MonitorLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/MonitorLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/MonitorOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +30 -21
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +64 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceOidTemplateService.js +180 -0
- package/build/dist/Server/Services/NetworkDeviceOidTemplateService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +154 -1
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/RunbookLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/RunbookLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/RunbookOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/RunbookOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/RunbookRuleEngineService.js +8 -1
- package/build/dist/Server/Services/RunbookRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +8 -1
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ServiceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/StatusPageLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +6 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/StatusPageOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +396 -1
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TeamService.js +43 -0
- package/build/dist/Server/Services/TeamService.js.map +1 -1
- package/build/dist/Server/Services/TelemetrySourceMapService.js +213 -19
- package/build/dist/Server/Services/TelemetrySourceMapService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
- package/build/dist/Server/Services/UserService.js +25 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/WorkflowLabelRuleEngineService.js +8 -1
- package/build/dist/Server/Services/WorkflowLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/WorkflowOwnerRuleEngineService.js +8 -1
- package/build/dist/Server/Services/WorkflowOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +145 -0
- package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/CapturedMetricAttributeUtil.js +201 -0
- package/build/dist/Server/Utils/Monitor/CapturedMetricAttributeUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +19 -2
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +42 -23
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +29 -0
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js +11 -2
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js +16 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +10 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +33 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
- package/build/dist/Server/Utils/Response.js +97 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/Rules/RuleEngineRuleRead.js +37 -0
- package/build/dist/Server/Utils/Rules/RuleEngineRuleRead.js.map +1 -0
- package/build/dist/Server/Utils/StartServer.js +9 -1
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +12 -1
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +14 -2
- package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/SourceMapResolver.js +15 -5
- package/build/dist/Server/Utils/Telemetry/SourceMapResolver.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/StackTraceParser.js +307 -0
- package/build/dist/Server/Utils/Telemetry/StackTraceParser.js.map +1 -1
- package/build/dist/Types/Calendar/ICalendar.js +290 -0
- package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
- package/build/dist/Types/Date.js +27 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpOidListUtil.js +253 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpOidListUtil.js.map +1 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
- package/build/dist/Types/Permission.js +86 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +19 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationHoverCard.js +83 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationHoverCard.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationLayout.js +531 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/AnnotationLayout.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/ChartAnnotationLayer.js +264 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/ChartAnnotationLayer.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/UseChartAnnotations.js +122 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Annotations/UseChartAnnotations.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +213 -233
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +139 -152
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +212 -232
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Types/ChartEventKind.js +14 -0
- package/build/dist/UI/Components/Charts/Types/ChartEventKind.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js +57 -28
- package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/XAxis.js +103 -17
- package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +198 -23
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js +15 -5
- package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js.map +1 -1
- package/build/dist/UI/Components/Feed/FeedItem.js +4 -5
- package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -2
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Header/UserProfile/UserProfile.js.map +1 -1
- package/build/dist/UI/Components/Image/Image.js +6 -1
- package/build/dist/UI/Components/Image/Image.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
- package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js +1 -2
- package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +104 -23
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +20 -22
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +13 -4
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +153 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +127 -7
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +50 -3
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/ReverseDnsNameUtil.js +132 -0
- package/build/dist/Utils/NetworkDiscovery/ReverseDnsNameUtil.js.map +1 -0
- package/build/dist/Utils/Rules/RuleEngineLimits.js +31 -0
- package/build/dist/Utils/Rules/RuleEngineLimits.js.map +1 -0
- package/jest.config.json +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,2194 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DisableOnCallCalendarFeed,
|
|
3
|
+
Host,
|
|
4
|
+
HttpProtocol,
|
|
5
|
+
ProvisionSsl,
|
|
6
|
+
TrustedProxyHops,
|
|
7
|
+
} from "../EnvironmentConfig";
|
|
8
|
+
import OnCallCalendarFeedCache from "../Infrastructure/OnCallCalendarFeedCache";
|
|
9
|
+
import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
|
|
10
|
+
import OnCallCalendarFeedRateLimit from "../Middleware/OnCallCalendarFeedRateLimit";
|
|
11
|
+
import UserMiddleware from "../Middleware/UserAuthorization";
|
|
12
|
+
import OnCallDutyPolicyScheduleCalendarFeedService from "../Services/OnCallDutyPolicyScheduleCalendarFeedService";
|
|
13
|
+
import OnCallDutyPolicyScheduleService from "../Services/OnCallDutyPolicyScheduleService";
|
|
14
|
+
import ProjectOnCallCalendarFeedService from "../Services/ProjectOnCallCalendarFeedService";
|
|
15
|
+
import UserOnCallCalendarFeedService from "../Services/UserOnCallCalendarFeedService";
|
|
16
|
+
import Express, {
|
|
17
|
+
ExpressRequest,
|
|
18
|
+
ExpressResponse,
|
|
19
|
+
ExpressRouter,
|
|
20
|
+
NextFunction,
|
|
21
|
+
} from "../Utils/Express";
|
|
22
|
+
import logger from "../Utils/Logger";
|
|
23
|
+
import CalendarFeedToken, {
|
|
24
|
+
CalendarFeedRotation,
|
|
25
|
+
MintedCalendarFeedToken,
|
|
26
|
+
} from "../Utils/OnCall/CalendarFeedToken";
|
|
27
|
+
import OnCallCalendarFeedRenderer, {
|
|
28
|
+
FEED_DISABLED_REASON,
|
|
29
|
+
FeedRenderOutcome,
|
|
30
|
+
FeedRenderRequest,
|
|
31
|
+
FeedRenderStatus,
|
|
32
|
+
TOKEN_ROTATED_REASON,
|
|
33
|
+
UserShiftsResult,
|
|
34
|
+
} from "../Utils/OnCall/OnCallCalendarFeedRenderer";
|
|
35
|
+
import OnCallCalendarFeedUrls, {
|
|
36
|
+
FeedUrls,
|
|
37
|
+
} from "../Utils/OnCall/OnCallCalendarFeedUrls";
|
|
38
|
+
import Response from "../Utils/Response";
|
|
39
|
+
import CommonAPI from "./CommonAPI";
|
|
40
|
+
import OnCallDutyPolicySchedule from "../../Models/DatabaseModels/OnCallDutyPolicySchedule";
|
|
41
|
+
import OnCallDutyPolicyScheduleCalendarFeed from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed";
|
|
42
|
+
import ProjectOnCallCalendarFeed from "../../Models/DatabaseModels/ProjectOnCallCalendarFeed";
|
|
43
|
+
import UserOnCallCalendarFeed from "../../Models/DatabaseModels/UserOnCallCalendarFeed";
|
|
44
|
+
import Protocol from "../../Types/API/Protocol";
|
|
45
|
+
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
46
|
+
import OneUptimeDate from "../../Types/Date";
|
|
47
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
48
|
+
import Exception from "../../Types/Exception/Exception";
|
|
49
|
+
import ExceptionCode from "../../Types/Exception/ExceptionCode";
|
|
50
|
+
import NotFoundException from "../../Types/Exception/NotFoundException";
|
|
51
|
+
import ServiceUnavailableException from "../../Types/Exception/ServiceUnavailableException";
|
|
52
|
+
import { JSONObject } from "../../Types/JSON";
|
|
53
|
+
import ObjectID from "../../Types/ObjectID";
|
|
54
|
+
import {
|
|
55
|
+
DEFAULT_FUTURE_DAYS,
|
|
56
|
+
DEFAULT_PAST_DAYS,
|
|
57
|
+
MAX_FUTURE_DAYS,
|
|
58
|
+
MAX_PAST_DAYS,
|
|
59
|
+
} from "../../Types/OnCallDutyPolicy/CalendarFeedWindow";
|
|
60
|
+
import MaterializedShiftUtil from "../../Types/OnCallDutyPolicy/MaterializedShift";
|
|
61
|
+
import { OnCallCalendarFeedKind } from "../../Types/OnCallDutyPolicy/OnCallCalendarFeedUtil";
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* The HTTP face of the on-call calendar feeds.
|
|
65
|
+
*
|
|
66
|
+
* Two very different kinds of route share this router, and the difference is
|
|
67
|
+
* the whole design:
|
|
68
|
+
*
|
|
69
|
+
* PUBLIC CAPABILITY ROUTES (no UserMiddleware). A calendar client sends no
|
|
70
|
+
* cookie and no bearer token; the 43-character token in the path IS the
|
|
71
|
+
* credential. UserMiddleware is deliberately absent: with no cookie it would
|
|
72
|
+
* add nothing, but a browser holding an expired session cookie would be
|
|
73
|
+
* answered 401 before the token was even read, and an `apikey` header -- even
|
|
74
|
+
* an empty one -- would hijack the request into API-key authentication. The
|
|
75
|
+
* pipeline is, in order: kill switch -> scheme guard -> rate limit -> shape
|
|
76
|
+
* guard -> token lookup (current hash, then the rotated-out hash inside its
|
|
77
|
+
* grace) -> the renderer, which re-derives authorisation on every fetch
|
|
78
|
+
* (a valid token is not authorisation) -> response -> bookkeeping.
|
|
79
|
+
*
|
|
80
|
+
* ONE RESPONSE RULE, applied everywhere:
|
|
81
|
+
* unknown / malformed / expired token -> 404, generic body
|
|
82
|
+
* disabled, rotated-out-in-grace, below plan,
|
|
83
|
+
* no eligible schedule -> 200, EMPTY VCALENDAR
|
|
84
|
+
* kill switch, or render cap with nothing cached -> 503 + Retry-After
|
|
85
|
+
* The empty-calendar cases are 200 on purpose: a client that gets 404 keeps
|
|
86
|
+
* showing the last copy it downloaded (or drops the subscription with an
|
|
87
|
+
* error), whereas an empty calendar makes it clear its copy.
|
|
88
|
+
*
|
|
89
|
+
* SESSION ROUTES (UserMiddleware + requireUserAuthentication). The settings
|
|
90
|
+
* pages and the mobile app read and rotate feeds here. /current is the ONLY
|
|
91
|
+
* place the encrypted `token` column is ever read back; the public routes
|
|
92
|
+
* never select it, so a rotated ENCRYPTION_SECRET can break the settings page
|
|
93
|
+
* (which says "Regenerate link") but never the subscribed calendars.
|
|
94
|
+
*
|
|
95
|
+
* The token never reaches a log line, an error message, a span attribute or
|
|
96
|
+
* a cache key. Everything that identifies a request in logs is the feed kind
|
|
97
|
+
* and the row id.
|
|
98
|
+
*/
|
|
99
|
+
const router: ExpressRouter = Express.getRouter();
|
|
100
|
+
|
|
101
|
+
// -- Routes (exported so tests and the dashboard pin the same strings) ------
|
|
102
|
+
|
|
103
|
+
export const PERSONAL_FEED_ROUTE: string =
|
|
104
|
+
"/on-call-calendar/user/:token/shifts.ics";
|
|
105
|
+
export const SCHEDULE_FEED_ROUTE: string =
|
|
106
|
+
"/on-call-calendar/schedule/:token/schedule.ics";
|
|
107
|
+
export const PROJECT_FEED_ROUTE: string =
|
|
108
|
+
"/on-call-calendar/project/:token/project.ics";
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* Anything else under a token-bearing prefix: a link pasted without the
|
|
112
|
+
* trailing `shifts.ics`, a typo in the filename, a client or a scanner
|
|
113
|
+
* sending POST/PUT/DELETE to a feed URL. Without these the request falls
|
|
114
|
+
* through to the application's own catch-alls, which answer
|
|
115
|
+
* `Page not found - ${req.url}` -- the plaintext token, at ERROR level, in
|
|
116
|
+
* stdout, in the master-admin support bundle's recent-log buffer and in the
|
|
117
|
+
* OTel log exporter. The token is the credential; it must never be logged.
|
|
118
|
+
*/
|
|
119
|
+
export const PERSONAL_FEED_FALLBACK_ROUTE: string = "/on-call-calendar/user/*";
|
|
120
|
+
export const SCHEDULE_FEED_FALLBACK_ROUTE: string =
|
|
121
|
+
"/on-call-calendar/schedule/*";
|
|
122
|
+
export const PROJECT_FEED_FALLBACK_ROUTE: string =
|
|
123
|
+
"/on-call-calendar/project/*";
|
|
124
|
+
|
|
125
|
+
export const FEED_CURRENT_ROUTE: string = "/on-call-calendar/feed/current";
|
|
126
|
+
export const FEED_ROTATE_ROUTE: string = "/on-call-calendar/feed/rotate";
|
|
127
|
+
|
|
128
|
+
export const SCHEDULE_FEED_CURRENT_ROUTE: string =
|
|
129
|
+
"/on-call-calendar/schedule-feed/:scheduleId/current";
|
|
130
|
+
export const SCHEDULE_FEED_PUBLISH_ROUTE: string =
|
|
131
|
+
"/on-call-calendar/schedule-feed/:scheduleId/publish";
|
|
132
|
+
export const SCHEDULE_FEED_ROTATE_ROUTE: string =
|
|
133
|
+
"/on-call-calendar/schedule-feed/:scheduleId/rotate";
|
|
134
|
+
|
|
135
|
+
export const PROJECT_FEED_CURRENT_ROUTE: string =
|
|
136
|
+
"/on-call-calendar/project-feed/current";
|
|
137
|
+
export const PROJECT_FEED_PUBLISH_ROUTE: string =
|
|
138
|
+
"/on-call-calendar/project-feed/publish";
|
|
139
|
+
export const PROJECT_FEED_ROTATE_ROUTE: string =
|
|
140
|
+
"/on-call-calendar/project-feed/rotate";
|
|
141
|
+
|
|
142
|
+
export const MY_SHIFTS_ROUTE: string = "/on-call-calendar/my-shifts";
|
|
143
|
+
|
|
144
|
+
// -- Constants --------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
export const KILL_SWITCH_RETRY_AFTER_SECONDS: number = 3600;
|
|
147
|
+
|
|
148
|
+
/* Bookkeeping is written at most this often per feed. */
|
|
149
|
+
export const BOOKKEEPING_INTERVAL_MS: number = 5 * 60 * 1000;
|
|
150
|
+
|
|
151
|
+
export const MY_SHIFTS_DEFAULT_DAYS: number = 30;
|
|
152
|
+
export const MY_SHIFTS_MAX_DAYS: number = 120;
|
|
153
|
+
|
|
154
|
+
export const ROTATE_LOCK_NAMESPACE: string = "OnCallCalendarFeed.rotate";
|
|
155
|
+
export const ROTATE_LOCK_TIMEOUT_MS: number = 10000;
|
|
156
|
+
|
|
157
|
+
const STALE_WARNING_HEADER: string = '110 - "Response is Stale"';
|
|
158
|
+
|
|
159
|
+
/* The body of every 404 on the public routes. Says nothing. */
|
|
160
|
+
const NOT_FOUND_MESSAGE: string = "Not found.";
|
|
161
|
+
|
|
162
|
+
// -- Wire shapes ------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
export interface FeedStatusSettings {
|
|
165
|
+
includeCoveringShifts?: boolean;
|
|
166
|
+
includeCoverageGaps?: boolean;
|
|
167
|
+
minimumGapMinutes?: number;
|
|
168
|
+
pastDays: number;
|
|
169
|
+
futureDays: number;
|
|
170
|
+
rotateWhenMemberLeaves?: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface FeedStatus {
|
|
174
|
+
exists: boolean;
|
|
175
|
+
feedId: string | null;
|
|
176
|
+
isEnabled: boolean;
|
|
177
|
+
needsRegeneration: boolean;
|
|
178
|
+
tokenHint: string | null;
|
|
179
|
+
rotatedAt: string | null;
|
|
180
|
+
previousTokenExpiresAt: string | null;
|
|
181
|
+
lastFetchedAt: string | null;
|
|
182
|
+
lastFetchedClient: string | null;
|
|
183
|
+
fetchCount: number;
|
|
184
|
+
lastRenderTruncated: boolean;
|
|
185
|
+
settings: FeedStatusSettings;
|
|
186
|
+
urls: FeedUrls | null;
|
|
187
|
+
hostWarning: string | null;
|
|
188
|
+
protocolWarning: string | null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface MyShiftsResponse {
|
|
192
|
+
shifts: Array<JSONObject>;
|
|
193
|
+
truncated: boolean;
|
|
194
|
+
generatedAt: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/*
|
|
198
|
+
* The columns every feed row shares, as the status builder reads them. The
|
|
199
|
+
* three models are structurally alike here; typing the slice keeps the
|
|
200
|
+
* builder free of casts.
|
|
201
|
+
*/
|
|
202
|
+
interface FeedRowLike {
|
|
203
|
+
id?: ObjectID | null | undefined;
|
|
204
|
+
_id?: string | undefined;
|
|
205
|
+
isEnabled?: boolean | undefined;
|
|
206
|
+
tokenHint?: string | undefined;
|
|
207
|
+
tokenHash?: string | undefined;
|
|
208
|
+
token?: string | undefined;
|
|
209
|
+
rotatedAt?: Date | undefined;
|
|
210
|
+
previousTokenExpiresAt?: Date | undefined;
|
|
211
|
+
lastFetchedAt?: Date | undefined;
|
|
212
|
+
lastFetchedClient?: string | undefined;
|
|
213
|
+
fetchCount?: number | undefined;
|
|
214
|
+
lastRenderTruncated?: boolean | undefined;
|
|
215
|
+
pastDays?: number | undefined;
|
|
216
|
+
futureDays?: number | undefined;
|
|
217
|
+
includeCoveringShifts?: boolean | undefined;
|
|
218
|
+
includeCoverageGaps?: boolean | undefined;
|
|
219
|
+
minimumGapMinutes?: number | undefined;
|
|
220
|
+
rotateWhenMemberLeaves?: boolean | undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
type AnyFeedRow =
|
|
224
|
+
| UserOnCallCalendarFeed
|
|
225
|
+
| OnCallDutyPolicyScheduleCalendarFeed
|
|
226
|
+
| ProjectOnCallCalendarFeed;
|
|
227
|
+
|
|
228
|
+
interface TokenLookupResult<TFeed> {
|
|
229
|
+
feed: TFeed;
|
|
230
|
+
/* Matched the rotated-out hash, still inside its grace period. */
|
|
231
|
+
viaPreviousToken: boolean;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/*
|
|
235
|
+
* 415. ExceptionCode has no member named for it; the numeric value is what
|
|
236
|
+
* res.status() needs and what the client sees.
|
|
237
|
+
*/
|
|
238
|
+
class UnsupportedMediaTypeException extends Exception {
|
|
239
|
+
public constructor(message: string) {
|
|
240
|
+
super(415 as ExceptionCode, message);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* 405, same reasoning as the 415 above. */
|
|
245
|
+
class MethodNotAllowedException extends Exception {
|
|
246
|
+
public constructor(message: string) {
|
|
247
|
+
super(405 as ExceptionCode, message);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// -- Public-route middleware ----------------------------------------------
|
|
252
|
+
|
|
253
|
+
/*
|
|
254
|
+
* Kill switch. Answered BEFORE the rate limiter so a switched-off install
|
|
255
|
+
* spends no Redis round trips per poll. 503 + Retry-After: clients keep the
|
|
256
|
+
* copy they have and come back later, which is the point of a switch that
|
|
257
|
+
* says "not now" rather than "never".
|
|
258
|
+
*/
|
|
259
|
+
export function killSwitchMiddleware(
|
|
260
|
+
req: ExpressRequest,
|
|
261
|
+
res: ExpressResponse,
|
|
262
|
+
next: NextFunction,
|
|
263
|
+
): void {
|
|
264
|
+
if (DisableOnCallCalendarFeed) {
|
|
265
|
+
res.set("Retry-After", String(KILL_SWITCH_RETRY_AFTER_SECONDS));
|
|
266
|
+
return Response.sendErrorResponse(
|
|
267
|
+
req,
|
|
268
|
+
res,
|
|
269
|
+
new ServiceUnavailableException(
|
|
270
|
+
"On-call calendar feeds are disabled on this OneUptime instance.",
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return next();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/*
|
|
279
|
+
* The X-Forwarded-Proto entry written by OUR proxy, read the way ClientIp
|
|
280
|
+
* reads X-Forwarded-For: counting in from the right by TRUSTED_PROXY_HOPS,
|
|
281
|
+
* never the leftmost (caller-supplied) entry. With no trusted proxies there
|
|
282
|
+
* is no trustworthy header at all, and undefined is returned.
|
|
283
|
+
*/
|
|
284
|
+
export function resolveTrustedForwardedProto(
|
|
285
|
+
req: ExpressRequest,
|
|
286
|
+
options?: { trustedProxyHops?: number | undefined } | undefined,
|
|
287
|
+
): string | undefined {
|
|
288
|
+
const configuredHops: number =
|
|
289
|
+
options?.trustedProxyHops === undefined
|
|
290
|
+
? TrustedProxyHops
|
|
291
|
+
: options.trustedProxyHops;
|
|
292
|
+
|
|
293
|
+
const hops: number =
|
|
294
|
+
Number.isFinite(configuredHops) && configuredHops > 0
|
|
295
|
+
? Math.floor(configuredHops)
|
|
296
|
+
: 0;
|
|
297
|
+
|
|
298
|
+
if (hops === 0) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const header: string | Array<string> | undefined = req.headers?.[
|
|
303
|
+
"x-forwarded-proto"
|
|
304
|
+
] as string | Array<string> | undefined;
|
|
305
|
+
|
|
306
|
+
if (header === undefined || header === null) {
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const raw: string = Array.isArray(header) ? header.join(",") : header;
|
|
311
|
+
|
|
312
|
+
if (!raw.trim()) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const entries: Array<string> = raw.split(",");
|
|
317
|
+
|
|
318
|
+
if (entries.length < hops) {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const entry: string | undefined = entries[entries.length - hops];
|
|
323
|
+
|
|
324
|
+
return entry ? entry.trim().toLowerCase() : undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/*
|
|
328
|
+
* Scheme guard. An https instance whose proxy reports the request arrived
|
|
329
|
+
* over plain http answers 301 to the https URL, so a link pasted without the
|
|
330
|
+
* "s" is corrected once instead of served in the clear forever. The target
|
|
331
|
+
* is built from HOST, never from the request's own Host header (an open
|
|
332
|
+
* redirect otherwise). Nginx only does this itself when billing is on.
|
|
333
|
+
*
|
|
334
|
+
* It only fires when OUR nginx terminates TLS (PROVISION_SSL=true). Every
|
|
335
|
+
* proxying location in Nginx/default.conf.template sets
|
|
336
|
+
* `X-Forwarded-Proto $scheme`, REPLACING whatever an outer proxy sent, so on
|
|
337
|
+
* an install that terminates TLS on an external reverse proxy
|
|
338
|
+
* (PROVISION_SSL=false with HTTP_PROTOCOL=https -- the topology
|
|
339
|
+
* config.example.env documents) nginx always reports `http` even though the
|
|
340
|
+
* client spoke https. Redirecting there would 301 to the very URL the client
|
|
341
|
+
* just asked for: an endless loop, ERR_TOO_MANY_REDIRECTS in a browser and a
|
|
342
|
+
* dead subscription in every calendar client. Serving the feed is the right
|
|
343
|
+
* answer; the settings page still shows `protocolWarning` when the install
|
|
344
|
+
* really is plain http.
|
|
345
|
+
*/
|
|
346
|
+
export function schemeGuardMiddleware(
|
|
347
|
+
req: ExpressRequest,
|
|
348
|
+
res: ExpressResponse,
|
|
349
|
+
next: NextFunction,
|
|
350
|
+
): void {
|
|
351
|
+
if (HttpProtocol !== Protocol.HTTPS) {
|
|
352
|
+
return next();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!ProvisionSsl) {
|
|
356
|
+
return next();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const forwardedProto: string | undefined = resolveTrustedForwardedProto(req);
|
|
360
|
+
|
|
361
|
+
if (forwardedProto !== "http") {
|
|
362
|
+
return next();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const host: string = OnCallCalendarFeedUrls.normalizeHost(Host);
|
|
366
|
+
|
|
367
|
+
if (!host) {
|
|
368
|
+
return next();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const originalUrl: string = req.originalUrl || req.url || "/";
|
|
372
|
+
|
|
373
|
+
res.set("Cache-Control", "no-store");
|
|
374
|
+
res.redirect(301, `${Protocol.HTTPS}${host}${originalUrl}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// -- Helpers ----------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
function sendNotFound(req: ExpressRequest, res: ExpressResponse): void {
|
|
380
|
+
return Response.sendErrorResponse(
|
|
381
|
+
req,
|
|
382
|
+
res,
|
|
383
|
+
new NotFoundException(NOT_FOUND_MESSAGE),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function sendUnavailable(
|
|
388
|
+
req: ExpressRequest,
|
|
389
|
+
res: ExpressResponse,
|
|
390
|
+
retryAfterSeconds: number,
|
|
391
|
+
): void {
|
|
392
|
+
res.set("Retry-After", String(retryAfterSeconds));
|
|
393
|
+
|
|
394
|
+
return Response.sendErrorResponse(
|
|
395
|
+
req,
|
|
396
|
+
res,
|
|
397
|
+
new ServiceUnavailableException(
|
|
398
|
+
"The calendar feed cannot be rendered right now. Please try again later.",
|
|
399
|
+
),
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/*
|
|
404
|
+
* The answer to every near miss on a token-bearing path. It says nothing
|
|
405
|
+
* about the URL it was asked for: the message is a constant, so neither the
|
|
406
|
+
* response body nor the error log line sendErrorResponse writes can carry
|
|
407
|
+
* the token. OPTIONS is answered the way OPTIONS should be; any other wrong
|
|
408
|
+
* method gets 405 + Allow (a client can fix that); a wrong or missing
|
|
409
|
+
* filename gets the same generic 404 an unknown token gets.
|
|
410
|
+
*/
|
|
411
|
+
export const FEED_ALLOWED_METHODS: string = "GET, HEAD, OPTIONS";
|
|
412
|
+
|
|
413
|
+
export function feedFallbackHandler(
|
|
414
|
+
req: ExpressRequest,
|
|
415
|
+
res: ExpressResponse,
|
|
416
|
+
): void {
|
|
417
|
+
const method: string = (req.method || "GET").toUpperCase();
|
|
418
|
+
|
|
419
|
+
if (method === "OPTIONS") {
|
|
420
|
+
res.set("Allow", FEED_ALLOWED_METHODS);
|
|
421
|
+
|
|
422
|
+
return Response.sendEmptySuccessResponse(req, res);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
426
|
+
res.set("Allow", FEED_ALLOWED_METHODS);
|
|
427
|
+
|
|
428
|
+
return Response.sendErrorResponse(
|
|
429
|
+
req,
|
|
430
|
+
res,
|
|
431
|
+
new MethodNotAllowedException(
|
|
432
|
+
"Calendar feeds are read with GET on the full feed URL.",
|
|
433
|
+
),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return sendNotFound(req, res);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/*
|
|
441
|
+
* The `?schedule=` filter of the personal feed. Absent -> no filter. Present
|
|
442
|
+
* but not a UUID -> null, which the route answers 404: a URL the user
|
|
443
|
+
* believed was filtered must never quietly render the whole feed.
|
|
444
|
+
*/
|
|
445
|
+
export function readScheduleFilter(
|
|
446
|
+
req: ExpressRequest,
|
|
447
|
+
): ObjectID | undefined | null {
|
|
448
|
+
const raw: unknown = req.query?.["schedule"];
|
|
449
|
+
|
|
450
|
+
if (raw === undefined || raw === null) {
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const value: string = Array.isArray(raw) ? String(raw[0] ?? "") : String(raw);
|
|
455
|
+
|
|
456
|
+
const trimmed: string = value.trim();
|
|
457
|
+
|
|
458
|
+
if (!trimmed) {
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!ObjectID.isValidUUID(trimmed)) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return new ObjectID(trimmed);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/*
|
|
470
|
+
* Coarse client family only, from the User-Agent. The raw string is never
|
|
471
|
+
* stored: it is high-cardinality, occasionally carries device identifiers,
|
|
472
|
+
* and the settings page only wants to say "Google Calendar, 12 minutes ago".
|
|
473
|
+
*/
|
|
474
|
+
export function classifyCalendarClient(
|
|
475
|
+
userAgent: string | Array<string> | undefined,
|
|
476
|
+
): string | null {
|
|
477
|
+
const raw: string = (
|
|
478
|
+
Array.isArray(userAgent) ? userAgent.join(" ") : userAgent || ""
|
|
479
|
+
).trim();
|
|
480
|
+
|
|
481
|
+
if (!raw) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const value: string = raw.toLowerCase();
|
|
486
|
+
|
|
487
|
+
if (value.includes("google-calendar") || value.includes("googlecalendar")) {
|
|
488
|
+
return "Google Calendar";
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (
|
|
492
|
+
value.includes("outlook") ||
|
|
493
|
+
value.includes("microsoft office") ||
|
|
494
|
+
value.includes("exchange") ||
|
|
495
|
+
value.includes("microsoft-cryptoapi")
|
|
496
|
+
) {
|
|
497
|
+
return "Microsoft Outlook";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (
|
|
501
|
+
value.includes("calendaragent") ||
|
|
502
|
+
value.includes("dataaccessd") ||
|
|
503
|
+
value.includes("ios/") ||
|
|
504
|
+
value.includes("macos/") ||
|
|
505
|
+
value.includes("darwin") ||
|
|
506
|
+
value.includes("ical")
|
|
507
|
+
) {
|
|
508
|
+
return "Apple Calendar";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (value.includes("thunderbird") || value.includes("lightning")) {
|
|
512
|
+
return "Thunderbird";
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (value.includes("okhttp") || value.includes("android")) {
|
|
516
|
+
return "Android app";
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (
|
|
520
|
+
value.includes("mozilla") ||
|
|
521
|
+
value.includes("chrome") ||
|
|
522
|
+
value.includes("safari") ||
|
|
523
|
+
value.includes("firefox") ||
|
|
524
|
+
value.includes("edge")
|
|
525
|
+
) {
|
|
526
|
+
return "Browser";
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (value.includes("curl") || value.includes("wget")) {
|
|
530
|
+
return "Command line";
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return "Other";
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function toDate(value: unknown): Date | null {
|
|
537
|
+
if (value instanceof Date) {
|
|
538
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (typeof value === "string" && value) {
|
|
542
|
+
const parsed: Date = OneUptimeDate.fromString(value);
|
|
543
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function toIso(value: unknown): string | null {
|
|
550
|
+
const date: Date | null = toDate(value);
|
|
551
|
+
return date ? date.toISOString() : null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/*
|
|
555
|
+
* Is a previousTokenHash match still honoured? Only while
|
|
556
|
+
* previousTokenExpiresAt lies in the future; a rotated-out hash past its
|
|
557
|
+
* grace is indistinguishable from an unknown one (404).
|
|
558
|
+
*/
|
|
559
|
+
export function isPreviousTokenInGrace(
|
|
560
|
+
feed: { previousTokenExpiresAt?: Date | undefined },
|
|
561
|
+
now: Date,
|
|
562
|
+
): boolean {
|
|
563
|
+
const expiresAt: Date | null = toDate(feed.previousTokenExpiresAt);
|
|
564
|
+
|
|
565
|
+
if (!expiresAt) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return expiresAt.getTime() > now.getTime();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/*
|
|
573
|
+
* Bookkeeping is only worth a write once in a while, and only for a real
|
|
574
|
+
* fetch: a HEAD is a client checking whether anything changed, not a read.
|
|
575
|
+
*/
|
|
576
|
+
export function shouldRecordFetch(data: {
|
|
577
|
+
method: string | undefined;
|
|
578
|
+
lastFetchedAt: Date | undefined | null;
|
|
579
|
+
now: Date;
|
|
580
|
+
}): boolean {
|
|
581
|
+
if ((data.method || "GET").toUpperCase() !== "GET") {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const last: Date | null = toDate(data.lastFetchedAt);
|
|
586
|
+
|
|
587
|
+
if (!last) {
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return data.now.getTime() - last.getTime() >= BOOKKEEPING_INTERVAL_MS;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function readObjectIdParam(req: ExpressRequest, name: string): ObjectID {
|
|
595
|
+
const raw: unknown = req.params?.[name];
|
|
596
|
+
|
|
597
|
+
if (typeof raw !== "string" || !ObjectID.isValidUUID(raw.trim())) {
|
|
598
|
+
throw new BadDataException(`${name} must be a valid id.`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return new ObjectID(raw.trim());
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/*
|
|
605
|
+
* POST bodies on the session routes must be application/json, else 415. The
|
|
606
|
+
* routes carry no payload, so this is not about parsing: a JSON content type
|
|
607
|
+
* cannot be produced by a cross-site form post, which is what keeps a
|
|
608
|
+
* "rotate my link" request from being triggered by a page the user did not
|
|
609
|
+
* mean to visit.
|
|
610
|
+
*/
|
|
611
|
+
export function assertJsonRequest(req: ExpressRequest): void {
|
|
612
|
+
const contentType: string | Array<string> | undefined = req.headers?.[
|
|
613
|
+
"content-type"
|
|
614
|
+
] as string | Array<string> | undefined;
|
|
615
|
+
|
|
616
|
+
const value: string = (
|
|
617
|
+
Array.isArray(contentType) ? contentType[0] || "" : contentType || ""
|
|
618
|
+
)
|
|
619
|
+
.trim()
|
|
620
|
+
.toLowerCase();
|
|
621
|
+
|
|
622
|
+
if (!value.startsWith("application/json")) {
|
|
623
|
+
throw new UnsupportedMediaTypeException(
|
|
624
|
+
"This endpoint expects a JSON request (Content-Type: application/json).",
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/*
|
|
630
|
+
* Parse ?from / ?to for /my-shifts. Defaults now -> +30 d; the span is
|
|
631
|
+
* capped at 120 d by moving `to` in, never by refusing the request.
|
|
632
|
+
*
|
|
633
|
+
* The window is then clamped into the same range the feeds themselves can
|
|
634
|
+
* address -- [now - MAX_PAST_DAYS, now + MAX_FUTURE_DAYS]. Without that,
|
|
635
|
+
* `?from=2500-01-01` was accepted: every distinct day-aligned window is a
|
|
636
|
+
* fresh schedule-cache entry AND a fresh LayerUtil expansion that walks one
|
|
637
|
+
* rotation period at a time from the layer's start to the window, which for a
|
|
638
|
+
* far-future start means the full 200,000-iteration cap per restricted layer
|
|
639
|
+
* -- seconds of synchronous CPU per request, holding one of the four
|
|
640
|
+
* per-process render slots the public feeds share. Clamping keeps a session
|
|
641
|
+
* request no more expensive than a feed poll and keeps it on the same cache
|
|
642
|
+
* keys.
|
|
643
|
+
*/
|
|
644
|
+
export function readMyShiftsWindow(
|
|
645
|
+
req: ExpressRequest,
|
|
646
|
+
now: Date,
|
|
647
|
+
): { from: Date; to: Date } {
|
|
648
|
+
const rawFrom: unknown = req.query?.["from"];
|
|
649
|
+
const rawTo: unknown = req.query?.["to"];
|
|
650
|
+
|
|
651
|
+
let from: Date = now;
|
|
652
|
+
|
|
653
|
+
if (typeof rawFrom === "string" && rawFrom.trim()) {
|
|
654
|
+
if (!OneUptimeDate.isValidDateString(rawFrom.trim())) {
|
|
655
|
+
throw new BadDataException("from must be an ISO 8601 date.");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
from = OneUptimeDate.fromString(rawFrom.trim());
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
let to: Date = OneUptimeDate.addRemoveDays(from, MY_SHIFTS_DEFAULT_DAYS);
|
|
662
|
+
|
|
663
|
+
if (typeof rawTo === "string" && rawTo.trim()) {
|
|
664
|
+
if (!OneUptimeDate.isValidDateString(rawTo.trim())) {
|
|
665
|
+
throw new BadDataException("to must be an ISO 8601 date.");
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
to = OneUptimeDate.fromString(rawTo.trim());
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) {
|
|
672
|
+
throw new BadDataException("from and to must be ISO 8601 dates.");
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (to.getTime() <= from.getTime()) {
|
|
676
|
+
throw new BadDataException("to must be after from.");
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/*
|
|
680
|
+
* The addressable range. It is 240 days wide, so it always leaves room for
|
|
681
|
+
* a non-empty window whatever the caller asked for.
|
|
682
|
+
*/
|
|
683
|
+
const earliestFrom: Date = OneUptimeDate.addRemoveDays(now, -MAX_PAST_DAYS);
|
|
684
|
+
const latestTo: Date = OneUptimeDate.addRemoveDays(now, MAX_FUTURE_DAYS);
|
|
685
|
+
const latestFrom: Date = OneUptimeDate.addRemoveDays(latestTo, -1);
|
|
686
|
+
|
|
687
|
+
if (from.getTime() < earliestFrom.getTime()) {
|
|
688
|
+
from = earliestFrom;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (from.getTime() > latestFrom.getTime()) {
|
|
692
|
+
from = latestFrom;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/*
|
|
696
|
+
* `to` was validated against the RAW `from`; once `from` has moved, a `to`
|
|
697
|
+
* that now lies at or before it is answered with the default span rather
|
|
698
|
+
* than an empty window.
|
|
699
|
+
*/
|
|
700
|
+
if (to.getTime() <= from.getTime()) {
|
|
701
|
+
to = OneUptimeDate.addRemoveDays(from, MY_SHIFTS_DEFAULT_DAYS);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const spanCap: Date = OneUptimeDate.addRemoveDays(from, MY_SHIFTS_MAX_DAYS);
|
|
705
|
+
|
|
706
|
+
if (to.getTime() > spanCap.getTime()) {
|
|
707
|
+
to = spanCap;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (to.getTime() > latestTo.getTime()) {
|
|
711
|
+
to = latestTo;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return { from, to };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// -- FeedStatus -------------------------------------------------------------
|
|
718
|
+
|
|
719
|
+
function defaultSettings(kind: OnCallCalendarFeedKind): FeedStatusSettings {
|
|
720
|
+
if (kind === OnCallCalendarFeedKind.Personal) {
|
|
721
|
+
return {
|
|
722
|
+
includeCoveringShifts: true,
|
|
723
|
+
pastDays: DEFAULT_PAST_DAYS,
|
|
724
|
+
futureDays: DEFAULT_FUTURE_DAYS,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
includeCoverageGaps: false,
|
|
730
|
+
minimumGapMinutes: 60,
|
|
731
|
+
pastDays: DEFAULT_PAST_DAYS,
|
|
732
|
+
futureDays: DEFAULT_FUTURE_DAYS,
|
|
733
|
+
rotateWhenMemberLeaves: false,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/*
|
|
738
|
+
* FeedStatus for a feed that does not exist (yet).
|
|
739
|
+
*/
|
|
740
|
+
export function buildAbsentFeedStatus(
|
|
741
|
+
kind: OnCallCalendarFeedKind,
|
|
742
|
+
): FeedStatus {
|
|
743
|
+
return {
|
|
744
|
+
exists: false,
|
|
745
|
+
feedId: null,
|
|
746
|
+
isEnabled: false,
|
|
747
|
+
needsRegeneration: false,
|
|
748
|
+
tokenHint: null,
|
|
749
|
+
rotatedAt: null,
|
|
750
|
+
previousTokenExpiresAt: null,
|
|
751
|
+
lastFetchedAt: null,
|
|
752
|
+
lastFetchedClient: null,
|
|
753
|
+
fetchCount: 0,
|
|
754
|
+
lastRenderTruncated: false,
|
|
755
|
+
settings: defaultSettings(kind),
|
|
756
|
+
urls: null,
|
|
757
|
+
hostWarning: OnCallCalendarFeedUrls.getHostWarning(),
|
|
758
|
+
protocolWarning: OnCallCalendarFeedUrls.getProtocolWarning(),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/*
|
|
763
|
+
* FeedStatus for an existing row. `plaintextToken` is the token just minted
|
|
764
|
+
* by a rotate/publish (known without a decrypt); otherwise `decryptedToken`
|
|
765
|
+
* is what the root read handed back, verified against the stored hash. A
|
|
766
|
+
* token that fails to decrypt or does not hash to tokenHash means the
|
|
767
|
+
* ENCRYPTION_SECRET changed under the row: the URL cannot be shown and the
|
|
768
|
+
* UI offers "Regenerate link".
|
|
769
|
+
*
|
|
770
|
+
* The hash the decrypted token is checked against is `verifiedTokenHash` --
|
|
771
|
+
* the one readTokenForStatus read alongside the token -- falling back to the
|
|
772
|
+
* row's own `tokenHash`. The status rows the routes read do NOT carry
|
|
773
|
+
* tokenHash (STATUS_SELECT cannot select it: the column's read access list is
|
|
774
|
+
* empty, so the non-root schedule/project reads would be refused), and
|
|
775
|
+
* requiring it here is what made every /current and /publish answer
|
|
776
|
+
* `needsRegeneration: true, urls: null` for a perfectly good feed.
|
|
777
|
+
*/
|
|
778
|
+
export function buildFeedStatus(data: {
|
|
779
|
+
kind: OnCallCalendarFeedKind;
|
|
780
|
+
feed: FeedRowLike;
|
|
781
|
+
plaintextToken?: string | undefined;
|
|
782
|
+
decryptedToken?: string | undefined;
|
|
783
|
+
decryptFailed?: boolean | undefined;
|
|
784
|
+
verifiedTokenHash?: string | undefined;
|
|
785
|
+
}): FeedStatus {
|
|
786
|
+
const feed: FeedRowLike = data.feed;
|
|
787
|
+
|
|
788
|
+
const expectedTokenHash: string | undefined =
|
|
789
|
+
feed.tokenHash || data.verifiedTokenHash;
|
|
790
|
+
|
|
791
|
+
let token: string | null = null;
|
|
792
|
+
let needsRegeneration: boolean = false;
|
|
793
|
+
|
|
794
|
+
if (
|
|
795
|
+
data.plaintextToken &&
|
|
796
|
+
CalendarFeedToken.isValidShape(data.plaintextToken)
|
|
797
|
+
) {
|
|
798
|
+
token = data.plaintextToken;
|
|
799
|
+
} else if (data.decryptFailed) {
|
|
800
|
+
needsRegeneration = true;
|
|
801
|
+
} else if (
|
|
802
|
+
data.decryptedToken &&
|
|
803
|
+
CalendarFeedToken.isValidShape(data.decryptedToken) &&
|
|
804
|
+
expectedTokenHash &&
|
|
805
|
+
CalendarFeedToken.isHashEqual(
|
|
806
|
+
CalendarFeedToken.hash(data.decryptedToken),
|
|
807
|
+
expectedTokenHash,
|
|
808
|
+
)
|
|
809
|
+
) {
|
|
810
|
+
token = data.decryptedToken;
|
|
811
|
+
} else {
|
|
812
|
+
needsRegeneration = true;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const settings: FeedStatusSettings =
|
|
816
|
+
data.kind === OnCallCalendarFeedKind.Personal
|
|
817
|
+
? {
|
|
818
|
+
includeCoveringShifts: feed.includeCoveringShifts !== false,
|
|
819
|
+
pastDays: feed.pastDays ?? DEFAULT_PAST_DAYS,
|
|
820
|
+
futureDays: feed.futureDays ?? DEFAULT_FUTURE_DAYS,
|
|
821
|
+
}
|
|
822
|
+
: {
|
|
823
|
+
includeCoverageGaps: feed.includeCoverageGaps === true,
|
|
824
|
+
minimumGapMinutes: feed.minimumGapMinutes ?? 60,
|
|
825
|
+
pastDays: feed.pastDays ?? DEFAULT_PAST_DAYS,
|
|
826
|
+
futureDays: feed.futureDays ?? DEFAULT_FUTURE_DAYS,
|
|
827
|
+
rotateWhenMemberLeaves: feed.rotateWhenMemberLeaves === true,
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
const feedId: string | null = feed.id
|
|
831
|
+
? feed.id.toString()
|
|
832
|
+
: feed._id
|
|
833
|
+
? feed._id.toString()
|
|
834
|
+
: null;
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
exists: true,
|
|
838
|
+
feedId,
|
|
839
|
+
isEnabled: feed.isEnabled !== false,
|
|
840
|
+
needsRegeneration,
|
|
841
|
+
tokenHint: feed.tokenHint || null,
|
|
842
|
+
rotatedAt: toIso(feed.rotatedAt),
|
|
843
|
+
previousTokenExpiresAt: toIso(feed.previousTokenExpiresAt),
|
|
844
|
+
lastFetchedAt: toIso(feed.lastFetchedAt),
|
|
845
|
+
lastFetchedClient: feed.lastFetchedClient || null,
|
|
846
|
+
fetchCount: typeof feed.fetchCount === "number" ? feed.fetchCount : 0,
|
|
847
|
+
lastRenderTruncated: feed.lastRenderTruncated === true,
|
|
848
|
+
settings,
|
|
849
|
+
urls: token
|
|
850
|
+
? OnCallCalendarFeedUrls.buildFeedUrls({ kind: data.kind, token })
|
|
851
|
+
: null,
|
|
852
|
+
hostWarning: OnCallCalendarFeedUrls.getHostWarning(),
|
|
853
|
+
protocolWarning: OnCallCalendarFeedUrls.getProtocolWarning(),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/*
|
|
858
|
+
* The one decrypting read. Selecting `token` makes DatabaseService run
|
|
859
|
+
* Encryption.decrypt on the way out; with the wrong secret that either
|
|
860
|
+
* throws or yields bytes that do not hash to tokenHash. Both become
|
|
861
|
+
* needsRegeneration, never a 500.
|
|
862
|
+
*/
|
|
863
|
+
async function readTokenForStatus(data: {
|
|
864
|
+
kind: OnCallCalendarFeedKind;
|
|
865
|
+
id: ObjectID;
|
|
866
|
+
}): Promise<{
|
|
867
|
+
decryptedToken?: string | undefined;
|
|
868
|
+
/* The tokenHash this read verified the token against. */
|
|
869
|
+
verifiedTokenHash?: string | undefined;
|
|
870
|
+
decryptFailed: boolean;
|
|
871
|
+
}> {
|
|
872
|
+
try {
|
|
873
|
+
let row: FeedRowLike | null = null;
|
|
874
|
+
|
|
875
|
+
if (data.kind === OnCallCalendarFeedKind.Personal) {
|
|
876
|
+
row = await UserOnCallCalendarFeedService.findOneById({
|
|
877
|
+
id: data.id,
|
|
878
|
+
select: { _id: true, token: true, tokenHash: true },
|
|
879
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
880
|
+
});
|
|
881
|
+
} else if (data.kind === OnCallCalendarFeedKind.Schedule) {
|
|
882
|
+
row = await OnCallDutyPolicyScheduleCalendarFeedService.findOneById({
|
|
883
|
+
id: data.id,
|
|
884
|
+
select: { _id: true, token: true, tokenHash: true },
|
|
885
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
886
|
+
});
|
|
887
|
+
} else {
|
|
888
|
+
row = await ProjectOnCallCalendarFeedService.findOneById({
|
|
889
|
+
id: data.id,
|
|
890
|
+
select: { _id: true, token: true, tokenHash: true },
|
|
891
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (!row || !row.token) {
|
|
896
|
+
return { decryptFailed: true };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (
|
|
900
|
+
!row.tokenHash ||
|
|
901
|
+
!CalendarFeedToken.isValidShape(row.token) ||
|
|
902
|
+
!CalendarFeedToken.isHashEqual(
|
|
903
|
+
CalendarFeedToken.hash(row.token),
|
|
904
|
+
row.tokenHash,
|
|
905
|
+
)
|
|
906
|
+
) {
|
|
907
|
+
return { decryptFailed: true };
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return {
|
|
911
|
+
decryptedToken: row.token,
|
|
912
|
+
verifiedTokenHash: row.tokenHash,
|
|
913
|
+
decryptFailed: false,
|
|
914
|
+
};
|
|
915
|
+
} catch (err) {
|
|
916
|
+
logger.warn(
|
|
917
|
+
`OnCallCalendarAPI: could not decrypt the ${data.kind} calendar feed token for feed ${data.id.toString()}; the link needs regenerating.`,
|
|
918
|
+
);
|
|
919
|
+
logger.warn(err);
|
|
920
|
+
return { decryptFailed: true };
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async function buildStatusForRow(data: {
|
|
925
|
+
kind: OnCallCalendarFeedKind;
|
|
926
|
+
feed: AnyFeedRow;
|
|
927
|
+
plaintextToken?: string | undefined;
|
|
928
|
+
}): Promise<FeedStatus> {
|
|
929
|
+
if (data.plaintextToken) {
|
|
930
|
+
return buildFeedStatus({
|
|
931
|
+
kind: data.kind,
|
|
932
|
+
feed: data.feed as FeedRowLike,
|
|
933
|
+
plaintextToken: data.plaintextToken,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (!data.feed.id) {
|
|
938
|
+
return buildFeedStatus({
|
|
939
|
+
kind: data.kind,
|
|
940
|
+
feed: data.feed as FeedRowLike,
|
|
941
|
+
decryptFailed: true,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const decrypted: {
|
|
946
|
+
decryptedToken?: string | undefined;
|
|
947
|
+
verifiedTokenHash?: string | undefined;
|
|
948
|
+
decryptFailed: boolean;
|
|
949
|
+
} = await readTokenForStatus({ kind: data.kind, id: data.feed.id });
|
|
950
|
+
|
|
951
|
+
return buildFeedStatus({
|
|
952
|
+
kind: data.kind,
|
|
953
|
+
feed: data.feed as FeedRowLike,
|
|
954
|
+
decryptedToken: decrypted.decryptedToken,
|
|
955
|
+
verifiedTokenHash: decrypted.verifiedTokenHash,
|
|
956
|
+
decryptFailed: decrypted.decryptFailed,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/* The non-secret columns a status read selects (any of the three models). */
|
|
961
|
+
const STATUS_SELECT: {
|
|
962
|
+
_id: true;
|
|
963
|
+
projectId: true;
|
|
964
|
+
isEnabled: true;
|
|
965
|
+
tokenHint: true;
|
|
966
|
+
rotatedAt: true;
|
|
967
|
+
previousTokenExpiresAt: true;
|
|
968
|
+
lastFetchedAt: true;
|
|
969
|
+
lastFetchedClient: true;
|
|
970
|
+
fetchCount: true;
|
|
971
|
+
lastRenderTruncated: true;
|
|
972
|
+
pastDays: true;
|
|
973
|
+
futureDays: true;
|
|
974
|
+
} = {
|
|
975
|
+
_id: true,
|
|
976
|
+
projectId: true,
|
|
977
|
+
isEnabled: true,
|
|
978
|
+
tokenHint: true,
|
|
979
|
+
rotatedAt: true,
|
|
980
|
+
previousTokenExpiresAt: true,
|
|
981
|
+
lastFetchedAt: true,
|
|
982
|
+
lastFetchedClient: true,
|
|
983
|
+
fetchCount: true,
|
|
984
|
+
lastRenderTruncated: true,
|
|
985
|
+
pastDays: true,
|
|
986
|
+
futureDays: true,
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
// -- Public feed lookup -----------------------------------------------------
|
|
990
|
+
|
|
991
|
+
/*
|
|
992
|
+
* The columns the PUBLIC path reads. `token` is never among them: the public
|
|
993
|
+
* routes must keep working after an ENCRYPTION_SECRET rotation, and a select
|
|
994
|
+
* that included the encrypted column would make DatabaseService decrypt it on
|
|
995
|
+
* every poll. Tests pin this.
|
|
996
|
+
*/
|
|
997
|
+
const PUBLIC_COMMON_SELECT: {
|
|
998
|
+
_id: true;
|
|
999
|
+
projectId: true;
|
|
1000
|
+
isEnabled: true;
|
|
1001
|
+
pastDays: true;
|
|
1002
|
+
futureDays: true;
|
|
1003
|
+
lastFetchedAt: true;
|
|
1004
|
+
fetchCount: true;
|
|
1005
|
+
previousTokenExpiresAt: true;
|
|
1006
|
+
} = {
|
|
1007
|
+
_id: true,
|
|
1008
|
+
projectId: true,
|
|
1009
|
+
isEnabled: true,
|
|
1010
|
+
pastDays: true,
|
|
1011
|
+
futureDays: true,
|
|
1012
|
+
lastFetchedAt: true,
|
|
1013
|
+
fetchCount: true,
|
|
1014
|
+
previousTokenExpiresAt: true,
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
async function lookupPersonalFeed(
|
|
1018
|
+
tokenHash: string,
|
|
1019
|
+
now: Date,
|
|
1020
|
+
): Promise<TokenLookupResult<UserOnCallCalendarFeed> | null> {
|
|
1021
|
+
const select: typeof PUBLIC_COMMON_SELECT & {
|
|
1022
|
+
userId: true;
|
|
1023
|
+
includeCoveringShifts: true;
|
|
1024
|
+
} = {
|
|
1025
|
+
...PUBLIC_COMMON_SELECT,
|
|
1026
|
+
userId: true,
|
|
1027
|
+
includeCoveringShifts: true,
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
const current: UserOnCallCalendarFeed | null =
|
|
1031
|
+
await UserOnCallCalendarFeedService.findOneBy({
|
|
1032
|
+
query: { tokenHash },
|
|
1033
|
+
select,
|
|
1034
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
if (current) {
|
|
1038
|
+
return { feed: current, viaPreviousToken: false };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const previous: UserOnCallCalendarFeed | null =
|
|
1042
|
+
await UserOnCallCalendarFeedService.findOneBy({
|
|
1043
|
+
query: { previousTokenHash: tokenHash },
|
|
1044
|
+
select,
|
|
1045
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
if (!previous || !isPreviousTokenInGrace(previous, now)) {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return { feed: previous, viaPreviousToken: true };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
async function lookupScheduleFeed(
|
|
1056
|
+
tokenHash: string,
|
|
1057
|
+
now: Date,
|
|
1058
|
+
): Promise<TokenLookupResult<OnCallDutyPolicyScheduleCalendarFeed> | null> {
|
|
1059
|
+
const select: typeof PUBLIC_COMMON_SELECT & {
|
|
1060
|
+
onCallDutyPolicyScheduleId: true;
|
|
1061
|
+
includeCoverageGaps: true;
|
|
1062
|
+
minimumGapMinutes: true;
|
|
1063
|
+
} = {
|
|
1064
|
+
...PUBLIC_COMMON_SELECT,
|
|
1065
|
+
onCallDutyPolicyScheduleId: true,
|
|
1066
|
+
includeCoverageGaps: true,
|
|
1067
|
+
minimumGapMinutes: true,
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
const current: OnCallDutyPolicyScheduleCalendarFeed | null =
|
|
1071
|
+
await OnCallDutyPolicyScheduleCalendarFeedService.findOneBy({
|
|
1072
|
+
query: { tokenHash },
|
|
1073
|
+
select,
|
|
1074
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
if (current) {
|
|
1078
|
+
return { feed: current, viaPreviousToken: false };
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const previous: OnCallDutyPolicyScheduleCalendarFeed | null =
|
|
1082
|
+
await OnCallDutyPolicyScheduleCalendarFeedService.findOneBy({
|
|
1083
|
+
query: { previousTokenHash: tokenHash },
|
|
1084
|
+
select,
|
|
1085
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
if (!previous || !isPreviousTokenInGrace(previous, now)) {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return { feed: previous, viaPreviousToken: true };
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async function lookupProjectFeed(
|
|
1096
|
+
tokenHash: string,
|
|
1097
|
+
now: Date,
|
|
1098
|
+
): Promise<TokenLookupResult<ProjectOnCallCalendarFeed> | null> {
|
|
1099
|
+
const select: typeof PUBLIC_COMMON_SELECT & {
|
|
1100
|
+
includeCoverageGaps: true;
|
|
1101
|
+
minimumGapMinutes: true;
|
|
1102
|
+
} = {
|
|
1103
|
+
...PUBLIC_COMMON_SELECT,
|
|
1104
|
+
includeCoverageGaps: true,
|
|
1105
|
+
minimumGapMinutes: true,
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
const current: ProjectOnCallCalendarFeed | null =
|
|
1109
|
+
await ProjectOnCallCalendarFeedService.findOneBy({
|
|
1110
|
+
query: { tokenHash },
|
|
1111
|
+
select,
|
|
1112
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
if (current) {
|
|
1116
|
+
return { feed: current, viaPreviousToken: false };
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const previous: ProjectOnCallCalendarFeed | null =
|
|
1120
|
+
await ProjectOnCallCalendarFeedService.findOneBy({
|
|
1121
|
+
query: { previousTokenHash: tokenHash },
|
|
1122
|
+
select,
|
|
1123
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
if (!previous || !isPreviousTokenInGrace(previous, now)) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
return { feed: previous, viaPreviousToken: true };
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// -- Bookkeeping ------------------------------------------------------------
|
|
1134
|
+
|
|
1135
|
+
/*
|
|
1136
|
+
* Fire-and-forget root update after a served GET: when it was fetched, by
|
|
1137
|
+
* what kind of client, how many times (read-modify-write of the value the
|
|
1138
|
+
* lookup already read, so approximate under concurrency and deliberately
|
|
1139
|
+
* so), and whether the last render was cut. Never awaited by the route, never
|
|
1140
|
+
* allowed to throw into it.
|
|
1141
|
+
*/
|
|
1142
|
+
function recordFetch(data: {
|
|
1143
|
+
kind: OnCallCalendarFeedKind;
|
|
1144
|
+
feed: AnyFeedRow;
|
|
1145
|
+
req: ExpressRequest;
|
|
1146
|
+
now: Date;
|
|
1147
|
+
truncated: boolean;
|
|
1148
|
+
}): void {
|
|
1149
|
+
if (!data.feed.id) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (
|
|
1154
|
+
!shouldRecordFetch({
|
|
1155
|
+
method: data.req.method,
|
|
1156
|
+
lastFetchedAt: data.feed.lastFetchedAt,
|
|
1157
|
+
now: data.now,
|
|
1158
|
+
})
|
|
1159
|
+
) {
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const client: string | null = classifyCalendarClient(
|
|
1164
|
+
data.req.headers?.["user-agent"] as string | Array<string> | undefined,
|
|
1165
|
+
);
|
|
1166
|
+
|
|
1167
|
+
const previousCount: number =
|
|
1168
|
+
typeof data.feed.fetchCount === "number" &&
|
|
1169
|
+
Number.isFinite(data.feed.fetchCount)
|
|
1170
|
+
? data.feed.fetchCount
|
|
1171
|
+
: Number(data.feed.fetchCount) || 0;
|
|
1172
|
+
|
|
1173
|
+
const update: {
|
|
1174
|
+
lastFetchedAt: Date;
|
|
1175
|
+
lastFetchedClient: string;
|
|
1176
|
+
fetchCount: number;
|
|
1177
|
+
lastRenderTruncated: boolean;
|
|
1178
|
+
} = {
|
|
1179
|
+
lastFetchedAt: data.now,
|
|
1180
|
+
lastFetchedClient: client || "Other",
|
|
1181
|
+
fetchCount: previousCount + 1,
|
|
1182
|
+
lastRenderTruncated: data.truncated,
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
const id: ObjectID = data.feed.id;
|
|
1186
|
+
|
|
1187
|
+
let write: Promise<unknown>;
|
|
1188
|
+
|
|
1189
|
+
if (data.kind === OnCallCalendarFeedKind.Personal) {
|
|
1190
|
+
write = UserOnCallCalendarFeedService.updateOneById({
|
|
1191
|
+
id,
|
|
1192
|
+
data: update,
|
|
1193
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
1194
|
+
});
|
|
1195
|
+
} else if (data.kind === OnCallCalendarFeedKind.Schedule) {
|
|
1196
|
+
write = OnCallDutyPolicyScheduleCalendarFeedService.updateOneById({
|
|
1197
|
+
id,
|
|
1198
|
+
data: update,
|
|
1199
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
1200
|
+
});
|
|
1201
|
+
} else {
|
|
1202
|
+
write = ProjectOnCallCalendarFeedService.updateOneById({
|
|
1203
|
+
id,
|
|
1204
|
+
data: update,
|
|
1205
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
write.catch((err: unknown) => {
|
|
1210
|
+
logger.warn(
|
|
1211
|
+
`OnCallCalendarAPI: bookkeeping for the ${data.kind} calendar feed ${id.toString()} failed.`,
|
|
1212
|
+
);
|
|
1213
|
+
logger.warn(err);
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// -- Public routes ----------------------------------------------------------
|
|
1218
|
+
|
|
1219
|
+
/*
|
|
1220
|
+
* The shared tail of the three public routes, from "we have a lookup result"
|
|
1221
|
+
* to "bytes went out". Everything kind-specific is in the render request.
|
|
1222
|
+
*/
|
|
1223
|
+
async function serveFeed(data: {
|
|
1224
|
+
kind: OnCallCalendarFeedKind;
|
|
1225
|
+
req: ExpressRequest;
|
|
1226
|
+
res: ExpressResponse;
|
|
1227
|
+
lookup: TokenLookupResult<AnyFeedRow>;
|
|
1228
|
+
request: FeedRenderRequest;
|
|
1229
|
+
now: Date;
|
|
1230
|
+
emptyNames?:
|
|
1231
|
+
| {
|
|
1232
|
+
scheduleName?: string | undefined;
|
|
1233
|
+
projectName?: string | undefined;
|
|
1234
|
+
}
|
|
1235
|
+
| undefined;
|
|
1236
|
+
}): Promise<void> {
|
|
1237
|
+
let outcome: FeedRenderOutcome;
|
|
1238
|
+
|
|
1239
|
+
if (data.lookup.viaPreviousToken) {
|
|
1240
|
+
outcome = OnCallCalendarFeedRenderer.buildEmptyOutcome({
|
|
1241
|
+
kind: data.kind,
|
|
1242
|
+
reason: TOKEN_ROTATED_REASON,
|
|
1243
|
+
now: data.now,
|
|
1244
|
+
});
|
|
1245
|
+
} else if (data.lookup.feed.isEnabled === false) {
|
|
1246
|
+
outcome = OnCallCalendarFeedRenderer.buildEmptyOutcome({
|
|
1247
|
+
kind: data.kind,
|
|
1248
|
+
reason: FEED_DISABLED_REASON,
|
|
1249
|
+
now: data.now,
|
|
1250
|
+
});
|
|
1251
|
+
} else {
|
|
1252
|
+
outcome = await OnCallCalendarFeedRenderer.render(data.request);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if (outcome.status === FeedRenderStatus.Unavailable) {
|
|
1256
|
+
return sendUnavailable(
|
|
1257
|
+
data.req,
|
|
1258
|
+
data.res,
|
|
1259
|
+
outcome.retryAfterSeconds || KILL_SWITCH_RETRY_AFTER_SECONDS,
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (outcome.stale) {
|
|
1264
|
+
data.res.set("Warning", STALE_WARNING_HEADER);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
Response.sendCalendarResponse(data.req, data.res, {
|
|
1268
|
+
body: outcome.body,
|
|
1269
|
+
etag: outcome.etag,
|
|
1270
|
+
lastModified: outcome.lastModified,
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
/*
|
|
1274
|
+
* Only a real, current, enabled feed is worth bookkeeping. An empty
|
|
1275
|
+
* calendar served for a rotated-out link or a disabled feed is not a
|
|
1276
|
+
* fetch the settings page should count -- the two conditions above say so
|
|
1277
|
+
* exactly. Everything else that went out as a 200 IS a fetch, including the
|
|
1278
|
+
* empty calendars the renderer produces for "you are not on a schedule yet"
|
|
1279
|
+
* or "this project is below plan": those are precisely the users staring at
|
|
1280
|
+
* an empty calendar and asking "is my calendar app even reaching the
|
|
1281
|
+
* server?", which lastFetchedAt/lastFetchedClient exist to answer. Only an
|
|
1282
|
+
* Unavailable outcome is skipped, and that one already returned above
|
|
1283
|
+
* without sending a body.
|
|
1284
|
+
*/
|
|
1285
|
+
if (!data.lookup.viaPreviousToken && data.lookup.feed.isEnabled !== false) {
|
|
1286
|
+
recordFetch({
|
|
1287
|
+
kind: data.kind,
|
|
1288
|
+
feed: data.lookup.feed,
|
|
1289
|
+
req: data.req,
|
|
1290
|
+
now: data.now,
|
|
1291
|
+
truncated: outcome.truncated || outcome.stale,
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
router.get(
|
|
1297
|
+
PERSONAL_FEED_ROUTE,
|
|
1298
|
+
killSwitchMiddleware,
|
|
1299
|
+
schemeGuardMiddleware,
|
|
1300
|
+
OnCallCalendarFeedRateLimit.getMiddleware(),
|
|
1301
|
+
async (
|
|
1302
|
+
req: ExpressRequest,
|
|
1303
|
+
res: ExpressResponse,
|
|
1304
|
+
next: NextFunction,
|
|
1305
|
+
): Promise<void> => {
|
|
1306
|
+
try {
|
|
1307
|
+
const token: unknown = req.params?.["token"];
|
|
1308
|
+
|
|
1309
|
+
if (!CalendarFeedToken.isValidShape(token)) {
|
|
1310
|
+
return sendNotFound(req, res);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const scheduleFilter: ObjectID | undefined | null =
|
|
1314
|
+
readScheduleFilter(req);
|
|
1315
|
+
|
|
1316
|
+
if (scheduleFilter === null) {
|
|
1317
|
+
return sendNotFound(req, res);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const now: Date = OneUptimeDate.getCurrentDate();
|
|
1321
|
+
const tokenHash: string = CalendarFeedToken.hash(token);
|
|
1322
|
+
|
|
1323
|
+
const lookup: TokenLookupResult<UserOnCallCalendarFeed> | null =
|
|
1324
|
+
await lookupPersonalFeed(tokenHash, now);
|
|
1325
|
+
|
|
1326
|
+
if (!lookup || !lookup.feed.id || !lookup.feed.projectId) {
|
|
1327
|
+
return sendNotFound(req, res);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (!lookup.feed.userId) {
|
|
1331
|
+
return sendNotFound(req, res);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
return await serveFeed({
|
|
1335
|
+
kind: OnCallCalendarFeedKind.Personal,
|
|
1336
|
+
req,
|
|
1337
|
+
res,
|
|
1338
|
+
lookup,
|
|
1339
|
+
now,
|
|
1340
|
+
request: {
|
|
1341
|
+
kind: OnCallCalendarFeedKind.Personal,
|
|
1342
|
+
feedId: lookup.feed.id,
|
|
1343
|
+
projectId: lookup.feed.projectId,
|
|
1344
|
+
userId: lookup.feed.userId,
|
|
1345
|
+
tokenHash,
|
|
1346
|
+
includeCoveringShifts: lookup.feed.includeCoveringShifts !== false,
|
|
1347
|
+
pastDays: lookup.feed.pastDays ?? DEFAULT_PAST_DAYS,
|
|
1348
|
+
futureDays: lookup.feed.futureDays ?? DEFAULT_FUTURE_DAYS,
|
|
1349
|
+
scheduleFilterId: scheduleFilter,
|
|
1350
|
+
now,
|
|
1351
|
+
},
|
|
1352
|
+
});
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
next(err);
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
);
|
|
1358
|
+
|
|
1359
|
+
router.get(
|
|
1360
|
+
SCHEDULE_FEED_ROUTE,
|
|
1361
|
+
killSwitchMiddleware,
|
|
1362
|
+
schemeGuardMiddleware,
|
|
1363
|
+
OnCallCalendarFeedRateLimit.getMiddleware(),
|
|
1364
|
+
async (
|
|
1365
|
+
req: ExpressRequest,
|
|
1366
|
+
res: ExpressResponse,
|
|
1367
|
+
next: NextFunction,
|
|
1368
|
+
): Promise<void> => {
|
|
1369
|
+
try {
|
|
1370
|
+
const token: unknown = req.params?.["token"];
|
|
1371
|
+
|
|
1372
|
+
if (!CalendarFeedToken.isValidShape(token)) {
|
|
1373
|
+
return sendNotFound(req, res);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const now: Date = OneUptimeDate.getCurrentDate();
|
|
1377
|
+
const tokenHash: string = CalendarFeedToken.hash(token);
|
|
1378
|
+
|
|
1379
|
+
const lookup: TokenLookupResult<OnCallDutyPolicyScheduleCalendarFeed> | null =
|
|
1380
|
+
await lookupScheduleFeed(tokenHash, now);
|
|
1381
|
+
|
|
1382
|
+
if (
|
|
1383
|
+
!lookup ||
|
|
1384
|
+
!lookup.feed.id ||
|
|
1385
|
+
!lookup.feed.projectId ||
|
|
1386
|
+
!lookup.feed.onCallDutyPolicyScheduleId
|
|
1387
|
+
) {
|
|
1388
|
+
return sendNotFound(req, res);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
return await serveFeed({
|
|
1392
|
+
kind: OnCallCalendarFeedKind.Schedule,
|
|
1393
|
+
req,
|
|
1394
|
+
res,
|
|
1395
|
+
lookup,
|
|
1396
|
+
now,
|
|
1397
|
+
request: {
|
|
1398
|
+
kind: OnCallCalendarFeedKind.Schedule,
|
|
1399
|
+
feedId: lookup.feed.id,
|
|
1400
|
+
projectId: lookup.feed.projectId,
|
|
1401
|
+
scheduleId: lookup.feed.onCallDutyPolicyScheduleId,
|
|
1402
|
+
tokenHash,
|
|
1403
|
+
includeCoverageGaps: lookup.feed.includeCoverageGaps === true,
|
|
1404
|
+
minimumGapMinutes: lookup.feed.minimumGapMinutes ?? 60,
|
|
1405
|
+
pastDays: lookup.feed.pastDays ?? DEFAULT_PAST_DAYS,
|
|
1406
|
+
futureDays: lookup.feed.futureDays ?? DEFAULT_FUTURE_DAYS,
|
|
1407
|
+
now,
|
|
1408
|
+
},
|
|
1409
|
+
});
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
next(err);
|
|
1412
|
+
}
|
|
1413
|
+
},
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
router.get(
|
|
1417
|
+
PROJECT_FEED_ROUTE,
|
|
1418
|
+
killSwitchMiddleware,
|
|
1419
|
+
schemeGuardMiddleware,
|
|
1420
|
+
OnCallCalendarFeedRateLimit.getMiddleware(),
|
|
1421
|
+
async (
|
|
1422
|
+
req: ExpressRequest,
|
|
1423
|
+
res: ExpressResponse,
|
|
1424
|
+
next: NextFunction,
|
|
1425
|
+
): Promise<void> => {
|
|
1426
|
+
try {
|
|
1427
|
+
const token: unknown = req.params?.["token"];
|
|
1428
|
+
|
|
1429
|
+
if (!CalendarFeedToken.isValidShape(token)) {
|
|
1430
|
+
return sendNotFound(req, res);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const now: Date = OneUptimeDate.getCurrentDate();
|
|
1434
|
+
const tokenHash: string = CalendarFeedToken.hash(token);
|
|
1435
|
+
|
|
1436
|
+
const lookup: TokenLookupResult<ProjectOnCallCalendarFeed> | null =
|
|
1437
|
+
await lookupProjectFeed(tokenHash, now);
|
|
1438
|
+
|
|
1439
|
+
if (!lookup || !lookup.feed.id || !lookup.feed.projectId) {
|
|
1440
|
+
return sendNotFound(req, res);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
return await serveFeed({
|
|
1444
|
+
kind: OnCallCalendarFeedKind.Project,
|
|
1445
|
+
req,
|
|
1446
|
+
res,
|
|
1447
|
+
lookup,
|
|
1448
|
+
now,
|
|
1449
|
+
request: {
|
|
1450
|
+
kind: OnCallCalendarFeedKind.Project,
|
|
1451
|
+
feedId: lookup.feed.id,
|
|
1452
|
+
projectId: lookup.feed.projectId,
|
|
1453
|
+
tokenHash,
|
|
1454
|
+
includeCoverageGaps: lookup.feed.includeCoverageGaps === true,
|
|
1455
|
+
minimumGapMinutes: lookup.feed.minimumGapMinutes ?? 60,
|
|
1456
|
+
pastDays: lookup.feed.pastDays ?? DEFAULT_PAST_DAYS,
|
|
1457
|
+
futureDays: lookup.feed.futureDays ?? DEFAULT_FUTURE_DAYS,
|
|
1458
|
+
now,
|
|
1459
|
+
},
|
|
1460
|
+
});
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
next(err);
|
|
1463
|
+
}
|
|
1464
|
+
},
|
|
1465
|
+
);
|
|
1466
|
+
|
|
1467
|
+
/*
|
|
1468
|
+
* Registered AFTER the three exact GET routes, so those still win, and
|
|
1469
|
+
* before the session routes, whose paths (`feed/`, `schedule-feed/`,
|
|
1470
|
+
* `project-feed/`, `my-shifts`) share no prefix with these.
|
|
1471
|
+
*/
|
|
1472
|
+
router.all(PERSONAL_FEED_FALLBACK_ROUTE, feedFallbackHandler);
|
|
1473
|
+
router.all(SCHEDULE_FEED_FALLBACK_ROUTE, feedFallbackHandler);
|
|
1474
|
+
router.all(PROJECT_FEED_FALLBACK_ROUTE, feedFallbackHandler);
|
|
1475
|
+
|
|
1476
|
+
// -- Session routes: personal feed -----------------------------------------
|
|
1477
|
+
|
|
1478
|
+
/*
|
|
1479
|
+
* The caller's own feed row in the tenant project. Root read: the row is
|
|
1480
|
+
* addressed by (projectId, userId) where userId is the SESSION's user, so
|
|
1481
|
+
* the model's CurrentUser predicate is satisfied by construction and a
|
|
1482
|
+
* non-root read would only add a permission round trip.
|
|
1483
|
+
*/
|
|
1484
|
+
async function findPersonalFeed(data: {
|
|
1485
|
+
projectId: ObjectID;
|
|
1486
|
+
userId: ObjectID;
|
|
1487
|
+
}): Promise<UserOnCallCalendarFeed | null> {
|
|
1488
|
+
return await UserOnCallCalendarFeedService.findOneBy({
|
|
1489
|
+
query: {
|
|
1490
|
+
projectId: data.projectId,
|
|
1491
|
+
userId: data.userId,
|
|
1492
|
+
},
|
|
1493
|
+
select: {
|
|
1494
|
+
...STATUS_SELECT,
|
|
1495
|
+
userId: true,
|
|
1496
|
+
includeCoveringShifts: true,
|
|
1497
|
+
},
|
|
1498
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function requireUserId(props: DatabaseCommonInteractionProps): ObjectID {
|
|
1503
|
+
if (!props.userId) {
|
|
1504
|
+
throw new BadDataException("A logged-in user is required.");
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
return props.userId;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
router.get(
|
|
1511
|
+
FEED_CURRENT_ROUTE,
|
|
1512
|
+
UserMiddleware.getUserMiddleware,
|
|
1513
|
+
UserMiddleware.requireUserAuthentication,
|
|
1514
|
+
async (
|
|
1515
|
+
req: ExpressRequest,
|
|
1516
|
+
res: ExpressResponse,
|
|
1517
|
+
next: NextFunction,
|
|
1518
|
+
): Promise<void> => {
|
|
1519
|
+
try {
|
|
1520
|
+
const props: DatabaseCommonInteractionProps =
|
|
1521
|
+
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
|
1522
|
+
|
|
1523
|
+
const projectId: ObjectID =
|
|
1524
|
+
CommonAPI.assertAuthenticatedProjectMember(props);
|
|
1525
|
+
const userId: ObjectID = requireUserId(props);
|
|
1526
|
+
|
|
1527
|
+
const feed: UserOnCallCalendarFeed | null = await findPersonalFeed({
|
|
1528
|
+
projectId,
|
|
1529
|
+
userId,
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
if (!feed) {
|
|
1533
|
+
return Response.sendJsonObjectResponse(
|
|
1534
|
+
req,
|
|
1535
|
+
res,
|
|
1536
|
+
buildAbsentFeedStatus(
|
|
1537
|
+
OnCallCalendarFeedKind.Personal,
|
|
1538
|
+
) as unknown as JSONObject,
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const status: FeedStatus = await buildStatusForRow({
|
|
1543
|
+
kind: OnCallCalendarFeedKind.Personal,
|
|
1544
|
+
feed,
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
return Response.sendJsonObjectResponse(
|
|
1548
|
+
req,
|
|
1549
|
+
res,
|
|
1550
|
+
status as unknown as JSONObject,
|
|
1551
|
+
);
|
|
1552
|
+
} catch (err) {
|
|
1553
|
+
next(err);
|
|
1554
|
+
}
|
|
1555
|
+
},
|
|
1556
|
+
);
|
|
1557
|
+
|
|
1558
|
+
/*
|
|
1559
|
+
* Mint-or-rotate under a per-(project, user) lock. The UNIQUE(projectId,
|
|
1560
|
+
* userId) index is the backstop; the lock is what turns a double click into
|
|
1561
|
+
* one new link rather than one new link and one 500. Without Redis the lock
|
|
1562
|
+
* cannot be taken, and the index alone has to do -- logged, not fatal.
|
|
1563
|
+
*/
|
|
1564
|
+
async function withRotateLock<T>(
|
|
1565
|
+
key: string,
|
|
1566
|
+
work: () => Promise<T>,
|
|
1567
|
+
): Promise<T> {
|
|
1568
|
+
let mutex: SemaphoreMutex | null = null;
|
|
1569
|
+
|
|
1570
|
+
try {
|
|
1571
|
+
mutex = await Semaphore.lock({
|
|
1572
|
+
key,
|
|
1573
|
+
namespace: ROTATE_LOCK_NAMESPACE,
|
|
1574
|
+
lockTimeout: ROTATE_LOCK_TIMEOUT_MS,
|
|
1575
|
+
});
|
|
1576
|
+
} catch (err) {
|
|
1577
|
+
logger.warn(
|
|
1578
|
+
"OnCallCalendarAPI: could not take the rotate lock; relying on the unique index.",
|
|
1579
|
+
);
|
|
1580
|
+
logger.warn(err);
|
|
1581
|
+
mutex = null;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
try {
|
|
1585
|
+
return await work();
|
|
1586
|
+
} finally {
|
|
1587
|
+
if (mutex) {
|
|
1588
|
+
try {
|
|
1589
|
+
await Semaphore.release(mutex);
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
logger.warn("OnCallCalendarAPI: releasing the rotate lock failed.");
|
|
1592
|
+
logger.warn(err);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
router.post(
|
|
1599
|
+
FEED_ROTATE_ROUTE,
|
|
1600
|
+
UserMiddleware.getUserMiddleware,
|
|
1601
|
+
UserMiddleware.requireUserAuthentication,
|
|
1602
|
+
async (
|
|
1603
|
+
req: ExpressRequest,
|
|
1604
|
+
res: ExpressResponse,
|
|
1605
|
+
next: NextFunction,
|
|
1606
|
+
): Promise<void> => {
|
|
1607
|
+
try {
|
|
1608
|
+
assertJsonRequest(req);
|
|
1609
|
+
|
|
1610
|
+
const props: DatabaseCommonInteractionProps =
|
|
1611
|
+
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
|
1612
|
+
|
|
1613
|
+
const projectId: ObjectID =
|
|
1614
|
+
CommonAPI.assertAuthenticatedProjectMember(props);
|
|
1615
|
+
const userId: ObjectID = requireUserId(props);
|
|
1616
|
+
|
|
1617
|
+
const plaintextToken: string = await withRotateLock(
|
|
1618
|
+
`${projectId.toString()}-${userId.toString()}`,
|
|
1619
|
+
async (): Promise<string> => {
|
|
1620
|
+
const existing: UserOnCallCalendarFeed | null =
|
|
1621
|
+
await UserOnCallCalendarFeedService.findOneBy({
|
|
1622
|
+
query: { projectId, userId },
|
|
1623
|
+
select: { _id: true },
|
|
1624
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
if (!existing || !existing.id) {
|
|
1628
|
+
const created: {
|
|
1629
|
+
feed: UserOnCallCalendarFeed;
|
|
1630
|
+
minted: MintedCalendarFeedToken;
|
|
1631
|
+
} = await UserOnCallCalendarFeedService.createForUser({
|
|
1632
|
+
projectId,
|
|
1633
|
+
userId,
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
return created.minted.token;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
const rotation: CalendarFeedRotation =
|
|
1640
|
+
await UserOnCallCalendarFeedService.rotateTokenById({
|
|
1641
|
+
id: existing.id,
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
return rotation.token;
|
|
1645
|
+
},
|
|
1646
|
+
);
|
|
1647
|
+
|
|
1648
|
+
await OnCallCalendarFeedCache.purgeForUser(
|
|
1649
|
+
projectId.toString(),
|
|
1650
|
+
userId.toString(),
|
|
1651
|
+
);
|
|
1652
|
+
|
|
1653
|
+
const feed: UserOnCallCalendarFeed | null = await findPersonalFeed({
|
|
1654
|
+
projectId,
|
|
1655
|
+
userId,
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
if (!feed) {
|
|
1659
|
+
throw new NotFoundException("Calendar feed not found.");
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
const status: FeedStatus = await buildStatusForRow({
|
|
1663
|
+
kind: OnCallCalendarFeedKind.Personal,
|
|
1664
|
+
feed,
|
|
1665
|
+
plaintextToken,
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
return Response.sendJsonObjectResponse(
|
|
1669
|
+
req,
|
|
1670
|
+
res,
|
|
1671
|
+
status as unknown as JSONObject,
|
|
1672
|
+
);
|
|
1673
|
+
} catch (err) {
|
|
1674
|
+
next(err);
|
|
1675
|
+
}
|
|
1676
|
+
},
|
|
1677
|
+
);
|
|
1678
|
+
|
|
1679
|
+
// -- Session routes: schedule feed -----------------------------------------
|
|
1680
|
+
|
|
1681
|
+
/*
|
|
1682
|
+
* NON-root read with the caller's props: the copied TableAccessControl and
|
|
1683
|
+
* the @CanAccessIfCanReadOn label scoping decide whether this caller may see
|
|
1684
|
+
* that the feed exists at all.
|
|
1685
|
+
*/
|
|
1686
|
+
async function findScheduleFeedAsCaller(data: {
|
|
1687
|
+
scheduleId: ObjectID;
|
|
1688
|
+
projectId: ObjectID;
|
|
1689
|
+
props: DatabaseCommonInteractionProps;
|
|
1690
|
+
}): Promise<OnCallDutyPolicyScheduleCalendarFeed | null> {
|
|
1691
|
+
return await OnCallDutyPolicyScheduleCalendarFeedService.findOneBy({
|
|
1692
|
+
query: {
|
|
1693
|
+
onCallDutyPolicyScheduleId: data.scheduleId,
|
|
1694
|
+
projectId: data.projectId,
|
|
1695
|
+
},
|
|
1696
|
+
select: {
|
|
1697
|
+
...STATUS_SELECT,
|
|
1698
|
+
onCallDutyPolicyScheduleId: true,
|
|
1699
|
+
includeCoverageGaps: true,
|
|
1700
|
+
minimumGapMinutes: true,
|
|
1701
|
+
rotateWhenMemberLeaves: true,
|
|
1702
|
+
},
|
|
1703
|
+
props: data.props,
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
router.get(
|
|
1708
|
+
SCHEDULE_FEED_CURRENT_ROUTE,
|
|
1709
|
+
UserMiddleware.getUserMiddleware,
|
|
1710
|
+
UserMiddleware.requireUserAuthentication,
|
|
1711
|
+
async (
|
|
1712
|
+
req: ExpressRequest,
|
|
1713
|
+
res: ExpressResponse,
|
|
1714
|
+
next: NextFunction,
|
|
1715
|
+
): Promise<void> => {
|
|
1716
|
+
try {
|
|
1717
|
+
const props: DatabaseCommonInteractionProps =
|
|
1718
|
+
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
|
1719
|
+
|
|
1720
|
+
const projectId: ObjectID =
|
|
1721
|
+
CommonAPI.assertAuthenticatedProjectMember(props);
|
|
1722
|
+
const scheduleId: ObjectID = readObjectIdParam(req, "scheduleId");
|
|
1723
|
+
|
|
1724
|
+
const feed: OnCallDutyPolicyScheduleCalendarFeed | null =
|
|
1725
|
+
await findScheduleFeedAsCaller({ scheduleId, projectId, props });
|
|
1726
|
+
|
|
1727
|
+
if (!feed) {
|
|
1728
|
+
return Response.sendJsonObjectResponse(
|
|
1729
|
+
req,
|
|
1730
|
+
res,
|
|
1731
|
+
buildAbsentFeedStatus(
|
|
1732
|
+
OnCallCalendarFeedKind.Schedule,
|
|
1733
|
+
) as unknown as JSONObject,
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
const status: FeedStatus = await buildStatusForRow({
|
|
1738
|
+
kind: OnCallCalendarFeedKind.Schedule,
|
|
1739
|
+
feed,
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
return Response.sendJsonObjectResponse(
|
|
1743
|
+
req,
|
|
1744
|
+
res,
|
|
1745
|
+
status as unknown as JSONObject,
|
|
1746
|
+
);
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
next(err);
|
|
1749
|
+
}
|
|
1750
|
+
},
|
|
1751
|
+
);
|
|
1752
|
+
|
|
1753
|
+
/*
|
|
1754
|
+
* Publish = a NON-root create with the caller's props, so DatabaseService
|
|
1755
|
+
* runs the schedule's Create permission check and the service hook mints the
|
|
1756
|
+
* token (anything the request carried is discarded). A feed that already
|
|
1757
|
+
* exists is re-enabled through a non-root update instead -- same permission
|
|
1758
|
+
* gate, and the existing link keeps working for everyone who already has it.
|
|
1759
|
+
*/
|
|
1760
|
+
router.post(
|
|
1761
|
+
SCHEDULE_FEED_PUBLISH_ROUTE,
|
|
1762
|
+
UserMiddleware.getUserMiddleware,
|
|
1763
|
+
UserMiddleware.requireUserAuthentication,
|
|
1764
|
+
async (
|
|
1765
|
+
req: ExpressRequest,
|
|
1766
|
+
res: ExpressResponse,
|
|
1767
|
+
next: NextFunction,
|
|
1768
|
+
): Promise<void> => {
|
|
1769
|
+
try {
|
|
1770
|
+
assertJsonRequest(req);
|
|
1771
|
+
|
|
1772
|
+
const props: DatabaseCommonInteractionProps =
|
|
1773
|
+
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
|
1774
|
+
|
|
1775
|
+
const projectId: ObjectID =
|
|
1776
|
+
CommonAPI.assertAuthenticatedProjectMember(props);
|
|
1777
|
+
const scheduleId: ObjectID = readObjectIdParam(req, "scheduleId");
|
|
1778
|
+
|
|
1779
|
+
const existing: OnCallDutyPolicyScheduleCalendarFeed | null =
|
|
1780
|
+
await findScheduleFeedAsCaller({ scheduleId, projectId, props });
|
|
1781
|
+
|
|
1782
|
+
if (existing && existing.id) {
|
|
1783
|
+
await OnCallDutyPolicyScheduleCalendarFeedService.updateOneBy({
|
|
1784
|
+
query: { _id: existing.id, projectId },
|
|
1785
|
+
data: { isEnabled: true },
|
|
1786
|
+
props,
|
|
1787
|
+
});
|
|
1788
|
+
} else {
|
|
1789
|
+
/*
|
|
1790
|
+
* The create alone is not enough of a gate. Its permission check
|
|
1791
|
+
* (CreatePermission) looks at table and column permissions and at
|
|
1792
|
+
* ownership; @CanAccessIfCanReadOn -- the label scoping that decides
|
|
1793
|
+
* WHICH schedules an editor may touch -- is only applied to query
|
|
1794
|
+
* operations (read/update/delete), and the service's own check runs
|
|
1795
|
+
* as root. So a label-restricted editor who knows a schedule's id
|
|
1796
|
+
* could publish a feed for a schedule outside their labels. Reading
|
|
1797
|
+
* the schedule with the caller's props first applies exactly the
|
|
1798
|
+
* scoping /current and /rotate already get.
|
|
1799
|
+
*/
|
|
1800
|
+
const schedule: OnCallDutyPolicySchedule | null =
|
|
1801
|
+
await OnCallDutyPolicyScheduleService.findOneBy({
|
|
1802
|
+
query: { _id: scheduleId, projectId },
|
|
1803
|
+
select: { _id: true },
|
|
1804
|
+
props,
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
if (!schedule) {
|
|
1808
|
+
throw new NotFoundException("On-call schedule not found.");
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
const model: OnCallDutyPolicyScheduleCalendarFeed =
|
|
1812
|
+
new OnCallDutyPolicyScheduleCalendarFeed();
|
|
1813
|
+
model.projectId = projectId;
|
|
1814
|
+
model.onCallDutyPolicyScheduleId = scheduleId;
|
|
1815
|
+
|
|
1816
|
+
await OnCallDutyPolicyScheduleCalendarFeedService.create({
|
|
1817
|
+
data: model,
|
|
1818
|
+
props,
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
await OnCallCalendarFeedCache.purgeForSchedule(scheduleId.toString());
|
|
1823
|
+
|
|
1824
|
+
const feed: OnCallDutyPolicyScheduleCalendarFeed | null =
|
|
1825
|
+
await findScheduleFeedAsCaller({ scheduleId, projectId, props });
|
|
1826
|
+
|
|
1827
|
+
if (!feed) {
|
|
1828
|
+
throw new NotFoundException("Calendar feed not found.");
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
const status: FeedStatus = await buildStatusForRow({
|
|
1832
|
+
kind: OnCallCalendarFeedKind.Schedule,
|
|
1833
|
+
feed,
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
return Response.sendJsonObjectResponse(
|
|
1837
|
+
req,
|
|
1838
|
+
res,
|
|
1839
|
+
status as unknown as JSONObject,
|
|
1840
|
+
);
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
next(err);
|
|
1843
|
+
}
|
|
1844
|
+
},
|
|
1845
|
+
);
|
|
1846
|
+
|
|
1847
|
+
/*
|
|
1848
|
+
* Rotate = a NON-root update of an updatable column (isEnabled, written back
|
|
1849
|
+
* with its current value) so the Edit permission check runs, THEN the root
|
|
1850
|
+
* mint. The token columns themselves are root-only on update, which is why
|
|
1851
|
+
* the permission probe cannot be the rotation write itself.
|
|
1852
|
+
*/
|
|
1853
|
+
router.post(
|
|
1854
|
+
SCHEDULE_FEED_ROTATE_ROUTE,
|
|
1855
|
+
UserMiddleware.getUserMiddleware,
|
|
1856
|
+
UserMiddleware.requireUserAuthentication,
|
|
1857
|
+
async (
|
|
1858
|
+
req: ExpressRequest,
|
|
1859
|
+
res: ExpressResponse,
|
|
1860
|
+
next: NextFunction,
|
|
1861
|
+
): Promise<void> => {
|
|
1862
|
+
try {
|
|
1863
|
+
assertJsonRequest(req);
|
|
1864
|
+
|
|
1865
|
+
const props: DatabaseCommonInteractionProps =
|
|
1866
|
+
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
|
1867
|
+
|
|
1868
|
+
const projectId: ObjectID =
|
|
1869
|
+
CommonAPI.assertAuthenticatedProjectMember(props);
|
|
1870
|
+
const scheduleId: ObjectID = readObjectIdParam(req, "scheduleId");
|
|
1871
|
+
|
|
1872
|
+
const existing: OnCallDutyPolicyScheduleCalendarFeed | null =
|
|
1873
|
+
await findScheduleFeedAsCaller({ scheduleId, projectId, props });
|
|
1874
|
+
|
|
1875
|
+
if (!existing || !existing.id) {
|
|
1876
|
+
throw new NotFoundException(
|
|
1877
|
+
"This schedule has no shared calendar feed yet. Publish one first.",
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
await OnCallDutyPolicyScheduleCalendarFeedService.updateOneBy({
|
|
1882
|
+
query: { _id: existing.id, projectId },
|
|
1883
|
+
data: { isEnabled: existing.isEnabled !== false },
|
|
1884
|
+
props,
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
const rotation: CalendarFeedRotation =
|
|
1888
|
+
await OnCallDutyPolicyScheduleCalendarFeedService.rotateTokenById({
|
|
1889
|
+
id: existing.id,
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
await OnCallCalendarFeedCache.purgeForSchedule(scheduleId.toString());
|
|
1893
|
+
|
|
1894
|
+
const feed: OnCallDutyPolicyScheduleCalendarFeed | null =
|
|
1895
|
+
await findScheduleFeedAsCaller({ scheduleId, projectId, props });
|
|
1896
|
+
|
|
1897
|
+
if (!feed) {
|
|
1898
|
+
throw new NotFoundException("Calendar feed not found.");
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
const status: FeedStatus = await buildStatusForRow({
|
|
1902
|
+
kind: OnCallCalendarFeedKind.Schedule,
|
|
1903
|
+
feed,
|
|
1904
|
+
plaintextToken: rotation.token,
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
return Response.sendJsonObjectResponse(
|
|
1908
|
+
req,
|
|
1909
|
+
res,
|
|
1910
|
+
status as unknown as JSONObject,
|
|
1911
|
+
);
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
next(err);
|
|
1914
|
+
}
|
|
1915
|
+
},
|
|
1916
|
+
);
|
|
1917
|
+
|
|
1918
|
+
// -- Session routes: project feed ------------------------------------------
|
|
1919
|
+
|
|
1920
|
+
async function findProjectFeedAsCaller(data: {
|
|
1921
|
+
projectId: ObjectID;
|
|
1922
|
+
props: DatabaseCommonInteractionProps;
|
|
1923
|
+
}): Promise<ProjectOnCallCalendarFeed | null> {
|
|
1924
|
+
return await ProjectOnCallCalendarFeedService.findOneBy({
|
|
1925
|
+
query: {
|
|
1926
|
+
projectId: data.projectId,
|
|
1927
|
+
},
|
|
1928
|
+
select: {
|
|
1929
|
+
...STATUS_SELECT,
|
|
1930
|
+
includeCoverageGaps: true,
|
|
1931
|
+
minimumGapMinutes: true,
|
|
1932
|
+
rotateWhenMemberLeaves: true,
|
|
1933
|
+
},
|
|
1934
|
+
props: data.props,
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
router.get(
|
|
1939
|
+
PROJECT_FEED_CURRENT_ROUTE,
|
|
1940
|
+
UserMiddleware.getUserMiddleware,
|
|
1941
|
+
UserMiddleware.requireUserAuthentication,
|
|
1942
|
+
async (
|
|
1943
|
+
req: ExpressRequest,
|
|
1944
|
+
res: ExpressResponse,
|
|
1945
|
+
next: NextFunction,
|
|
1946
|
+
): Promise<void> => {
|
|
1947
|
+
try {
|
|
1948
|
+
const props: DatabaseCommonInteractionProps =
|
|
1949
|
+
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
|
1950
|
+
|
|
1951
|
+
const projectId: ObjectID =
|
|
1952
|
+
CommonAPI.assertAuthenticatedProjectMember(props);
|
|
1953
|
+
|
|
1954
|
+
const feed: ProjectOnCallCalendarFeed | null =
|
|
1955
|
+
await findProjectFeedAsCaller({ projectId, props });
|
|
1956
|
+
|
|
1957
|
+
if (!feed) {
|
|
1958
|
+
return Response.sendJsonObjectResponse(
|
|
1959
|
+
req,
|
|
1960
|
+
res,
|
|
1961
|
+
buildAbsentFeedStatus(
|
|
1962
|
+
OnCallCalendarFeedKind.Project,
|
|
1963
|
+
) as unknown as JSONObject,
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
const status: FeedStatus = await buildStatusForRow({
|
|
1968
|
+
kind: OnCallCalendarFeedKind.Project,
|
|
1969
|
+
feed,
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
return Response.sendJsonObjectResponse(
|
|
1973
|
+
req,
|
|
1974
|
+
res,
|
|
1975
|
+
status as unknown as JSONObject,
|
|
1976
|
+
);
|
|
1977
|
+
} catch (err) {
|
|
1978
|
+
next(err);
|
|
1979
|
+
}
|
|
1980
|
+
},
|
|
1981
|
+
);
|
|
1982
|
+
|
|
1983
|
+
router.post(
|
|
1984
|
+
PROJECT_FEED_PUBLISH_ROUTE,
|
|
1985
|
+
UserMiddleware.getUserMiddleware,
|
|
1986
|
+
UserMiddleware.requireUserAuthentication,
|
|
1987
|
+
async (
|
|
1988
|
+
req: ExpressRequest,
|
|
1989
|
+
res: ExpressResponse,
|
|
1990
|
+
next: NextFunction,
|
|
1991
|
+
): Promise<void> => {
|
|
1992
|
+
try {
|
|
1993
|
+
assertJsonRequest(req);
|
|
1994
|
+
|
|
1995
|
+
const props: DatabaseCommonInteractionProps =
|
|
1996
|
+
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
|
1997
|
+
|
|
1998
|
+
const projectId: ObjectID =
|
|
1999
|
+
CommonAPI.assertAuthenticatedProjectMember(props);
|
|
2000
|
+
|
|
2001
|
+
const existing: ProjectOnCallCalendarFeed | null =
|
|
2002
|
+
await findProjectFeedAsCaller({ projectId, props });
|
|
2003
|
+
|
|
2004
|
+
if (existing && existing.id) {
|
|
2005
|
+
await ProjectOnCallCalendarFeedService.updateOneBy({
|
|
2006
|
+
query: { _id: existing.id, projectId },
|
|
2007
|
+
data: { isEnabled: true },
|
|
2008
|
+
props,
|
|
2009
|
+
});
|
|
2010
|
+
} else {
|
|
2011
|
+
const model: ProjectOnCallCalendarFeed =
|
|
2012
|
+
new ProjectOnCallCalendarFeed();
|
|
2013
|
+
model.projectId = projectId;
|
|
2014
|
+
|
|
2015
|
+
await ProjectOnCallCalendarFeedService.create({
|
|
2016
|
+
data: model,
|
|
2017
|
+
props,
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
await OnCallCalendarFeedCache.purgeForProject(projectId.toString());
|
|
2022
|
+
|
|
2023
|
+
const feed: ProjectOnCallCalendarFeed | null =
|
|
2024
|
+
await findProjectFeedAsCaller({ projectId, props });
|
|
2025
|
+
|
|
2026
|
+
if (!feed) {
|
|
2027
|
+
throw new NotFoundException("Calendar feed not found.");
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
const status: FeedStatus = await buildStatusForRow({
|
|
2031
|
+
kind: OnCallCalendarFeedKind.Project,
|
|
2032
|
+
feed,
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
return Response.sendJsonObjectResponse(
|
|
2036
|
+
req,
|
|
2037
|
+
res,
|
|
2038
|
+
status as unknown as JSONObject,
|
|
2039
|
+
);
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
next(err);
|
|
2042
|
+
}
|
|
2043
|
+
},
|
|
2044
|
+
);
|
|
2045
|
+
|
|
2046
|
+
router.post(
|
|
2047
|
+
PROJECT_FEED_ROTATE_ROUTE,
|
|
2048
|
+
UserMiddleware.getUserMiddleware,
|
|
2049
|
+
UserMiddleware.requireUserAuthentication,
|
|
2050
|
+
async (
|
|
2051
|
+
req: ExpressRequest,
|
|
2052
|
+
res: ExpressResponse,
|
|
2053
|
+
next: NextFunction,
|
|
2054
|
+
): Promise<void> => {
|
|
2055
|
+
try {
|
|
2056
|
+
assertJsonRequest(req);
|
|
2057
|
+
|
|
2058
|
+
const props: DatabaseCommonInteractionProps =
|
|
2059
|
+
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
|
2060
|
+
|
|
2061
|
+
const projectId: ObjectID =
|
|
2062
|
+
CommonAPI.assertAuthenticatedProjectMember(props);
|
|
2063
|
+
|
|
2064
|
+
const existing: ProjectOnCallCalendarFeed | null =
|
|
2065
|
+
await findProjectFeedAsCaller({ projectId, props });
|
|
2066
|
+
|
|
2067
|
+
if (!existing || !existing.id) {
|
|
2068
|
+
throw new NotFoundException(
|
|
2069
|
+
"This project has no shared calendar feed yet. Publish one first.",
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
await ProjectOnCallCalendarFeedService.updateOneBy({
|
|
2074
|
+
query: { _id: existing.id, projectId },
|
|
2075
|
+
data: { isEnabled: existing.isEnabled !== false },
|
|
2076
|
+
props,
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
const rotation: CalendarFeedRotation =
|
|
2080
|
+
await ProjectOnCallCalendarFeedService.rotateTokenById({
|
|
2081
|
+
id: existing.id,
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
await OnCallCalendarFeedCache.purgeForProject(projectId.toString());
|
|
2085
|
+
|
|
2086
|
+
const feed: ProjectOnCallCalendarFeed | null =
|
|
2087
|
+
await findProjectFeedAsCaller({ projectId, props });
|
|
2088
|
+
|
|
2089
|
+
if (!feed) {
|
|
2090
|
+
throw new NotFoundException("Calendar feed not found.");
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
const status: FeedStatus = await buildStatusForRow({
|
|
2094
|
+
kind: OnCallCalendarFeedKind.Project,
|
|
2095
|
+
feed,
|
|
2096
|
+
plaintextToken: rotation.token,
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
return Response.sendJsonObjectResponse(
|
|
2100
|
+
req,
|
|
2101
|
+
res,
|
|
2102
|
+
status as unknown as JSONObject,
|
|
2103
|
+
);
|
|
2104
|
+
} catch (err) {
|
|
2105
|
+
next(err);
|
|
2106
|
+
}
|
|
2107
|
+
},
|
|
2108
|
+
);
|
|
2109
|
+
|
|
2110
|
+
// -- Session route: /my-shifts ---------------------------------------------
|
|
2111
|
+
|
|
2112
|
+
/*
|
|
2113
|
+
* The caller's own upcoming shifts as JSON. Scoped to the tenant project
|
|
2114
|
+
* when a `tenantid` header is present (the dashboard), to every project the
|
|
2115
|
+
* caller is rostered in otherwise (the mobile app). Same resolver and
|
|
2116
|
+
* schedule-level cache as the feeds; the per-process render cap applies, and
|
|
2117
|
+
* a capped request is a 503 the mobile app answers by falling back to its
|
|
2118
|
+
* roster-derived list.
|
|
2119
|
+
*/
|
|
2120
|
+
router.get(
|
|
2121
|
+
MY_SHIFTS_ROUTE,
|
|
2122
|
+
UserMiddleware.getUserMiddleware,
|
|
2123
|
+
UserMiddleware.requireUserAuthentication,
|
|
2124
|
+
async (
|
|
2125
|
+
req: ExpressRequest,
|
|
2126
|
+
res: ExpressResponse,
|
|
2127
|
+
next: NextFunction,
|
|
2128
|
+
): Promise<void> => {
|
|
2129
|
+
try {
|
|
2130
|
+
const props: DatabaseCommonInteractionProps =
|
|
2131
|
+
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
|
2132
|
+
|
|
2133
|
+
const userId: ObjectID = requireUserId(props);
|
|
2134
|
+
|
|
2135
|
+
let projectIds: Array<ObjectID> | undefined = undefined;
|
|
2136
|
+
|
|
2137
|
+
if (props.tenantId) {
|
|
2138
|
+
projectIds = [CommonAPI.assertAuthenticatedProjectMember(props)];
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
const now: Date = OneUptimeDate.getCurrentDate();
|
|
2142
|
+
const window: { from: Date; to: Date } = readMyShiftsWindow(req, now);
|
|
2143
|
+
|
|
2144
|
+
/*
|
|
2145
|
+
* Session renders leave slots free for the public feeds. /my-shifts is
|
|
2146
|
+
* called by a logged-in client that can retry and that falls back to
|
|
2147
|
+
* its roster list; a calendar client that gets a 503 shows a stale or
|
|
2148
|
+
* empty calendar to somebody who may be on call. With the default
|
|
2149
|
+
* concurrency of 4 this route may hold at most 2 slots at once.
|
|
2150
|
+
*/
|
|
2151
|
+
if (
|
|
2152
|
+
!OnCallCalendarFeedCache.tryAcquireRenderSlot({
|
|
2153
|
+
leaveFreeSlots: Math.floor(
|
|
2154
|
+
OnCallCalendarFeedCache.getRenderConcurrency() / 2,
|
|
2155
|
+
),
|
|
2156
|
+
})
|
|
2157
|
+
) {
|
|
2158
|
+
return sendUnavailable(req, res, 60);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
let result: UserShiftsResult;
|
|
2162
|
+
|
|
2163
|
+
try {
|
|
2164
|
+
result = await OnCallCalendarFeedRenderer.materializeUserShifts({
|
|
2165
|
+
userId,
|
|
2166
|
+
projectIds,
|
|
2167
|
+
from: window.from,
|
|
2168
|
+
to: window.to,
|
|
2169
|
+
now,
|
|
2170
|
+
});
|
|
2171
|
+
} finally {
|
|
2172
|
+
OnCallCalendarFeedCache.releaseRenderSlot();
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
const payload: MyShiftsResponse = {
|
|
2176
|
+
shifts: MaterializedShiftUtil.toJSONArray(
|
|
2177
|
+
result.shifts,
|
|
2178
|
+
) as unknown as Array<JSONObject>,
|
|
2179
|
+
truncated: result.truncated,
|
|
2180
|
+
generatedAt: result.generatedAt.toISOString(),
|
|
2181
|
+
};
|
|
2182
|
+
|
|
2183
|
+
return Response.sendJsonObjectResponse(
|
|
2184
|
+
req,
|
|
2185
|
+
res,
|
|
2186
|
+
payload as unknown as JSONObject,
|
|
2187
|
+
);
|
|
2188
|
+
} catch (err) {
|
|
2189
|
+
next(err);
|
|
2190
|
+
}
|
|
2191
|
+
},
|
|
2192
|
+
);
|
|
2193
|
+
|
|
2194
|
+
export default router;
|