@oneuptime/common 12.0.27 → 12.0.29

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.
Files changed (204) hide show
  1. package/Models/AnalyticsModels/RumSessionChunk.ts +52 -0
  2. package/Models/DatabaseModels/CloudResource.ts +17 -1
  3. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  4. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  5. package/Models/DatabaseModels/RumApplication.ts +35 -2
  6. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  7. package/Server/API/DashboardDomainAPI.ts +74 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  9. package/Server/API/StatusPageDomainAPI.ts +74 -0
  10. package/Server/API/TelemetryAPI.ts +104 -15
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  14. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  15. package/Server/Services/DashboardDomainService.ts +131 -0
  16. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  17. package/Server/Services/MonitorTemplateService.ts +70 -11
  18. package/Server/Services/StatusPageDomainService.ts +163 -3
  19. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  20. package/Server/Types/Domain.ts +17 -0
  21. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  22. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  23. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  24. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  25. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  26. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
  27. package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
  28. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
  29. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  30. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  31. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  32. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  33. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  34. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  35. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  36. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  37. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  38. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
  39. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  40. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  41. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  42. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  43. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  44. package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
  45. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  46. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  47. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  48. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  49. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  50. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  51. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  52. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  53. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  54. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  55. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  56. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  57. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  58. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  59. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  60. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  61. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  62. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  63. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  64. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  65. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  66. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  67. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  68. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  69. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  70. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  71. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  72. package/Tests/Types/API/Route.test.ts +36 -4
  73. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  74. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  75. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  76. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  77. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  78. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  79. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  80. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  81. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  82. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  83. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  84. package/Tests/Utils/Array.test.ts +100 -0
  85. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  86. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  87. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  88. package/Types/API/Route.ts +20 -5
  89. package/Types/Date.ts +143 -13
  90. package/Types/Rum/SessionReplay.ts +76 -1
  91. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  92. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  93. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  94. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  95. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  96. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  97. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  98. package/UI/Utils/Navigation.ts +42 -0
  99. package/Utils/Array.ts +37 -0
  100. package/Utils/CertificateReissue.ts +128 -0
  101. package/Utils/Grok/Grok.ts +461 -0
  102. package/Utils/Grok/GrokPatterns.ts +118 -0
  103. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  104. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
  105. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
  107. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  109. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  110. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  111. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
  113. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  115. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  116. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  117. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  118. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  119. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  120. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  121. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  122. package/build/dist/Server/API/TelemetryAPI.js +67 -14
  123. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  130. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  131. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  132. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  133. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  134. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  135. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  136. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  137. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  138. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  139. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  140. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  141. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  142. package/build/dist/Server/Types/Domain.js +16 -0
  143. package/build/dist/Server/Types/Domain.js.map +1 -1
  144. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  145. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  146. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  147. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  148. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  149. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  150. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  151. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  152. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  153. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  154. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  155. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
  156. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
  157. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
  158. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  159. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  160. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  161. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  162. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  163. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  164. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  165. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  166. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  167. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  168. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  169. package/build/dist/Types/API/Route.js +16 -5
  170. package/build/dist/Types/API/Route.js.map +1 -1
  171. package/build/dist/Types/Date.js +115 -13
  172. package/build/dist/Types/Date.js.map +1 -1
  173. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  174. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  175. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  176. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  177. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  178. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  179. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  180. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  181. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  182. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  183. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  184. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  185. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  186. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  187. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  188. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  189. package/build/dist/UI/Utils/Navigation.js +30 -0
  190. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  191. package/build/dist/Utils/Array.js +25 -0
  192. package/build/dist/Utils/Array.js.map +1 -1
  193. package/build/dist/Utils/CertificateReissue.js +97 -0
  194. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  195. package/build/dist/Utils/Grok/Grok.js +324 -0
  196. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  197. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  198. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  199. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  200. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  201. package/package.json +1 -1
  202. package/UI/Utils/JsonWebToken.ts +0 -14
  203. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  204. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -28,26 +28,27 @@ function formatTooltipTime(label: string | undefined): string {
28
28
  return label;
29
29
  }
30
30
 
