@oneuptime/common 10.2.1 → 10.2.2

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 (35) hide show
  1. package/Models/DatabaseModels/Index.ts +2 -0
  2. package/Models/DatabaseModels/ProjectOidc.ts +705 -0
  3. package/Server/API/ProjectOIDC.ts +73 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1778506655291-AddProjectOIDC.ts +79 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  6. package/Server/Services/Index.ts +2 -0
  7. package/Server/Services/OnCallDutyPolicyScheduleService.ts +139 -26
  8. package/Server/Services/ProjectOidcService.ts +10 -0
  9. package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +155 -0
  10. package/Types/Permission.ts +42 -0
  11. package/UI/Components/Calendar/Calendar.css +257 -0
  12. package/UI/Components/Calendar/Calendar.tsx +22 -11
  13. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  14. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  15. package/build/dist/Models/DatabaseModels/ProjectOidc.js +727 -0
  16. package/build/dist/Models/DatabaseModels/ProjectOidc.js.map +1 -0
  17. package/build/dist/Server/API/ProjectOIDC.js +45 -0
  18. package/build/dist/Server/API/ProjectOIDC.js.map +1 -0
  19. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778506655291-AddProjectOIDC.js +34 -0
  20. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778506655291-AddProjectOIDC.js.map +1 -0
  21. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  22. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  23. package/build/dist/Server/Services/Index.js +2 -0
  24. package/build/dist/Server/Services/Index.js.map +1 -1
  25. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +106 -17
  26. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  27. package/build/dist/Server/Services/ProjectOidcService.js +9 -0
  28. package/build/dist/Server/Services/ProjectOidcService.js.map +1 -0
  29. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +86 -0
  30. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -0
  31. package/build/dist/Types/Permission.js +40 -0
  32. package/build/dist/Types/Permission.js.map +1 -1
  33. package/build/dist/UI/Components/Calendar/Calendar.js +12 -10
  34. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  35. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ import ProjectOidcService, {
2
+ Service as ProjectOidcServiceType,
3
+ } from "../Services/ProjectOidcService";
4
+ import {
5
+ ExpressRequest,
6
+ ExpressResponse,
7
+ NextFunction,
8
+ } from "../Utils/Express";
9
+ import Response from "../Utils/Response";
10
+ import BaseAPI from "./BaseAPI";
11
+ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
12
+ import BadDataException from "../../Types/Exception/BadDataException";
13
+ import ObjectID from "../../Types/ObjectID";
14
+ import PositiveNumber from "../../Types/PositiveNumber";
15
+ import ProjectOIDC from "../../Models/DatabaseModels/ProjectOidc";
16
+
17
+ export default class ProjectOidcAPI extends BaseAPI<
18
+ ProjectOIDC,
19
+ ProjectOidcServiceType
20
+ > {
21
+ public constructor() {
22
+ super(ProjectOIDC, ProjectOidcService);
23
+
24
+ // OIDC Fetch API
25
+ this.router.post(
26
+ `${new this.entityType()
27
+ .getCrudApiPath()
28
+ ?.toString()}/:projectId/oidc-list`,
29
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
30
+ try {
31
+ const projectId: ObjectID = new ObjectID(
32
+ req.params["projectId"] as string,
33
+ );
34
+
35
+ if (!projectId) {
36
+ return Response.sendErrorResponse(
37
+ req,
38
+ res,
39
+ new BadDataException("Invalid project id."),
40
+ );
41
+ }
42
+
43
+ const oidc: Array<ProjectOIDC> = await this.service.findBy({
44
+ query: {
45
+ projectId: projectId,
46
+ isEnabled: true,
47
+ },
48
+ limit: LIMIT_PER_PROJECT,
49
+ skip: 0,
50
+ select: {
51
+ name: true,
52
+ description: true,
53
+ _id: true,
54
+ },
55
+ props: {
56
+ isRoot: true,
57
+ },
58
+ });
59
+
60
+ return Response.sendEntityArrayResponse(
61
+ req,
62
+ res,
63
+ oidc,
64
+ new PositiveNumber(oidc.length),
65
+ ProjectOIDC,
66
+ );
67
+ } catch (err) {
68
+ return next(err);
69
+ }
70
+ },
71
+ );
72
+ }
73
+ }
@@ -0,0 +1,79 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddProjectOIDC1778506655291 implements MigrationInterface {
4
+ name = "AddProjectOIDC1778506655291";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `CREATE TABLE "ProjectOIDC" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(100) NOT NULL, "description" character varying NOT NULL, "discoveryURL" text NOT NULL, "issuerURL" text NOT NULL, "clientId" character varying(100) NOT NULL, "clientSecret" character varying NOT NULL, "scopes" character varying(100) NOT NULL, "emailClaimName" character varying(100) NOT NULL, "nameClaimName" character varying(100) NOT NULL, "createdByUserId" uuid, "deletedByUserId" uuid, "isEnabled" boolean NOT NULL DEFAULT false, "isTested" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_22abf8119bac3f7f4f9f03b201f" PRIMARY KEY ("_id"))`,
9
+ );
10
+ await queryRunner.query(
11
+ `CREATE INDEX "IDX_b5b93c3e2885549c370b816194" ON "ProjectOIDC" ("projectId") `,
12
+ );
13
+ await queryRunner.query(
14
+ `CREATE TABLE "ProjectOidcTeam" ("projectOidcId" uuid NOT NULL, "teamId" uuid NOT NULL, CONSTRAINT "PK_bdae022f28d23653e2aa4a40abb" PRIMARY KEY ("projectOidcId", "teamId"))`,
15
+ );
16
+ await queryRunner.query(
17
+ `CREATE INDEX "IDX_8c317a3effac6698ad8dfbc82d" ON "ProjectOidcTeam" ("projectOidcId") `,
18
+ );
19
+ await queryRunner.query(
20
+ `CREATE INDEX "IDX_456cebe2924528d694b29f91bc" ON "ProjectOidcTeam" ("teamId") `,
21
+ );
22
+ await queryRunner.query(
23
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
24
+ );
25
+ await queryRunner.query(
26
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
27
+ );
28
+ await queryRunner.query(
29
+ `ALTER TABLE "ProjectOIDC" ADD CONSTRAINT "FK_b5b93c3e2885549c370b8161940" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
30
+ );
31
+ await queryRunner.query(
32
+ `ALTER TABLE "ProjectOIDC" ADD CONSTRAINT "FK_df47207e1005bef42ca062f7a4b" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
33
+ );
34
+ await queryRunner.query(
35
+ `ALTER TABLE "ProjectOIDC" ADD CONSTRAINT "FK_3f386ff54e38e36f4c016187648" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
36
+ );
37
+ await queryRunner.query(
38
+ `ALTER TABLE "ProjectOidcTeam" ADD CONSTRAINT "FK_8c317a3effac6698ad8dfbc82d4" FOREIGN KEY ("projectOidcId") REFERENCES "ProjectOIDC"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
39
+ );
40
+ await queryRunner.query(
41
+ `ALTER TABLE "ProjectOidcTeam" ADD CONSTRAINT "FK_456cebe2924528d694b29f91bc9" FOREIGN KEY ("teamId") REFERENCES "Team"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
42
+ );
43
+ }
44
+
45
+ public async down(queryRunner: QueryRunner): Promise<void> {
46
+ await queryRunner.query(
47
+ `ALTER TABLE "ProjectOidcTeam" DROP CONSTRAINT "FK_456cebe2924528d694b29f91bc9"`,
48
+ );
49
+ await queryRunner.query(
50
+ `ALTER TABLE "ProjectOidcTeam" DROP CONSTRAINT "FK_8c317a3effac6698ad8dfbc82d4"`,
51
+ );
52
+ await queryRunner.query(
53
+ `ALTER TABLE "ProjectOIDC" DROP CONSTRAINT "FK_3f386ff54e38e36f4c016187648"`,
54
+ );
55
+ await queryRunner.query(
56
+ `ALTER TABLE "ProjectOIDC" DROP CONSTRAINT "FK_df47207e1005bef42ca062f7a4b"`,
57
+ );
58
+ await queryRunner.query(
59
+ `ALTER TABLE "ProjectOIDC" DROP CONSTRAINT "FK_b5b93c3e2885549c370b8161940"`,
60
+ );
61
+ await queryRunner.query(
62
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
63
+ );
64
+ await queryRunner.query(
65
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
66
+ );
67
+ await queryRunner.query(
68
+ `DROP INDEX "public"."IDX_456cebe2924528d694b29f91bc"`,
69
+ );
70
+ await queryRunner.query(
71
+ `DROP INDEX "public"."IDX_8c317a3effac6698ad8dfbc82d"`,
72
+ );
73
+ await queryRunner.query(`DROP TABLE "ProjectOidcTeam"`);
74
+ await queryRunner.query(
75
+ `DROP INDEX "public"."IDX_b5b93c3e2885549c370b816194"`,
76
+ );
77
+ await queryRunner.query(`DROP TABLE "ProjectOIDC"`);
78
+ }
79
+ }
@@ -312,6 +312,7 @@ import { AddOwnerRuleInheritFlags1778413144103 } from "./1778413144103-AddOwnerR
312
312
  import { AddAlertIsPrivate1778438949454 } from "./1778438949454-AddAlertIsPrivate";
313
313
  import { AddPrivacyRules1778440665575 } from "./1778440665575-AddPrivacyRules";
314
314
  import { AddEpisodePrivacyRules1778442385970 } from "./1778442385970-AddEpisodePrivacyRules";
315
+ import { AddProjectOIDC1778506655291 } from "./1778506655291-AddProjectOIDC";
315
316
  export default [
316
317
  InitialMigration,
317
318
  MigrationName1717678334852,
@@ -627,4 +628,5 @@ export default [
627
628
  AddAlertIsPrivate1778438949454,
628
629
  AddPrivacyRules1778440665575,
629
630
  AddEpisodePrivacyRules1778442385970,
631
+ AddProjectOIDC1778506655291,
630
632
  ];
@@ -85,6 +85,7 @@ import ProfileSampleService from "./ProfileSampleService";
85
85
  // Project SMTP Config.
86
86
  import ProjectSmtpConfigService from "./ProjectSmtpConfigService";
87
87
  import ProjectSsoService from "./ProjectSsoService";
88
+ import ProjectOidcService from "./ProjectOidcService";
88
89
  import PromoCodeService from "./PromoCodeService";
89
90
  import EnterpriseLicenseService from "./EnterpriseLicenseService";
90
91
  import OpenSourceDeploymentService from "./OpenSourceDeploymentService";
@@ -300,6 +301,7 @@ const services: Array<BaseService> = [
300
301
  AIAgentTaskLogService,
301
302
  AIAgentTaskPullRequestService,
302
303
  ProjectSsoService,
304
+ ProjectOidcService,
303
305
 
304
306
  ScheduledMaintenanceCustomFieldService,
305
307
  ScheduledMaintenanceInternalNoteService,
@@ -1,12 +1,18 @@
1
1
  import DatabaseService from "./DatabaseService";
2
2
  import OnCallDutyPolicyScheduleLayerService from "./OnCallDutyPolicyScheduleLayerService";
3
3
  import OnCallDutyPolicyScheduleLayerUserService from "./OnCallDutyPolicyScheduleLayerUserService";
4
+ import OnCallDutyPolicyUserOverrideService from "./OnCallDutyPolicyUserOverrideService";
4
5
  import SortOrder from "../../Types/BaseDatabase/SortOrder";
5
6
  import CalendarEvent from "../../Types/Calendar/CalendarEvent";
6
7
  import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
7
8
  import OneUptimeDate from "../../Types/Date";
8
9
  import ObjectID from "../../Types/ObjectID";
9
10
  import LayerUtil, { LayerProps } from "../../Types/OnCallDutyPolicy/Layer";
11
+ import UserOverrideUtil, {
12
+ UserOverrideRecord,
13
+ } from "../../Types/OnCallDutyPolicy/UserOverrideUtil";
14
+ import OnCallDutyPolicyUserOverride from "../../Models/DatabaseModels/OnCallDutyPolicyUserOverride";
15
+ import QueryHelper from "../Types/Database/QueryHelper";
10
16
  import OnCallDutyPolicyScheduleLayer from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleLayer";
11
17
  import OnCallDutyPolicyScheduleLayerUser from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser";
12
18
  import User from "../../Models/DatabaseModels/User";
@@ -914,9 +920,11 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
914
920
  return resultReturn;
915
921
  }
916
922
 
917
- private async getScheduleLayerProps(data: {
918
- scheduleId: ObjectID;
919
- }): Promise<Array<LayerProps>> {
923
+ private async getScheduleLayerProps(data: { scheduleId: ObjectID }): Promise<{
924
+ layerProps: Array<LayerProps>;
925
+ projectId: ObjectID | null;
926
+ scheduleUserIds: Array<ObjectID>;
927
+ }> {
920
928
  // get schedule layers.
921
929
 
922
930
  const scheduleId: ObjectID = data.scheduleId;
@@ -968,23 +976,34 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
968
976
  });
969
977
 
970
978
  const layerProps: Array<LayerProps> = [];
979
+ const scheduleUserIds: Array<ObjectID> = [];
980
+ const seenUserIds: Set<string> = new Set<string>();
971
981
 
972
982
  for (const layer of layers) {
983
+ const usersForLayer: Array<User> = layerUsers
984
+ .filter((layerUser: OnCallDutyPolicyScheduleLayerUser) => {
985
+ return (
986
+ layerUser.onCallDutyPolicyScheduleLayerId?.toString() ===
987
+ layer.id?.toString()
988
+ );
989
+ })
990
+ .map((layerUser: OnCallDutyPolicyScheduleLayerUser) => {
991
+ return layerUser.user!;
992
+ })
993
+ .filter((user: User) => {
994
+ return Boolean(user);
995
+ });
996
+
997
+ for (const user of usersForLayer) {
998
+ const idStr: string = user.id?.toString() || "";
999
+ if (idStr && !seenUserIds.has(idStr)) {
1000
+ seenUserIds.add(idStr);
1001
+ scheduleUserIds.push(user.id!);
1002
+ }
1003
+ }
1004
+
973
1005
  layerProps.push({
974
- users:
975
- layerUsers
976
- .filter((layerUser: OnCallDutyPolicyScheduleLayerUser) => {
977
- return (
978
- layerUser.onCallDutyPolicyScheduleLayerId?.toString() ===
979
- layer.id?.toString()
980
- );
981
- })
982
- .map((layerUser: OnCallDutyPolicyScheduleLayerUser) => {
983
- return layerUser.user!;
984
- })
985
- .filter((user: User) => {
986
- return Boolean(user);
987
- }) || [],
1006
+ users: usersForLayer,
988
1007
  startDateTimeOfLayer: layer.startsAt!,
989
1008
  restrictionTimes: layer.restrictionTimes!,
990
1009
  rotation: layer.rotation!,
@@ -992,7 +1011,65 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
992
1011
  });
993
1012
  }
994
1013
 
995
- return layerProps;
1014
+ const projectId: ObjectID | null = layers[0]?.projectId || null;
1015
+
1016
+ return { layerProps, projectId, scheduleUserIds };
1017
+ }
1018
+
1019
+ private async fetchOverridesForSchedule(data: {
1020
+ projectId: ObjectID;
1021
+ scheduleUserIds: Array<ObjectID>;
1022
+ windowStart: Date;
1023
+ windowEnd: Date;
1024
+ }): Promise<Array<UserOverrideRecord>> {
1025
+ if (data.scheduleUserIds.length === 0) {
1026
+ return [];
1027
+ }
1028
+
1029
+ const overrides: Array<OnCallDutyPolicyUserOverride> =
1030
+ await OnCallDutyPolicyUserOverrideService.findBy({
1031
+ query: {
1032
+ projectId: data.projectId,
1033
+ startsAt: QueryHelper.lessThanEqualTo(data.windowEnd),
1034
+ endsAt: QueryHelper.greaterThanEqualTo(data.windowStart),
1035
+ },
1036
+ select: {
1037
+ startsAt: true,
1038
+ endsAt: true,
1039
+ overrideUserId: true,
1040
+ routeAlertsToUserId: true,
1041
+ onCallDutyPolicyId: true,
1042
+ },
1043
+ sort: {
1044
+ startsAt: SortOrder.Ascending,
1045
+ },
1046
+ limit: LIMIT_PER_PROJECT,
1047
+ skip: 0,
1048
+ props: {
1049
+ isRoot: true,
1050
+ },
1051
+ });
1052
+
1053
+ const scheduleUserIdSet: Set<string> = new Set<string>(
1054
+ data.scheduleUserIds.map((id: ObjectID) => {
1055
+ return id.toString();
1056
+ }),
1057
+ );
1058
+
1059
+ return overrides
1060
+ .filter((o: OnCallDutyPolicyUserOverride) => {
1061
+ const overrideUserId: string = o.overrideUserId?.toString() || "";
1062
+ return scheduleUserIdSet.has(overrideUserId);
1063
+ })
1064
+ .map((o: OnCallDutyPolicyUserOverride): UserOverrideRecord => {
1065
+ return {
1066
+ overrideUserId: o.overrideUserId?.toString() || "",
1067
+ routeAlertsToUserId: o.routeAlertsToUserId?.toString() || "",
1068
+ startsAt: o.startsAt!,
1069
+ endsAt: o.endsAt!,
1070
+ onCallDutyPolicyId: o.onCallDutyPolicyId?.toString() || null,
1071
+ };
1072
+ });
996
1073
  }
997
1074
 
998
1075
  public async getEventByIndexInSchedule(data: {
@@ -1006,9 +1083,10 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1006
1083
  } as LogAttributes,
1007
1084
  );
1008
1085
 
1009
- const layerProps: Array<LayerProps> = await this.getScheduleLayerProps({
1010
- scheduleId: data.scheduleId,
1011
- });
1086
+ const { layerProps, projectId, scheduleUserIds } =
1087
+ await this.getScheduleLayerProps({
1088
+ scheduleId: data.scheduleId,
1089
+ });
1012
1090
 
1013
1091
  logger.debug("Layer properties fetched: " + JSON.stringify(layerProps), {
1014
1092
  onCallDutyPolicyScheduleId: data.scheduleId.toString(),
@@ -1042,7 +1120,7 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1042
1120
  onCallDutyPolicyScheduleId: data.scheduleId.toString(),
1043
1121
  } as LogAttributes);
1044
1122
 
1045
- const events: Array<CalendarEvent> = this.layerUtil.getMultiLayerEvents(
1123
+ let events: Array<CalendarEvent> = this.layerUtil.getMultiLayerEvents(
1046
1124
  {
1047
1125
  layers: layerProps,
1048
1126
  calendarStartDate: currentStartTime,
@@ -1053,6 +1131,23 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1053
1131
  },
1054
1132
  );
1055
1133
 
1134
+ if (projectId && events.length > 0) {
1135
+ const overrides: Array<UserOverrideRecord> =
1136
+ await this.fetchOverridesForSchedule({
1137
+ projectId,
1138
+ scheduleUserIds,
1139
+ windowStart: currentStartTime,
1140
+ windowEnd: currentEndTime,
1141
+ });
1142
+
1143
+ if (overrides.length > 0) {
1144
+ events = UserOverrideUtil.applyOverridesToEvents({
1145
+ events,
1146
+ overrides,
1147
+ });
1148
+ }
1149
+ }
1150
+
1056
1151
  logger.debug("Events fetched: " + JSON.stringify(events), {
1057
1152
  onCallDutyPolicyScheduleId: data.scheduleId.toString(),
1058
1153
  } as LogAttributes);
@@ -1064,9 +1159,10 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1064
1159
  public async getCurrentUserIdInSchedule(
1065
1160
  scheduleId: ObjectID,
1066
1161
  ): Promise<ObjectID | null> {
1067
- const layerProps: Array<LayerProps> = await this.getScheduleLayerProps({
1068
- scheduleId: scheduleId,
1069
- });
1162
+ const { layerProps, projectId, scheduleUserIds } =
1163
+ await this.getScheduleLayerProps({
1164
+ scheduleId: scheduleId,
1165
+ });
1070
1166
 
1071
1167
  if (layerProps.length === 0) {
1072
1168
  return null;
@@ -1078,7 +1174,7 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1078
1174
  1,
1079
1175
  );
1080
1176
 
1081
- const events: Array<CalendarEvent> = this.layerUtil.getMultiLayerEvents(
1177
+ let events: Array<CalendarEvent> = this.layerUtil.getMultiLayerEvents(
1082
1178
  {
1083
1179
  layers: layerProps,
1084
1180
  calendarStartDate: currentStartTime,
@@ -1089,6 +1185,23 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1089
1185
  },
1090
1186
  );
1091
1187
 
1188
+ if (projectId && events.length > 0) {
1189
+ const overrides: Array<UserOverrideRecord> =
1190
+ await this.fetchOverridesForSchedule({
1191
+ projectId,
1192
+ scheduleUserIds,
1193
+ windowStart: currentStartTime,
1194
+ windowEnd: currentEndTime,
1195
+ });
1196
+
1197
+ if (overrides.length > 0) {
1198
+ events = UserOverrideUtil.applyOverridesToEvents({
1199
+ events,
1200
+ overrides,
1201
+ });
1202
+ }
1203
+ }
1204
+
1092
1205
  const currentEvent: CalendarEvent | null = events[0] || null;
1093
1206
 
1094
1207
  if (!currentEvent) {
@@ -0,0 +1,10 @@
1
+ import DatabaseService from "./DatabaseService";
2
+ import Model from "../../Models/DatabaseModels/ProjectOidc";
3
+
4
+ export class Service extends DatabaseService<Model> {
5
+ public constructor() {
6
+ super(Model);
7
+ }
8
+ }
9
+
10
+ export default new Service();
@@ -0,0 +1,155 @@
1
+ import CalendarEvent from "../Calendar/CalendarEvent";
2
+ import OneUptimeDate from "../Date";
3
+
4
+ export interface UserOverrideRecord {
5
+ overrideUserId: string;
6
+ routeAlertsToUserId: string;
7
+ startsAt: Date;
8
+ endsAt: Date;
9
+ // null/undefined means global override (applies to all on-call duty policies)
10
+ onCallDutyPolicyId?: string | null | undefined;
11
+ }
12
+
13
+ export interface OverrideEventMeta {
14
+ isOverride: true;
15
+ originalUserId: string;
16
+ overrideUserId: string;
17
+ overrideStartsAt: Date;
18
+ overrideEndsAt: Date;
19
+ }
20
+
21
+ /*
22
+ * CalendarEvent extends JSONObject so it accepts string-indexed metadata.
23
+ * We attach the override info under a known key so downstream consumers can
24
+ * detect and render the substitution distinctly.
25
+ */
26
+ export const OVERRIDE_META_KEY: string = "_override";
27
+
28
+ export default class UserOverrideUtil {
29
+ /**
30
+ * Returns true when this override should be applied on top of the schedule
31
+ * events. Global overrides always apply; policy-scoped overrides apply to
32
+ * any schedule, since a schedule's calendar shows coverage information for
33
+ * every user who could potentially be paged through it.
34
+ */
35
+ public static isOverrideApplicable(_override: UserOverrideRecord): boolean {
36
+ return true;
37
+ }
38
+
39
+ public static applyOverridesToEvents(data: {
40
+ events: Array<CalendarEvent>;
41
+ overrides: Array<UserOverrideRecord>;
42
+ }): Array<CalendarEvent> {
43
+ const applicable: Array<UserOverrideRecord> = data.overrides.filter(
44
+ UserOverrideUtil.isOverrideApplicable,
45
+ );
46
+
47
+ if (applicable.length === 0) {
48
+ return data.events;
49
+ }
50
+
51
+ let working: Array<CalendarEvent> = data.events;
52
+
53
+ for (const override of applicable) {
54
+ const next: Array<CalendarEvent> = [];
55
+ for (const event of working) {
56
+ next.push(...UserOverrideUtil.splitEventByOverride(event, override));
57
+ }
58
+ working = next;
59
+ }
60
+
61
+ return UserOverrideUtil.reassignEventIds(working);
62
+ }
63
+
64
+ private static splitEventByOverride(
65
+ event: CalendarEvent,
66
+ override: UserOverrideRecord,
67
+ ): Array<CalendarEvent> {
68
+ if (event.title !== override.overrideUserId) {
69
+ return [event];
70
+ }
71
+
72
+ // Override window doesn't overlap event window at all.
73
+ if (
74
+ OneUptimeDate.isAfter(override.startsAt, event.end) ||
75
+ OneUptimeDate.isSame(override.startsAt, event.end) ||
76
+ OneUptimeDate.isBefore(override.endsAt, event.start) ||
77
+ OneUptimeDate.isSame(override.endsAt, event.start)
78
+ ) {
79
+ return [event];
80
+ }
81
+
82
+ const overrideStart: Date = OneUptimeDate.isAfter(
83
+ override.startsAt,
84
+ event.start,
85
+ )
86
+ ? override.startsAt
87
+ : event.start;
88
+
89
+ const overrideEnd: Date = OneUptimeDate.isBefore(override.endsAt, event.end)
90
+ ? override.endsAt
91
+ : event.end;
92
+
93
+ const segments: Array<CalendarEvent> = [];
94
+
95
+ // Segment before the override window — original user remains on call.
96
+ if (OneUptimeDate.isBefore(event.start, overrideStart)) {
97
+ segments.push({
98
+ ...event,
99
+ end: overrideStart,
100
+ });
101
+ }
102
+
103
+ // Override window — substitute user takes over.
104
+ const meta: OverrideEventMeta = {
105
+ isOverride: true,
106
+ originalUserId: override.overrideUserId,
107
+ overrideUserId: override.routeAlertsToUserId,
108
+ overrideStartsAt: override.startsAt,
109
+ overrideEndsAt: override.endsAt,
110
+ };
111
+
112
+ segments.push({
113
+ ...event,
114
+ start: overrideStart,
115
+ end: overrideEnd,
116
+ title: override.routeAlertsToUserId,
117
+ [OVERRIDE_META_KEY]: meta as unknown as never,
118
+ });
119
+
120
+ // Segment after the override window — original user resumes.
121
+ if (OneUptimeDate.isAfter(event.end, overrideEnd)) {
122
+ segments.push({
123
+ ...event,
124
+ start: overrideEnd,
125
+ });
126
+ }
127
+
128
+ return segments;
129
+ }
130
+
131
+ private static reassignEventIds(
132
+ events: Array<CalendarEvent>,
133
+ ): Array<CalendarEvent> {
134
+ let id: number = 1;
135
+ return events.map((event: CalendarEvent) => {
136
+ return { ...event, id: id++ };
137
+ });
138
+ }
139
+
140
+ public static getOverrideMeta(
141
+ event: CalendarEvent,
142
+ ): OverrideEventMeta | null {
143
+ const meta: unknown = (event as unknown as Record<string, unknown>)[
144
+ OVERRIDE_META_KEY
145
+ ];
146
+ if (
147
+ meta &&
148
+ typeof meta === "object" &&
149
+ (meta as { isOverride?: boolean }).isOverride === true
150
+ ) {
151
+ return meta as OverrideEventMeta;
152
+ }
153
+ return null;
154
+ }
155
+ }
@@ -527,6 +527,11 @@ enum Permission {
527
527
  EditProjectSSO = "EditProjectSSO",
528
528
  ReadProjectSSO = "ReadProjectSSO",
529
529
 
530
+ CreateProjectOIDC = "CreateProjectOIDC",
531
+ DeleteProjectOIDC = "DeleteProjectOIDC",
532
+ EditProjectOIDC = "EditProjectOIDC",
533
+ ReadProjectOIDC = "ReadProjectOIDC",
534
+
530
535
  CreateStatusPageSSO = "CreateStatusPageSSO",
531
536
  DeleteStatusPageSSO = "DeleteStatusPageSSO",
532
537
  EditStatusPageSSO = "EditStatusPageSSO",
@@ -2949,6 +2954,43 @@ export class PermissionHelper {
2949
2954
  group: PermissionGroup.Settings,
2950
2955
  },
2951
2956
 
2957
+ {
2958
+ permission: Permission.CreateProjectOIDC,
2959
+ title: "Create Project OIDC",
2960
+ description: "This permission can create Project OIDC in this project.",
2961
+ isAssignableToTenant: true,
2962
+ isAccessControlPermission: false,
2963
+ isRolePermission: false,
2964
+ group: PermissionGroup.Settings,
2965
+ },
2966
+ {
2967
+ permission: Permission.DeleteProjectOIDC,
2968
+ title: "Delete Project OIDC",
2969
+ description: "This permission can delete Project OIDC in this project.",
2970
+ isAssignableToTenant: true,
2971
+ isAccessControlPermission: false,
2972
+ isRolePermission: false,
2973
+ group: PermissionGroup.Settings,
2974
+ },
2975
+ {
2976
+ permission: Permission.EditProjectOIDC,
2977
+ title: "Edit Project OIDC",
2978
+ description: "This permission can edit Project OIDC in this project.",
2979
+ isAssignableToTenant: true,
2980
+ isAccessControlPermission: false,
2981
+ isRolePermission: false,
2982
+ group: PermissionGroup.Settings,
2983
+ },
2984
+ {
2985
+ permission: Permission.ReadProjectOIDC,
2986
+ title: "Read Project OIDC",
2987
+ description: "This permission can read Project OIDC in this project.",
2988
+ isAssignableToTenant: true,
2989
+ isAccessControlPermission: false,
2990
+ isRolePermission: false,
2991
+ group: PermissionGroup.Settings,
2992
+ },
2993
+
2952
2994
  {
2953
2995
  permission: Permission.CreateStatusPageSSO,
2954
2996
  title: "Create Status Page SSO",