@oneuptime/common 12.0.29 → 12.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/Index.ts +11 -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/UserOnCallCalendarFeed.ts +546 -0
- package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
- package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
- package/Server/API/OnCallCalendarAPI.ts +2194 -0
- package/Server/EnvironmentConfig.ts +138 -0
- package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
- package/Server/Services/Index.ts +12 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
- package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
- package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
- package/Server/Services/TeamMemberService.ts +489 -1
- package/Server/Services/TeamService.ts +50 -0
- 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/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/StartServer.ts +10 -0
- package/Server/Utils/Telemetry/AppMetrics.ts +51 -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/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
- package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -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/UserOnCallShiftReminderModels.test.ts +368 -0
- package/Tests/Server/API/Helpers.ts +8 -0
- package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -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 +369 -0
- package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -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/TeamDeleteMemberCleanup.test.ts +156 -0
- package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
- package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
- package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
- package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -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/ResponseSendCalendarResponse.test.ts +561 -0
- package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
- package/Tests/Types/Calendar/ICalendar.test.ts +488 -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/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Types/Calendar/ICalendar.ts +442 -0
- package/Types/Email/EmailTemplateType.ts +2 -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/build/dist/Models/DatabaseModels/Index.js +11 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- 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/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/OnCallCalendarAPI.js +1371 -0
- package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +102 -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/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.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/Index.js +12 -0
- package/build/dist/Server/Services/Index.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/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
- 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/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/ProjectOnCallCalendarFeedService.js +157 -0
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
- 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/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/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/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/Types/Calendar/ICalendar.js +290 -0
- package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- 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/package.json +1 -1
package/Server/Utils/Response.ts
CHANGED
|
@@ -26,6 +26,32 @@ import PositiveNumber from "../../Types/PositiveNumber";
|
|
|
26
26
|
import Route from "../../Types/API/Route";
|
|
27
27
|
import CaptureSpan from "./Telemetry/CaptureSpan";
|
|
28
28
|
import { GoogleTagManagerEnabled } from "../EnvironmentConfig";
|
|
29
|
+
import { createHash } from "crypto";
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
* How long a calendar client may reuse a feed body before asking again. The
|
|
33
|
+
* body cache behind the feed routes has the same TTL, so a client that honours
|
|
34
|
+
* this sees a new body no later than one it would have been served anyway.
|
|
35
|
+
*/
|
|
36
|
+
export const CALENDAR_RESPONSE_MAX_AGE_SECONDS: number = 300;
|
|
37
|
+
|
|
38
|
+
export const CALENDAR_RESPONSE_CONTENT_TYPE: string =
|
|
39
|
+
"text/calendar; charset=utf-8";
|
|
40
|
+
|
|
41
|
+
export const CALENDAR_RESPONSE_FILE_NAME: string = "oneuptime-on-call.ics";
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* The response headers a calendar feed carries, as a shape the API and its
|
|
45
|
+
* tests can share. `etag` may be passed bare or already quoted; the helper
|
|
46
|
+
* emits the quoted strong form either way. `lastModified` is the newest
|
|
47
|
+
* change to anything the feed was built from, and is what a client's
|
|
48
|
+
* If-Modified-Since is compared against.
|
|
49
|
+
*/
|
|
50
|
+
export interface CalendarResponseData {
|
|
51
|
+
body: string;
|
|
52
|
+
etag: string;
|
|
53
|
+
lastModified: Date;
|
|
54
|
+
}
|
|
29
55
|
|
|
30
56
|
/*
|
|
31
57
|
* Raster image types, which is to say the types that cannot carry script and
|
|
@@ -485,6 +511,114 @@ export default class Response {
|
|
|
485
511
|
oneUptimeResponse.end(javascript);
|
|
486
512
|
}
|
|
487
513
|
|
|
514
|
+
/*
|
|
515
|
+
* A strong ETag for a calendar body: the first 32 hex characters of the
|
|
516
|
+
* SHA-256 of the exact bytes sent, quoted. Strong rather than weak because
|
|
517
|
+
* the body is byte-deterministic for a given input (the iCalendar serializer
|
|
518
|
+
* orders properties and folds lines the same way every time), so two bodies
|
|
519
|
+
* with the same tag are byte-identical and a client may treat them so.
|
|
520
|
+
* Computed from the body rather than from an update timestamp so a cached
|
|
521
|
+
* body and a freshly rendered identical one carry the same tag.
|
|
522
|
+
*/
|
|
523
|
+
public static getCalendarETag(body: string): string {
|
|
524
|
+
return `"${createHash("sha256").update(body, "utf8").digest("hex").slice(0, 32)}"`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/*
|
|
528
|
+
* Send an iCalendar feed body with the headers calendar clients expect.
|
|
529
|
+
*
|
|
530
|
+
* Deliberately its own method rather than sendCustomResponse: that one
|
|
531
|
+
* starts from setNoCacheHeaders, which stamps `Pragma: no-cache` and cannot
|
|
532
|
+
* take it back, and a feed that says both "max-age=300" and "no-cache" is
|
|
533
|
+
* re-fetched on every open by clients that honour Pragma. Nor
|
|
534
|
+
* sendTextResponse, which sets no Content-Type at all and would let Express
|
|
535
|
+
* fall back to text/html.
|
|
536
|
+
*
|
|
537
|
+
* Conditional requests are left to Express: `res.send` answers 304 by itself
|
|
538
|
+
* when `req.fresh` is true (If-None-Match against the ETag set here, weak
|
|
539
|
+
* `W/` prefixes included, or If-Modified-Since against Last-Modified), and it
|
|
540
|
+
* strips the body for HEAD. Nothing here inspects the method.
|
|
541
|
+
*
|
|
542
|
+
* private, not public: the body is one person's (or one team's) on-call
|
|
543
|
+
* roster behind a bearer token in the URL, and a shared cache that stored it
|
|
544
|
+
* would hand it to the next requester of that URL regardless of who they
|
|
545
|
+
* are. Expires duplicates max-age for the clients that only read Expires.
|
|
546
|
+
*/
|
|
547
|
+
@CaptureSpan()
|
|
548
|
+
public static sendCalendarResponse(
|
|
549
|
+
_req: ExpressRequest,
|
|
550
|
+
res: ExpressResponse,
|
|
551
|
+
data: CalendarResponseData,
|
|
552
|
+
): void {
|
|
553
|
+
const oneUptimeResponse: OneUptimeResponse = res as OneUptimeResponse;
|
|
554
|
+
|
|
555
|
+
const etag: string = Response.normalizeStrongETag(data.etag);
|
|
556
|
+
|
|
557
|
+
const expiresAt: Date = new Date(
|
|
558
|
+
Date.now() + CALENDAR_RESPONSE_MAX_AGE_SECONDS * 1000,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
oneUptimeResponse.set("Content-Type", CALENDAR_RESPONSE_CONTENT_TYPE);
|
|
562
|
+
oneUptimeResponse.set(
|
|
563
|
+
"Content-Disposition",
|
|
564
|
+
`inline; filename="${CALENDAR_RESPONSE_FILE_NAME}"`,
|
|
565
|
+
);
|
|
566
|
+
oneUptimeResponse.set(
|
|
567
|
+
"Cache-Control",
|
|
568
|
+
`private, max-age=${CALENDAR_RESPONSE_MAX_AGE_SECONDS}`,
|
|
569
|
+
);
|
|
570
|
+
oneUptimeResponse.set("Expires", expiresAt.toUTCString());
|
|
571
|
+
oneUptimeResponse.set("ETag", etag);
|
|
572
|
+
oneUptimeResponse.set("Last-Modified", data.lastModified.toUTCString());
|
|
573
|
+
oneUptimeResponse.set("X-Content-Type-Options", "nosniff");
|
|
574
|
+
oneUptimeResponse.set("X-Robots-Tag", "noindex");
|
|
575
|
+
|
|
576
|
+
/*
|
|
577
|
+
* A middleware upstream of the route may already have stamped the
|
|
578
|
+
* no-cache trio (setNoCacheHeaders). Cache-Control and Expires were just
|
|
579
|
+
* overwritten; Pragma has no "do cache" value, so the only correct thing
|
|
580
|
+
* is for it not to be there.
|
|
581
|
+
*/
|
|
582
|
+
oneUptimeResponse.removeHeader("Pragma");
|
|
583
|
+
|
|
584
|
+
/*
|
|
585
|
+
* Never the body: it is a roster behind a bearer token, and the audit log
|
|
586
|
+
* is not where it belongs. Enough to know what went out and how big.
|
|
587
|
+
*/
|
|
588
|
+
oneUptimeResponse.logBody = {
|
|
589
|
+
contentType: CALENDAR_RESPONSE_CONTENT_TYPE,
|
|
590
|
+
bytes: Buffer.byteLength(data.body, "utf8"),
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
oneUptimeResponse.status(200).send(data.body);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/*
|
|
597
|
+
* Accept `abc`, `"abc"` or `W/"abc"` and return the strong, quoted form.
|
|
598
|
+
* The tag is content-derived (see getCalendarETag), so weakening it would
|
|
599
|
+
* only lose the byte-identical guarantee the client is entitled to.
|
|
600
|
+
*/
|
|
601
|
+
private static normalizeStrongETag(etag: string): string {
|
|
602
|
+
let value: string = etag.trim();
|
|
603
|
+
|
|
604
|
+
if (value.startsWith("W/")) {
|
|
605
|
+
value = value.slice(2).trim();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
609
|
+
value = value.slice(1, -1);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/*
|
|
613
|
+
* RFC 7232: an entity-tag is quoted and may not itself contain a double
|
|
614
|
+
* quote. Strip anything that would break the header rather than emit an
|
|
615
|
+
* unparseable one.
|
|
616
|
+
*/
|
|
617
|
+
value = value.replace(/["\r\n]/g, "");
|
|
618
|
+
|
|
619
|
+
return `"${value}"`;
|
|
620
|
+
}
|
|
621
|
+
|
|
488
622
|
public static setNoCacheHeaders(res: ExpressResponse): void {
|
|
489
623
|
const oneUptimeResponse: OneUptimeResponse = res as OneUptimeResponse;
|
|
490
624
|
|
|
@@ -3,6 +3,7 @@ import CommonAPI from "../API/Index";
|
|
|
3
3
|
import { StatusAPIOptions } from "../API/StatusAPI";
|
|
4
4
|
import {
|
|
5
5
|
AppVersion,
|
|
6
|
+
EncryptionSecretWarning,
|
|
6
7
|
GoogleTagManagerEnabled,
|
|
7
8
|
TrustedProxyHops,
|
|
8
9
|
getFrontendEnvVars,
|
|
@@ -314,6 +315,15 @@ const init: InitFunction = async (
|
|
|
314
315
|
|
|
315
316
|
logger.info(`App Version: ${AppVersion.toString()}`);
|
|
316
317
|
|
|
318
|
+
/*
|
|
319
|
+
* Said once per process at boot, where an operator reading the startup log
|
|
320
|
+
* will see it. EnvironmentConfig computes the message but cannot log it
|
|
321
|
+
* (Logger depends on EnvironmentConfig), so the entrypoint does.
|
|
322
|
+
*/
|
|
323
|
+
if (EncryptionSecretWarning) {
|
|
324
|
+
logger.warn(EncryptionSecretWarning);
|
|
325
|
+
}
|
|
326
|
+
|
|
317
327
|
await Express.launchApplication(appName, port);
|
|
318
328
|
LocalCache.setString("app", "name", appName);
|
|
319
329
|
|
|
@@ -239,4 +239,55 @@ export default class AppMetrics {
|
|
|
239
239
|
|
|
240
240
|
return this.ingestDroppedCounter;
|
|
241
241
|
}
|
|
242
|
+
|
|
243
|
+
// -- On-call calendar feeds -------------------------------------------
|
|
244
|
+
|
|
245
|
+
/*
|
|
246
|
+
* The one bounded attribute these carry: which kind of feed was rendered.
|
|
247
|
+
* "user" (personal), "schedule" (one schedule, shared link) or "project"
|
|
248
|
+
* (every schedule in a project). Never the token, the user or the project
|
|
249
|
+
* id -- those are unbounded and belong on traces and logs.
|
|
250
|
+
*/
|
|
251
|
+
public static readonly ON_CALL_CALENDAR_FEED_KIND_ATTRIBUTE: string =
|
|
252
|
+
"oneuptime.oncall_calendar.feed_kind";
|
|
253
|
+
|
|
254
|
+
private static onCallCalendarRenderDuration: TelemetryHistogram | null = null;
|
|
255
|
+
private static onCallCalendarRenderEvents: TelemetryHistogram | null = null;
|
|
256
|
+
|
|
257
|
+
/*
|
|
258
|
+
* Wall-clock time to materialise the shifts and serialise a feed body, on a
|
|
259
|
+
* cache miss. Rendering expands every layer of every candidate schedule
|
|
260
|
+
* across a window of up to 180 days, so this is the number that says whether
|
|
261
|
+
* the render cap and the schedule-level cache are doing their job.
|
|
262
|
+
*/
|
|
263
|
+
public static getOnCallCalendarRenderDuration(): TelemetryHistogram {
|
|
264
|
+
if (!this.onCallCalendarRenderDuration) {
|
|
265
|
+
this.onCallCalendarRenderDuration = Telemetry.getHistogram({
|
|
266
|
+
name: "oncall_calendar_render_duration_ms",
|
|
267
|
+
description:
|
|
268
|
+
"Time taken to render an on-call calendar feed body on a cache miss, partitioned by feed kind.",
|
|
269
|
+
unit: "ms",
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return this.onCallCalendarRenderDuration;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/*
|
|
277
|
+
* How many VEVENTs a rendered feed carried. Distribution matters more than
|
|
278
|
+
* the total: a fat tail near MAX_EVENTS means windows are being shrunk to
|
|
279
|
+
* fit and subscribers are losing the far end of their calendar.
|
|
280
|
+
*/
|
|
281
|
+
public static getOnCallCalendarRenderEvents(): TelemetryHistogram {
|
|
282
|
+
if (!this.onCallCalendarRenderEvents) {
|
|
283
|
+
this.onCallCalendarRenderEvents = Telemetry.getHistogram({
|
|
284
|
+
name: "oncall_calendar_render_events",
|
|
285
|
+
description:
|
|
286
|
+
"Number of calendar events in a rendered on-call calendar feed, partitioned by feed kind.",
|
|
287
|
+
unit: "1",
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return this.onCallCalendarRenderEvents;
|
|
292
|
+
}
|
|
242
293
|
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
11
|
+
import React from "react";
|
|
12
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* The shared "put this in your calendar" block. What matters here is that each
|
|
16
|
+
* of the three subscribe actions points at the RIGHT form of the same link
|
|
17
|
+
* (Google wants the https URL behind its cid parameter, Apple wants webcal,
|
|
18
|
+
* Outlook wants webcal pasted), that the secret stays hidden until asked for,
|
|
19
|
+
* and that the schedule filter - when the block is narrowed to one schedule -
|
|
20
|
+
* reaches all three forms, not just the visible one.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
jest.mock("react-i18next", () => {
|
|
24
|
+
return {
|
|
25
|
+
useTranslation: () => {
|
|
26
|
+
return {
|
|
27
|
+
t: (key: string, options?: { defaultValue?: string }): string => {
|
|
28
|
+
return options?.defaultValue ?? key;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
import CalendarFeedLinks from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeedLinks";
|
|
36
|
+
import { FeedUrls } from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/CalendarFeedTypes";
|
|
37
|
+
import {
|
|
38
|
+
REACHABILITY_COPY,
|
|
39
|
+
REFRESH_CADENCE_COPY,
|
|
40
|
+
buildGoogleAddUrl,
|
|
41
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/CalendarFeedUtil";
|
|
42
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
43
|
+
|
|
44
|
+
const HTTPS_URL: string =
|
|
45
|
+
"https://oneuptime.example.com/api/on-call-calendar/user/abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG/shifts.ics";
|
|
46
|
+
const WEBCAL_URL: string = HTTPS_URL.replace("https:", "webcals:");
|
|
47
|
+
|
|
48
|
+
const URLS: FeedUrls = {
|
|
49
|
+
https: HTTPS_URL,
|
|
50
|
+
webcal: WEBCAL_URL,
|
|
51
|
+
googleAdd: buildGoogleAddUrl(HTTPS_URL),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const SCHEDULE_ID: ObjectID = new ObjectID(
|
|
55
|
+
"33333333-3333-4333-8333-333333333333",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const windowOpenMock: MockFunction = getJestMockFunction();
|
|
59
|
+
|
|
60
|
+
describe("CalendarFeedLinks", () => {
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
windowOpenMock.mockReset();
|
|
63
|
+
Object.defineProperty(window, "open", {
|
|
64
|
+
value: windowOpenMock,
|
|
65
|
+
writable: true,
|
|
66
|
+
configurable: true,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
cleanup();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("keeps the https link hidden until the reader reveals it", () => {
|
|
75
|
+
render(<CalendarFeedLinks urls={URLS} />);
|
|
76
|
+
|
|
77
|
+
expect(screen.queryByText(HTTPS_URL)).not.toBeInTheDocument();
|
|
78
|
+
expect(screen.getByRole("hidden-text")).toBeInTheDocument();
|
|
79
|
+
|
|
80
|
+
fireEvent.click(screen.getByRole("hidden-text"));
|
|
81
|
+
|
|
82
|
+
expect(screen.getByRole("revealed-text")).toHaveTextContent(HTTPS_URL);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("the copy buttons carry the https and webcal forms respectively", () => {
|
|
86
|
+
render(<CalendarFeedLinks urls={URLS} />);
|
|
87
|
+
|
|
88
|
+
// Copy https link (the button next to the hidden text) and Copy webcal link.
|
|
89
|
+
expect(
|
|
90
|
+
screen.getByRole("button", { name: "Copy https link" }),
|
|
91
|
+
).toBeInTheDocument();
|
|
92
|
+
expect(
|
|
93
|
+
screen.getByRole("button", {
|
|
94
|
+
name: "Outlook: Add calendar, then Subscribe from web, then paste this link.",
|
|
95
|
+
}),
|
|
96
|
+
).toHaveTextContent("Copy webcal link");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("the Apple / other apps action is a real anchor to the webcal URL", () => {
|
|
100
|
+
render(<CalendarFeedLinks urls={URLS} />);
|
|
101
|
+
|
|
102
|
+
const anchor: HTMLElement = screen.getByTestId("calendar-feed-webcal");
|
|
103
|
+
|
|
104
|
+
expect(anchor.tagName).toBe("A");
|
|
105
|
+
expect(anchor).toHaveAttribute("href", WEBCAL_URL);
|
|
106
|
+
expect(anchor).toHaveTextContent("Apple / other apps");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("the Google button opens the cid deep link in a new, isolated window", () => {
|
|
110
|
+
render(<CalendarFeedLinks urls={URLS} />);
|
|
111
|
+
|
|
112
|
+
fireEvent.click(screen.getByTestId("calendar-feed-google"));
|
|
113
|
+
|
|
114
|
+
expect(windowOpenMock).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(windowOpenMock).toHaveBeenCalledWith(
|
|
116
|
+
URLS.googleAdd,
|
|
117
|
+
"_blank",
|
|
118
|
+
"noopener,noreferrer",
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("a schedule filter narrows every form of the link, including the Google one", () => {
|
|
123
|
+
render(<CalendarFeedLinks urls={URLS} scheduleId={SCHEDULE_ID} />);
|
|
124
|
+
|
|
125
|
+
const filteredHttps: string = `${HTTPS_URL}?schedule=${SCHEDULE_ID.toString()}`;
|
|
126
|
+
|
|
127
|
+
fireEvent.click(screen.getByRole("hidden-text"));
|
|
128
|
+
expect(screen.getByRole("revealed-text")).toHaveTextContent(filteredHttps);
|
|
129
|
+
|
|
130
|
+
expect(screen.getByTestId("calendar-feed-webcal")).toHaveAttribute(
|
|
131
|
+
"href",
|
|
132
|
+
`${WEBCAL_URL}?schedule=${SCHEDULE_ID.toString()}`,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
fireEvent.click(screen.getByTestId("calendar-feed-google"));
|
|
136
|
+
expect(windowOpenMock).toHaveBeenCalledWith(
|
|
137
|
+
buildGoogleAddUrl(filteredHttps),
|
|
138
|
+
"_blank",
|
|
139
|
+
"noopener,noreferrer",
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a string schedule id works the same as an ObjectID", () => {
|
|
144
|
+
render(<CalendarFeedLinks urls={URLS} scheduleId="sched-string" />);
|
|
145
|
+
|
|
146
|
+
expect(screen.getByTestId("calendar-feed-webcal")).toHaveAttribute(
|
|
147
|
+
"href",
|
|
148
|
+
`${WEBCAL_URL}?schedule=sched-string`,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("server warnings render only when the server sent them", () => {
|
|
153
|
+
const { rerender } = render(<CalendarFeedLinks urls={URLS} />);
|
|
154
|
+
|
|
155
|
+
expect(
|
|
156
|
+
screen.queryByTestId("calendar-feed-host-warning"),
|
|
157
|
+
).not.toBeInTheDocument();
|
|
158
|
+
expect(
|
|
159
|
+
screen.queryByTestId("calendar-feed-protocol-warning"),
|
|
160
|
+
).not.toBeInTheDocument();
|
|
161
|
+
expect(
|
|
162
|
+
screen.queryByTestId("calendar-feed-truncated-warning"),
|
|
163
|
+
).not.toBeInTheDocument();
|
|
164
|
+
|
|
165
|
+
rerender(
|
|
166
|
+
<CalendarFeedLinks
|
|
167
|
+
urls={URLS}
|
|
168
|
+
hostWarning="Set HOST to your public hostname"
|
|
169
|
+
protocolWarning="This link will travel unencrypted"
|
|
170
|
+
lastRenderTruncated={true}
|
|
171
|
+
/>,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(screen.getByTestId("calendar-feed-host-warning")).toHaveTextContent(
|
|
175
|
+
"Set HOST to your public hostname",
|
|
176
|
+
);
|
|
177
|
+
expect(
|
|
178
|
+
screen.getByTestId("calendar-feed-protocol-warning"),
|
|
179
|
+
).toHaveTextContent("This link will travel unencrypted");
|
|
180
|
+
expect(
|
|
181
|
+
screen.getByTestId("calendar-feed-truncated-warning"),
|
|
182
|
+
).toHaveTextContent(
|
|
183
|
+
"shortened because it would have exceeded the event limit",
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("the refresh alert carries the cadence copy, the reachability sentence and a docs link", () => {
|
|
188
|
+
render(<CalendarFeedLinks urls={URLS} />);
|
|
189
|
+
|
|
190
|
+
const alert: HTMLElement = screen.getByTestId(
|
|
191
|
+
"calendar-feed-refresh-alert",
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(alert).toHaveTextContent(REFRESH_CADENCE_COPY);
|
|
195
|
+
expect(alert).toHaveTextContent(REACHABILITY_COPY);
|
|
196
|
+
expect(alert).toHaveTextContent(
|
|
197
|
+
"Google Calendar and Outlook on the web fetch this link from their servers; it must be reachable from the internet.",
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const docsLink: HTMLElement = screen.getByText(
|
|
201
|
+
"Troubleshooting and per-app steps",
|
|
202
|
+
);
|
|
203
|
+
const anchor: HTMLElement | null = docsLink.closest("a");
|
|
204
|
+
|
|
205
|
+
expect(anchor).not.toBeNull();
|
|
206
|
+
expect(anchor!.getAttribute("href")).toContain("/on-call/calendar-feeds");
|
|
207
|
+
expect(anchor!.getAttribute("target")).toBe("_blank");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("the refresh alert can be switched off when the page renders it once elsewhere", () => {
|
|
211
|
+
render(<CalendarFeedLinks urls={URLS} showRefreshAlert={false} />);
|
|
212
|
+
|
|
213
|
+
expect(
|
|
214
|
+
screen.queryByTestId("calendar-feed-refresh-alert"),
|
|
215
|
+
).not.toBeInTheDocument();
|
|
216
|
+
// The links themselves are still there.
|
|
217
|
+
expect(screen.getByTestId("calendar-feed-webcal")).toBeInTheDocument();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("idPrefix namespaces every test id so two blocks on one page stay distinct", () => {
|
|
221
|
+
render(<CalendarFeedLinks urls={URLS} idPrefix="shared" />);
|
|
222
|
+
|
|
223
|
+
expect(screen.getByTestId("shared-links")).toBeInTheDocument();
|
|
224
|
+
expect(screen.getByTestId("shared-google")).toBeInTheDocument();
|
|
225
|
+
expect(screen.getByTestId("shared-webcal")).toBeInTheDocument();
|
|
226
|
+
expect(screen.getByTestId("shared-refresh-alert")).toBeInTheDocument();
|
|
227
|
+
expect(screen.queryByTestId("calendar-feed-links")).not.toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
});
|