31
- const now: Date = new Date();
32
- const isToday: boolean = date.toDateString() === now.toDateString();
33
-
34
- const time: string = date.toLocaleTimeString([], {
35
- hour: "2-digit",
36
- minute: "2-digit",
37
- second: "2-digit",
38
- hour12: false,
39
- });
40
-
41
- if (isToday) {
42
- return time;
31
+ const use12HourFormat: boolean = OneUptimeDate.getUserPrefers12HourFormat();
32
+
33
+ /*
34
+ * Today's buckets need no date on them - the explorer's own range picker
35
+ * already says which day is on screen. "Today" has to be decided in the
36
+ * configured timezone, or buckets either side of midnight there get the
37
+ * date added and dropped a few hours off.
38
+ */
39
+ if (
40
+ OneUptimeDate.areOnTheSameLocalDay(date, OneUptimeDate.getCurrentDate())
41
+ ) {
42
+ return OneUptimeDate.getLocalTimeString(date, {
43
+ includeSeconds: true,
44
+ use12HourFormat,
45
+ });
43
46
  }
44
47
 
45
- const dateStr: string = date.toLocaleDateString([], {
46
- month: "short",
47
- day: "numeric",
48
+ return OneUptimeDate.getDateAsLocalShortDateTimeString(date, {
49
+ includeSeconds: true,
50
+ use12HourFormat,
48
51
  });
49
-
50
- return `${dateStr}, ${time}`;
51
52
  }
52
53
 
53
54
  const HistogramTooltip: FunctionComponent<HistogramTooltipProps> = (
@@ -134,17 +134,32 @@ function pivotTimeseriesData(rows: Array<AnalyticsTimeseriesRow>): {
134
134
  };
135
135
  }
136
136
 
137
- function formatTickTime(time: string): string {
137
+ export function formatTickTime(time: string): string {
138
138
  const date: Date = OneUptimeDate.fromString(time);
139
139
 
140
140
  if (isNaN(date.getTime())) {
141
141
  return time;
142
142
  }
143
143
 
144
- return date.toLocaleTimeString([], {
145
- hour: "2-digit",
146
- minute: "2-digit",
147
- hour12: false,
144
+ return OneUptimeDate.getLocalTimeString(date, {
145
+ use12HourFormat: OneUptimeDate.getUserPrefers12HourFormat(),
146
+ });
147
+ }
148
+
149
+ /** The bucket a tooltip is pointing at, dated because it can be any day in the window. */
150
+ export function formatTooltipLabel(label: string | undefined): string {
151
+ if (!label) {
152
+ return "";
153
+ }
154
+
155
+ const date: Date = OneUptimeDate.fromString(label);
156
+
157
+ if (isNaN(date.getTime())) {
158
+ return label;
159
+ }
160
+
161
+ return OneUptimeDate.getDateAsLocalShortDateTimeString(date, {
162
+ use12HourFormat: OneUptimeDate.getUserPrefers12HourFormat(),
148
163
  });
149
164
  }
150
165
 
@@ -234,32 +249,10 @@ const AnalyticsTooltip: FunctionComponent<AnalyticsTooltipProps> = (
234
249
  0,
235
250
  );
236
251
 
237
- const formatTime: (label: string | undefined) => string = (
238
- label: string | undefined,
239
- ): string => {
240
- if (!label) {
241
- return "";
242
- }
243
-
244
- const date: Date = OneUptimeDate.fromString(label);
245
-
246
- if (isNaN(date.getTime())) {
247
- return label;
248
- }
249
-
250
- return date.toLocaleString([], {
251
- month: "short",
252
- day: "numeric",
253
- hour: "2-digit",
254
- minute: "2-digit",
255
- hour12: false,
256
- });
257
- };
258
-
259
252
  return (
260
253
  <div className="rounded-lg border border-gray-200/80 bg-white/95 px-3.5 py-2.5 shadow-lg backdrop-blur-sm">
261
254
  <p className="mb-2 border-b border-gray-100 pb-2 font-mono text-[11px] font-medium text-gray-400">
262
- {formatTime(props.label)}
255
+ {formatTooltipLabel(props.label)}
263
256
  </p>
264
257
  <div className="space-y-1">
265
258
  {entries.map((entry: { key: string; value: number; color: string }) => {
@@ -68,10 +68,8 @@ function formatTickTime(time: string): string {
68
68
  return time;
69
69
  }
70
70
 
71
- return date.toLocaleTimeString([], {
72
- hour: "2-digit",
73
- minute: "2-digit",
74
- hour12: false,
71
+ return OneUptimeDate.getLocalTimeString(date, {
72
+ use12HourFormat: OneUptimeDate.getUserPrefers12HourFormat(),
75
73
  });
76
74
  }
77
75
 
@@ -73,10 +73,8 @@ function formatTickTime(time: string): string {
73
73
  return time;
74
74
  }
75
75
 
76
- return date.toLocaleTimeString([], {
77
- hour: "2-digit",
78
- minute: "2-digit",
79
- hour12: false,
76
+ return OneUptimeDate.getLocalTimeString(date, {
77
+ use12HourFormat: OneUptimeDate.getUserPrefers12HourFormat(),
80
78
  });
81
79
  }
82
80
 
@@ -34,26 +34,27 @@ function formatTooltipTime(label: string | undefined): string {
34
34
  return label;
35
35
  }
36
36
 
37
- const now: Date = new Date();
38
- const isToday: boolean = date.toDateString() === now.toDateString();
37
+ const use12HourFormat: boolean = OneUptimeDate.getUserPrefers12HourFormat();
39
38
 
40
- const time: string = date.toLocaleTimeString([], {
41
- hour: "2-digit",
42
- minute: "2-digit",
43
- second: "2-digit",
44
- hour12: false,
45
- });
46
-
47
- if (isToday) {
48
- return time;
39
+ /*
40
+ * Today's buckets need no date on them - the explorer's own range picker
41
+ * already says which day is on screen. "Today" has to be decided in the
42
+ * configured timezone, or buckets either side of midnight there get the
43
+ * date added and dropped a few hours off.
44
+ */
45
+ if (
46
+ OneUptimeDate.areOnTheSameLocalDay(date, OneUptimeDate.getCurrentDate())
47
+ ) {
48
+ return OneUptimeDate.getLocalTimeString(date, {
49
+ includeSeconds: true,
50
+ use12HourFormat,
51
+ });
49
52
  }
50
53
 
51
- const dateStr: string = date.toLocaleDateString([], {
52
- month: "short",
53
- day: "numeric",
54
+ return OneUptimeDate.getDateAsLocalShortDateTimeString(date, {
55
+ includeSeconds: true,
56
+ use12HourFormat,
54
57
  });
55
-
56
- return `${dateStr}, ${time}`;
57
58
  }
58
59
 
59
60
  const TelemetryHistogramTooltip: FunctionComponent<
@@ -270,6 +270,42 @@ abstract class Navigation {
270
270
  this.navigateHook(-1);
271
271
  }
272
272
 
273
+ /**
274
+ * Return whether a value is an unambiguous same-origin path. Redirect
275
+ * targets must be paths rather than URLs so schemes, protocol-relative
276
+ * values, and backslash authority tricks can never reach a browser
277
+ * navigation sink.
278
+ */
279
+ public static isSafeInternalRoute(route: Route | string): boolean {
280
+ const routeValue: string = route.toString();
281
+
282
+ if (
283
+ !routeValue.startsWith("/") ||
284
+ routeValue.startsWith("//") ||
285
+ routeValue.includes("\\")
286
+ ) {
287
+ return false;
288
+ }
289
+
290
+ try {
291
+ /*
292
+ * URL accepts characters that Route deliberately rejects. Validate the
293
+ * exact sink contract here so a value marked safe cannot crash later
294
+ * when a caller constructs the Route used for navigation.
295
+ */
296
+ new Route(routeValue);
297
+
298
+ const parsedUrl: globalThis.URL = new window.URL(
299
+ routeValue,
300
+ window.location.origin,
301
+ );
302
+
303
+ return parsedUrl.origin === window.location.origin;
304
+ } catch {
305
+ return false;
306
+ }
307
+ }
308
+
273
309
  public static navigate(
274
310
  to: Route | URL,
275
311
  options?: {
@@ -288,6 +324,12 @@ abstract class Navigation {
288
324
  }
289
325
 
290
326
  if (options?.forceNavigate && to instanceof Route) {
327
+ if (!this.isSafeInternalRoute(to)) {
328
+ throw new BadDataException(
329
+ `Cannot force navigate to a non-internal route: ${finalUrl}`,
330
+ );
331
+ }
332
+
291
333
  window.location.href = finalUrl;
292
334
  return;
293
335
  }
package/Utils/Array.ts CHANGED
@@ -1,6 +1,43 @@
1
1
  import ObjectID from "../Types/ObjectID";
2
2
 
3
3
  export default class ArrayUtil {
4
+ /*
5
+ * Run handler over every item, at most `concurrency` of them in flight.
6
+ *
7
+ * For work that is spent waiting on the network rather than on the CPU -
8
+ * walking a list of customer domains and making a request against each -
9
+ * awaiting one at a time leaves the caller idle for almost all of its wall
10
+ * clock, and a single slow item delays every item behind it.
11
+ *
12
+ * Items are handed out in order, so a caller that has sorted by urgency still
13
+ * gets the urgent ones started first. handler is expected to deal with its
14
+ * own failures: as with Promise.all, the first rejection is what the caller
15
+ * sees, and it does not stop work already in flight.
16
+ */
17
+ public static async forEachWithConcurrency<T>(
18
+ array: Array<T>,
19
+ concurrency: number,
20
+ handler: (item: T) => Promise<void>,
21
+ ): Promise<void> {
22
+ let nextIndex: number = 0;
23
+
24
+ const worker: () => Promise<void> = async (): Promise<void> => {
25
+ while (nextIndex < array.length) {
26
+ const index: number = nextIndex++;
27
+ await handler(array[index] as T);
28
+ }
29
+ };
30
+
31
+ await Promise.all(
32
+ Array.from(
33
+ { length: Math.max(1, Math.min(concurrency, array.length)) },
34
+ (): Promise<void> => {
35
+ return worker();
36
+ },
37
+ ),
38
+ );
39
+ }
40
+
4
41
  public static mergeStringArrays(
5
42
  array1: Array<string>,
6
43
  array2: Array<string>,
@@ -0,0 +1,128 @@
1
+ import OneUptimeDate from "../Types/Date";
2
+
3
+ /*
4
+ * The cooldown that sits behind the "Reissue SSL Certificate" button on a
5
+ * custom domain.
6
+ *
7
+ * OneUptime orders certificates from Let's Encrypt against a single ACME
8
+ * account shared by every customer on the installation, so a button that any
9
+ * customer can press is a button that spends everybody's allowance. Two of
10
+ * Let's Encrypt's limits are the ones this guards:
11
+ *
12
+ * - 5 failed validations per account, per hostname, per hour. A domain whose
13
+ * CNAME is half-broken fails validation every single time, so an
14
+ * unthrottled button is a way to burn that hostname's whole hourly budget
15
+ * in seconds - including the budget the automated renewal cron needs.
16
+ * - 5 duplicate certificates per week for the same exact set of hostnames.
17
+ * A reissue is by definition a duplicate certificate: same hostname, no
18
+ * change. This is the limit a determined presser reaches second.
19
+ *
20
+ * Deliberately pure and synchronous, and deliberately in Common/Utils rather
21
+ * than Common/Server: the dashboard renders the very same countdown the API
22
+ * will enforce, so the button explains itself before it is pressed instead of
23
+ * only after the request comes back rejected.
24
+ *
25
+ * The cooldown is keyed off when a reissue was REQUESTED, not when one
26
+ * succeeded. A request that fails at the CA still cost a validation attempt,
27
+ * and a failing domain is exactly the domain a frustrated customer presses
28
+ * again - so a failed attempt must start the clock just as a successful one
29
+ * does.
30
+ */
31
+ export default class CertificateReissueUtil {
32
+ /*
33
+ * Note on the value: 24 hours permits 7 reissues a week, which is above the
34
+ * 5-duplicate-certificates-per-week limit described above. That is a
35
+ * deliberate choice, not an oversight - reaching it needs a customer to
36
+ * press the button on six consecutive days, and the failure that follows is
37
+ * a clear, temporary error from the CA rather than the sustained burst the
38
+ * per-hour validation limit punishes. The value is a single constant so the
39
+ * trade can be revisited in one place.
40
+ */
41
+ public static readonly COOLDOWN_IN_HOURS: number = 24;
42
+
43
+ /*
44
+ * The newest "requested at" stamp that is already out of cooldown.
45
+ *
46
+ * A row is eligible for another reissue when its stamp is null or is at or
47
+ * before this instant. The server turns this into the WHERE clause it
48
+ * claims the row with, so the comparison that decides eligibility is the
49
+ * same one on both sides.
50
+ */
51
+ public static getCooldownCutoff(now: Date): Date {
52
+ return OneUptimeDate.addRemoveHours(
53
+ OneUptimeDate.fromString(now),
54
+ -CertificateReissueUtil.COOLDOWN_IN_HOURS,
55
+ );
56
+ }
57
+
58
+ // When the domain may be reissued again, given its last request.
59
+ public static getNextReissueAllowedAt(lastRequestedAt: Date): Date {
60
+ return OneUptimeDate.addRemoveHours(
61
+ OneUptimeDate.fromString(lastRequestedAt),
62
+ CertificateReissueUtil.COOLDOWN_IN_HOURS,
63
+ );
64
+ }
65
+
66
+ /*
67
+ * A domain that has never been reissued is never cooling down, which is why
68
+ * null is accepted here rather than being the caller's problem.
69
+ */
70
+ public static isInCooldown(
71
+ lastRequestedAt: Date | null | undefined,
72
+ now: Date,
73
+ ): boolean {
74
+ if (!lastRequestedAt) {
75
+ return false;
76
+ }
77
+
78
+ return OneUptimeDate.isAfter(
79
+ CertificateReissueUtil.getNextReissueAllowedAt(lastRequestedAt),
80
+ OneUptimeDate.fromString(now),
81
+ );
82
+ }
83
+
84
+ /*
85
+ * How much longer the customer has to wait, in words. Whole minutes, since
86
+ * the countdown is only ever read by a person deciding whether to come back
87
+ * later.
88
+ */
89
+ public static getTimeRemainingText(lastRequestedAt: Date, now: Date): string {
90
+ const nextAllowedAt: Date =
91
+ CertificateReissueUtil.getNextReissueAllowedAt(lastRequestedAt);
92
+
93
+ const totalMinutes: number = Math.max(
94
+ 0,
95
+ OneUptimeDate.getMinutesBetweenTwoDates(
96
+ OneUptimeDate.fromString(now),
97
+ nextAllowedAt,
98
+ ),
99
+ );
100
+
101
+ if (totalMinutes < 1) {
102
+ return "less than a minute";
103
+ }
104
+
105
+ const hours: number = Math.floor(totalMinutes / 60);
106
+ const minutes: number = totalMinutes % 60;
107
+
108
+ const parts: Array<string> = [];
109
+
110
+ if (hours > 0) {
111
+ parts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`);
112
+ }
113
+
114
+ if (minutes > 0) {
115
+ parts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`);
116
+ }
117
+
118
+ return parts.join(" ");
119
+ }
120
+
121
+ /*
122
+ * The one sentence the API returns and the dashboard shows. Kept here so a
123
+ * customer reads the same explanation wherever they hit the limit.
124
+ */
125
+ public static getCooldownMessage(lastRequestedAt: Date, now: Date): string {
126
+ return `A certificate reissue was already requested for this domain in the last ${CertificateReissueUtil.COOLDOWN_IN_HOURS} hours. Certificates are issued by Let's Encrypt, which rate limits how often the same domain can be issued, so a reissue can only be requested once every ${CertificateReissueUtil.COOLDOWN_IN_HOURS} hours. Please try again in ${CertificateReissueUtil.getTimeRemainingText(lastRequestedAt, now)}.`;
127
+ }
128
+ }