@oneuptime/common 10.2.3 → 10.2.6
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 +2 -0
- package/Models/DatabaseModels/Service.ts +26 -0
- package/Models/DatabaseModels/StatusPage.ts +43 -0
- package/Models/DatabaseModels/StatusPageOidc.ts +689 -0
- package/Server/API/StatusPageAPI.ts +78 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1778521361934-MigrationName.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1778522070962-AddStatusPageOIDC.ts +63 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1778582583897-MigrationName.ts +15 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/OpenTelemetryIngestService.ts +15 -0
- package/Server/Services/ScheduledMaintenanceService.ts +34 -0
- package/Server/Services/ServiceService.ts +37 -0
- package/Server/Services/StatusPageOidcService.ts +10 -0
- package/Server/Services/StatusPageSubscriberService.ts +101 -0
- package/Server/Types/Database/QueryHelper.ts +38 -0
- package/Server/Types/Database/QueryUtil.ts +77 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +52 -0
- package/Server/Utils/StatusPageSubscriberWebhook.ts +39 -0
- package/Types/BaseDatabase/MultiSearch.ts +53 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardChartComponent.ts +2 -0
- package/Types/JSON.ts +3 -0
- package/Types/Permission.ts +46 -0
- package/Types/SerializableObjectDictionary.ts +2 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +988 -4
- package/Utils/Dashboard/Components/DashboardChartComponent.ts +11 -0
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Service.js +28 -0
- package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +45 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js +718 -0
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -0
- package/build/dist/Server/API/StatusPageAPI.js +80 -35
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778521361934-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778521361934-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778522070962-AddStatusPageOIDC.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778522070962-AddStatusPageOIDC.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778582583897-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778582583897-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/OpenTelemetryIngestService.js +11 -0
- package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +31 -2
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +34 -0
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageOidcService.js +9 -0
- package/build/dist/Server/Services/StatusPageOidcService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js +99 -4
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +33 -0
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +64 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +44 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageSubscriberWebhook.js +19 -0
- package/build/dist/Server/Utils/StatusPageSubscriberWebhook.js.map +1 -0
- package/build/dist/Types/BaseDatabase/MultiSearch.js +44 -0
- package/build/dist/Types/BaseDatabase/MultiSearch.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/JSON.js +1 -0
- package/build/dist/Types/JSON.js.map +1 -1
- package/build/dist/Types/Permission.js +40 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +2 -0
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +591 -7
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/Utils/Dashboard/Components/DashboardChartComponent.js +9 -0
- package/build/dist/Utils/Dashboard/Components/DashboardChartComponent.js.map +1 -1
- package/package.json +1 -1
|
@@ -26,6 +26,7 @@ import StatusPageService, {
|
|
|
26
26
|
Service as StatusPageServiceType,
|
|
27
27
|
} from "../Services/StatusPageService";
|
|
28
28
|
import StatusPageSsoService from "../Services/StatusPageSsoService";
|
|
29
|
+
import StatusPageOidcService from "../Services/StatusPageOidcService";
|
|
29
30
|
import StatusPageSubscriberService from "../Services/StatusPageSubscriberService";
|
|
30
31
|
import Query from "../Types/Database/Query";
|
|
31
32
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
@@ -81,6 +82,7 @@ import StatusPageHeaderLink from "../../Models/DatabaseModels/StatusPageHeaderLi
|
|
|
81
82
|
import StatusPageHistoryChartBarColorRule from "../../Models/DatabaseModels/StatusPageHistoryChartBarColorRule";
|
|
82
83
|
import StatusPageResource from "../../Models/DatabaseModels/StatusPageResource";
|
|
83
84
|
import StatusPageSSO from "../../Models/DatabaseModels/StatusPageSso";
|
|
85
|
+
import StatusPageOIDC from "../../Models/DatabaseModels/StatusPageOidc";
|
|
84
86
|
import StatusPageSubscriber from "../../Models/DatabaseModels/StatusPageSubscriber";
|
|
85
87
|
import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
|
|
86
88
|
import StatusPageResourceUptimeUtil from "../../Utils/StatusPage/ResourceUptime";
|
|
@@ -863,6 +865,7 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
863
865
|
enableEmailSubscribers: true,
|
|
864
866
|
enableSlackSubscribers: true,
|
|
865
867
|
enableMicrosoftTeamsSubscribers: true,
|
|
868
|
+
enableWebhookSubscribers: true,
|
|
866
869
|
enableSmsSubscribers: true,
|
|
867
870
|
isPublicStatusPage: true,
|
|
868
871
|
enableMasterPassword: true,
|
|
@@ -1094,6 +1097,46 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
1094
1097
|
},
|
|
1095
1098
|
);
|
|
1096
1099
|
|
|
1100
|
+
this.router.post(
|
|
1101
|
+
`${new this.entityType().getCrudApiPath()?.toString()}/oidc/:statusPageId`,
|
|
1102
|
+
UserMiddleware.getUserMiddleware,
|
|
1103
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
1104
|
+
try {
|
|
1105
|
+
const objectId: ObjectID = new ObjectID(
|
|
1106
|
+
req.params["statusPageId"] as string,
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
const oidc: Array<StatusPageOIDC> =
|
|
1110
|
+
await StatusPageOidcService.findBy({
|
|
1111
|
+
query: {
|
|
1112
|
+
statusPageId: objectId,
|
|
1113
|
+
isEnabled: true,
|
|
1114
|
+
},
|
|
1115
|
+
select: {
|
|
1116
|
+
name: true,
|
|
1117
|
+
description: true,
|
|
1118
|
+
_id: true,
|
|
1119
|
+
},
|
|
1120
|
+
limit: LIMIT_PER_PROJECT,
|
|
1121
|
+
skip: 0,
|
|
1122
|
+
props: {
|
|
1123
|
+
isRoot: true,
|
|
1124
|
+
},
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
return Response.sendEntityArrayResponse(
|
|
1128
|
+
req,
|
|
1129
|
+
res,
|
|
1130
|
+
oidc,
|
|
1131
|
+
new PositiveNumber(oidc.length),
|
|
1132
|
+
StatusPageOIDC,
|
|
1133
|
+
);
|
|
1134
|
+
} catch (err) {
|
|
1135
|
+
next(err);
|
|
1136
|
+
}
|
|
1137
|
+
},
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1097
1140
|
// Get all status page resources for subscriber to subscribe to.
|
|
1098
1141
|
this.router.post(
|
|
1099
1142
|
`${new this.entityType()
|
|
@@ -3146,6 +3189,7 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
3146
3189
|
enableEmailSubscribers: true,
|
|
3147
3190
|
enableSlackSubscribers: true,
|
|
3148
3191
|
enableMicrosoftTeamsSubscribers: true,
|
|
3192
|
+
enableWebhookSubscribers: true,
|
|
3149
3193
|
enableSmsSubscribers: true,
|
|
3150
3194
|
allowSubscribersToChooseResources: true,
|
|
3151
3195
|
allowSubscribersToChooseEventTypes: true,
|
|
@@ -3539,6 +3583,7 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
3539
3583
|
enableSmsSubscribers: true,
|
|
3540
3584
|
enableSlackSubscribers: true,
|
|
3541
3585
|
enableMicrosoftTeamsSubscribers: true,
|
|
3586
|
+
enableWebhookSubscribers: true,
|
|
3542
3587
|
allowSubscribersToChooseResources: true,
|
|
3543
3588
|
allowSubscribersToChooseEventTypes: true,
|
|
3544
3589
|
showSubscriberPageOnStatusPage: true,
|
|
@@ -3623,17 +3668,32 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
3623
3668
|
}
|
|
3624
3669
|
|
|
3625
3670
|
if (
|
|
3671
|
+
req.body.data["subscriberWebhook"] &&
|
|
3672
|
+
!statusPage.enableWebhookSubscribers
|
|
3673
|
+
) {
|
|
3674
|
+
logger.debug(
|
|
3675
|
+
`Webhook subscribers not enabled for status page with ID: ${objectId}`,
|
|
3676
|
+
getLogAttributesFromRequest(req as any),
|
|
3677
|
+
);
|
|
3678
|
+
throw new BadDataException(
|
|
3679
|
+
"Webhook subscribers not enabled for this status page.",
|
|
3680
|
+
);
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
if (
|
|
3684
|
+
!req.params["subscriberId"] &&
|
|
3626
3685
|
!req.body.data["subscriberEmail"] &&
|
|
3627
3686
|
!req.body.data["subscriberPhone"] &&
|
|
3628
3687
|
!req.body.data["slackWorkspaceName"] &&
|
|
3629
|
-
!req.body.data["microsoftTeamsWorkspaceName"]
|
|
3688
|
+
!req.body.data["microsoftTeamsWorkspaceName"] &&
|
|
3689
|
+
!req.body.data["subscriberWebhook"]
|
|
3630
3690
|
) {
|
|
3631
3691
|
logger.debug(
|
|
3632
|
-
`No email, phone, slack workspace name,
|
|
3692
|
+
`No email, phone, slack workspace name, Microsoft Teams workspace name, or webhook URL provided for subscription to status page with ID: ${objectId}`,
|
|
3633
3693
|
getLogAttributesFromRequest(req as any),
|
|
3634
3694
|
);
|
|
3635
3695
|
throw new BadDataException(
|
|
3636
|
-
"Email, phone, slack workspace name,
|
|
3696
|
+
"Email, phone, slack workspace name, Microsoft Teams workspace name, or webhook URL is required to subscribe to this status page.",
|
|
3637
3697
|
);
|
|
3638
3698
|
}
|
|
3639
3699
|
|
|
@@ -3669,6 +3729,12 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
3669
3729
|
? (req.body.data["microsoftTeamsWorkspaceName"] as string)
|
|
3670
3730
|
: undefined;
|
|
3671
3731
|
|
|
3732
|
+
const subscriberWebhookUrl: string | undefined = req.body.data[
|
|
3733
|
+
"subscriberWebhook"
|
|
3734
|
+
]
|
|
3735
|
+
? (req.body.data["subscriberWebhook"] as string)
|
|
3736
|
+
: undefined;
|
|
3737
|
+
|
|
3672
3738
|
let statusPageSubscriber: StatusPageSubscriber | null = null;
|
|
3673
3739
|
|
|
3674
3740
|
let isUpdate: boolean = false;
|
|
@@ -3761,6 +3827,15 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
3761
3827
|
microsoftTeamsWorkspaceName;
|
|
3762
3828
|
}
|
|
3763
3829
|
|
|
3830
|
+
if (subscriberWebhookUrl) {
|
|
3831
|
+
logger.debug(
|
|
3832
|
+
`Setting subscriber webhook URL: ${subscriberWebhookUrl}`,
|
|
3833
|
+
getLogAttributesFromRequest(req as any),
|
|
3834
|
+
);
|
|
3835
|
+
statusPageSubscriber.subscriberWebhook =
|
|
3836
|
+
URL.fromString(subscriberWebhookUrl);
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3764
3839
|
if (
|
|
3765
3840
|
!isUpdate &&
|
|
3766
3841
|
req.body.data["statusPageResources"] &&
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class MigrationName1778521361934 implements MigrationInterface {
|
|
4
|
+
public name = "MigrationName1778521361934";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(
|
|
8
|
+
`ALTER TABLE "StatusPage" ADD "enableWebhookSubscribers" boolean NOT NULL DEFAULT false`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
13
|
+
await queryRunner.query(
|
|
14
|
+
`ALTER TABLE "StatusPage" DROP COLUMN "enableWebhookSubscribers"`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class AddStatusPageOIDC1778522070962 implements MigrationInterface {
|
|
4
|
+
public name = "AddStatusPageOIDC1778522070962";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(
|
|
8
|
+
`CREATE TABLE "StatusPageOIDC" ("_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, "statusPageId" 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), "createdByUserId" uuid, "deletedByUserId" uuid, "isEnabled" boolean NOT NULL DEFAULT false, "isTested" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_d48cc7131b1383a0c293e1142f4" PRIMARY KEY ("_id"))`,
|
|
9
|
+
);
|
|
10
|
+
await queryRunner.query(
|
|
11
|
+
`CREATE INDEX "IDX_da08a1ff289745767b094724f7" ON "StatusPageOIDC" ("projectId") `,
|
|
12
|
+
);
|
|
13
|
+
await queryRunner.query(
|
|
14
|
+
`CREATE INDEX "IDX_13c9b134ba46772392bdfb760f" ON "StatusPageOIDC" ("statusPageId") `,
|
|
15
|
+
);
|
|
16
|
+
await queryRunner.query(
|
|
17
|
+
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
|
18
|
+
);
|
|
19
|
+
await queryRunner.query(
|
|
20
|
+
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
|
21
|
+
);
|
|
22
|
+
await queryRunner.query(
|
|
23
|
+
`ALTER TABLE "StatusPageOIDC" ADD CONSTRAINT "FK_da08a1ff289745767b094724f79" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
24
|
+
);
|
|
25
|
+
await queryRunner.query(
|
|
26
|
+
`ALTER TABLE "StatusPageOIDC" ADD CONSTRAINT "FK_13c9b134ba46772392bdfb760fd" FOREIGN KEY ("statusPageId") REFERENCES "StatusPage"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
27
|
+
);
|
|
28
|
+
await queryRunner.query(
|
|
29
|
+
`ALTER TABLE "StatusPageOIDC" ADD CONSTRAINT "FK_8e3b6fea29c79c99389893961f1" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
30
|
+
);
|
|
31
|
+
await queryRunner.query(
|
|
32
|
+
`ALTER TABLE "StatusPageOIDC" ADD CONSTRAINT "FK_47054926e3844c88872599d5d55" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
37
|
+
await queryRunner.query(
|
|
38
|
+
`ALTER TABLE "StatusPageOIDC" DROP CONSTRAINT "FK_47054926e3844c88872599d5d55"`,
|
|
39
|
+
);
|
|
40
|
+
await queryRunner.query(
|
|
41
|
+
`ALTER TABLE "StatusPageOIDC" DROP CONSTRAINT "FK_8e3b6fea29c79c99389893961f1"`,
|
|
42
|
+
);
|
|
43
|
+
await queryRunner.query(
|
|
44
|
+
`ALTER TABLE "StatusPageOIDC" DROP CONSTRAINT "FK_13c9b134ba46772392bdfb760fd"`,
|
|
45
|
+
);
|
|
46
|
+
await queryRunner.query(
|
|
47
|
+
`ALTER TABLE "StatusPageOIDC" DROP CONSTRAINT "FK_da08a1ff289745767b094724f79"`,
|
|
48
|
+
);
|
|
49
|
+
await queryRunner.query(
|
|
50
|
+
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
|
51
|
+
);
|
|
52
|
+
await queryRunner.query(
|
|
53
|
+
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
|
54
|
+
);
|
|
55
|
+
await queryRunner.query(
|
|
56
|
+
`DROP INDEX "public"."IDX_13c9b134ba46772392bdfb760f"`,
|
|
57
|
+
);
|
|
58
|
+
await queryRunner.query(
|
|
59
|
+
`DROP INDEX "public"."IDX_da08a1ff289745767b094724f7"`,
|
|
60
|
+
);
|
|
61
|
+
await queryRunner.query(`DROP TABLE "StatusPageOIDC"`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class MigrationName1778582583897 implements MigrationInterface {
|
|
4
|
+
public name = "MigrationName1778582583897";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(
|
|
8
|
+
`ALTER TABLE "Service" ADD "lastSeenAt" TIMESTAMP WITH TIME ZONE`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
13
|
+
await queryRunner.query(`ALTER TABLE "Service" DROP COLUMN "lastSeenAt"`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -314,6 +314,9 @@ import { AddPrivacyRules1778440665575 } from "./1778440665575-AddPrivacyRules";
|
|
|
314
314
|
import { AddEpisodePrivacyRules1778442385970 } from "./1778442385970-AddEpisodePrivacyRules";
|
|
315
315
|
import { AddProjectOIDC1778506655291 } from "./1778506655291-AddProjectOIDC";
|
|
316
316
|
import { MigrationName1778514515756 } from "./1778514515756-MigrationName";
|
|
317
|
+
import { MigrationName1778521361934 } from "./1778521361934-MigrationName";
|
|
318
|
+
import { AddStatusPageOIDC1778522070962 } from "./1778522070962-AddStatusPageOIDC";
|
|
319
|
+
import { MigrationName1778582583897 } from "./1778582583897-MigrationName";
|
|
317
320
|
export default [
|
|
318
321
|
InitialMigration,
|
|
319
322
|
MigrationName1717678334852,
|
|
@@ -631,4 +634,7 @@ export default [
|
|
|
631
634
|
AddEpisodePrivacyRules1778442385970,
|
|
632
635
|
AddProjectOIDC1778506655291,
|
|
633
636
|
MigrationName1778514515756,
|
|
637
|
+
MigrationName1778521361934,
|
|
638
|
+
AddStatusPageOIDC1778522070962,
|
|
639
|
+
MigrationName1778582583897,
|
|
634
640
|
];
|
package/Server/Services/Index.ts
CHANGED
|
@@ -130,6 +130,7 @@ import StatusPageResourceService from "./StatusPageResourceService";
|
|
|
130
130
|
// Status Page
|
|
131
131
|
import StatusPageService from "./StatusPageService";
|
|
132
132
|
import StatusPageSsoService from "./StatusPageSsoService";
|
|
133
|
+
import StatusPageOidcService from "./StatusPageOidcService";
|
|
133
134
|
import StatusPageSubscriberService from "./StatusPageSubscriberService";
|
|
134
135
|
import StatusPageSubscriberNotificationTemplateService from "./StatusPageSubscriberNotificationTemplateService";
|
|
135
136
|
import StatusPageSubscriberNotificationTemplateStatusPageService from "./StatusPageSubscriberNotificationTemplateStatusPageService";
|
|
@@ -335,6 +336,7 @@ const services: Array<BaseService> = [
|
|
|
335
336
|
StatusPageResourceService,
|
|
336
337
|
StatusPageService,
|
|
337
338
|
StatusPageSsoService,
|
|
339
|
+
StatusPageOidcService,
|
|
338
340
|
StatusPageSubscriberService,
|
|
339
341
|
StatusPageSubscriberNotificationTemplateService,
|
|
340
342
|
StatusPageSubscriberNotificationTemplateStatusPageService,
|
|
@@ -64,6 +64,21 @@ export default class OTelIngestService {
|
|
|
64
64
|
projectId: data.projectId,
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
/*
|
|
68
|
+
* Touch `lastSeenAt` on the service. Throttled per-service inside
|
|
69
|
+
* ServiceService.updateLastSeen so the steady-state firehose costs
|
|
70
|
+
* one in-memory cache lookup per batch.
|
|
71
|
+
*/
|
|
72
|
+
try {
|
|
73
|
+
await ServiceService.updateLastSeen(result.serviceId);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.warn(
|
|
76
|
+
`telemetryServiceFromName lastSeen update failed for "${data.serviceName}": ${
|
|
77
|
+
err instanceof Error ? err.message : String(err)
|
|
78
|
+
}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
/*
|
|
68
83
|
* Promote `oneuptime.label.<dim>=<val>` resource attributes into
|
|
69
84
|
* project labels and attach them to the discovered service. The
|
|
@@ -51,6 +51,7 @@ import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
|
|
|
51
51
|
import ScheduledMaintenanceFeedService from "./ScheduledMaintenanceFeedService";
|
|
52
52
|
import { ScheduledMaintenanceFeedEventType } from "../../Models/DatabaseModels/ScheduledMaintenanceFeed";
|
|
53
53
|
import SlackUtil from "../Utils/Workspace/Slack/Slack";
|
|
54
|
+
import StatusPageSubscriberWebhookUtil from "../Utils/StatusPageSubscriberWebhook";
|
|
54
55
|
import { Gray500, Red500 } from "../../Types/BrandColors";
|
|
55
56
|
import Label from "../../Models/DatabaseModels/Label";
|
|
56
57
|
import LabelService from "./LabelService";
|
|
@@ -332,6 +333,39 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
|
|
|
332
333
|
});
|
|
333
334
|
}
|
|
334
335
|
|
|
336
|
+
if (subscriber.subscriberWebhook) {
|
|
337
|
+
StatusPageSubscriberWebhookUtil.sendWebhookNotification({
|
|
338
|
+
webhookUrl: subscriber.subscriberWebhook,
|
|
339
|
+
payload: {
|
|
340
|
+
eventType: "ScheduledMaintenanceCreated",
|
|
341
|
+
statusPageId: statuspage.id!.toString(),
|
|
342
|
+
statusPageName: statusPageName,
|
|
343
|
+
statusPageUrl: statusPageURL,
|
|
344
|
+
unsubscribeUrl: unsubscribeUrl,
|
|
345
|
+
data: {
|
|
346
|
+
scheduledMaintenanceId: event.id?.toString() || "",
|
|
347
|
+
scheduledMaintenanceTitle: event.title || "",
|
|
348
|
+
scheduledMaintenanceDescription: event.description || "",
|
|
349
|
+
scheduledStartTime:
|
|
350
|
+
OneUptimeDate.getDateAsUserFriendlyFormattedString(
|
|
351
|
+
event.startsAt!,
|
|
352
|
+
),
|
|
353
|
+
scheduledEndTime: event.endsAt
|
|
354
|
+
? OneUptimeDate.getDateAsUserFriendlyFormattedString(
|
|
355
|
+
event.endsAt,
|
|
356
|
+
)
|
|
357
|
+
: "",
|
|
358
|
+
resourcesAffected: resourcesAffected,
|
|
359
|
+
detailsUrl: scheduledEventDetailsUrl,
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
}).catch((err: Error) => {
|
|
363
|
+
logger.error(err, {
|
|
364
|
+
projectId: statuspage.projectId?.toString(),
|
|
365
|
+
} as LogAttributes);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
335
369
|
if (subscriber.subscriberEmail) {
|
|
336
370
|
// send email here.
|
|
337
371
|
const statusPageIdString: string | null =
|
|
@@ -6,6 +6,7 @@ import ArrayUtil from "../../Utils/Array";
|
|
|
6
6
|
import { BrightColors } from "../../Types/BrandColors";
|
|
7
7
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
8
8
|
import ObjectID from "../../Types/ObjectID";
|
|
9
|
+
import OneUptimeDate from "../../Types/Date";
|
|
9
10
|
import Model from "../../Models/DatabaseModels/Service";
|
|
10
11
|
import Label from "../../Models/DatabaseModels/Label";
|
|
11
12
|
import Project from "../../Models/DatabaseModels/Project";
|
|
@@ -16,6 +17,9 @@ import crypto from "crypto";
|
|
|
16
17
|
|
|
17
18
|
const DEFAULT_TELEMETRY_RETENTION_IN_DAYS: number = 15;
|
|
18
19
|
|
|
20
|
+
const LAST_SEEN_CACHE_NAMESPACE: string = "service-last-seen";
|
|
21
|
+
const LAST_SEEN_THROTTLE_SECONDS: number = 60;
|
|
22
|
+
|
|
19
23
|
const LABELS_APPLIED_CACHE_NAMESPACE: string = "service-labels-applied";
|
|
20
24
|
const LABELS_APPLIED_CACHE_TTL_SECONDS: number = 60;
|
|
21
25
|
|
|
@@ -80,6 +84,39 @@ export class Service extends DatabaseService<Model> {
|
|
|
80
84
|
return DEFAULT_TELEMETRY_RETENTION_IN_DAYS;
|
|
81
85
|
}
|
|
82
86
|
|
|
87
|
+
/*
|
|
88
|
+
* Refresh `lastSeenAt` for a service. Throttled per-service so the
|
|
89
|
+
* steady-state telemetry firehose (every metric/log/trace batch
|
|
90
|
+
* re-resolves the same serviceId) costs one in-memory cache lookup
|
|
91
|
+
* per batch instead of a DB write.
|
|
92
|
+
*/
|
|
93
|
+
@CaptureSpan()
|
|
94
|
+
public async updateLastSeen(serviceId: ObjectID): Promise<void> {
|
|
95
|
+
const cacheKey: string = serviceId.toString();
|
|
96
|
+
const cached: string | null = await GlobalCache.getString(
|
|
97
|
+
LAST_SEEN_CACHE_NAMESPACE,
|
|
98
|
+
cacheKey,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (cached) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await GlobalCache.setString(LAST_SEEN_CACHE_NAMESPACE, cacheKey, "1", {
|
|
106
|
+
expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await this.updateOneById({
|
|
110
|
+
id: serviceId,
|
|
111
|
+
data: {
|
|
112
|
+
lastSeenAt: OneUptimeDate.getCurrentDate(),
|
|
113
|
+
},
|
|
114
|
+
props: {
|
|
115
|
+
isRoot: true,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
83
120
|
/**
|
|
84
121
|
* Additively attach labels to a telemetry service. Existing labels
|
|
85
122
|
* are never removed — manual labels set via the UI survive ingest.
|
|
@@ -36,6 +36,7 @@ import StatusPageSubscriberNotificationMethod from "../../Types/StatusPage/Statu
|
|
|
36
36
|
import NumberUtil from "../../Utils/Number";
|
|
37
37
|
import SlackUtil from "../Utils/Workspace/Slack/Slack";
|
|
38
38
|
import MicrosoftTeamsUtil from "../Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
|
|
39
|
+
import StatusPageSubscriberWebhookUtil from "../Utils/StatusPageSubscriberWebhook";
|
|
39
40
|
import StatusPageSubscriberNotificationTemplateService, {
|
|
40
41
|
Service as StatusPageSubscriberNotificationTemplateServiceClass,
|
|
41
42
|
} from "./StatusPageSubscriberNotificationTemplateService";
|
|
@@ -540,6 +541,43 @@ Stay informed about service availability! 🚀`;
|
|
|
540
541
|
});
|
|
541
542
|
}
|
|
542
543
|
|
|
544
|
+
// if generic webhook URL is provided and sendYouHaveSubscribedMessage is true, then ping the webhook with the subscription event.
|
|
545
|
+
if (
|
|
546
|
+
createdItem.subscriberWebhook &&
|
|
547
|
+
createdItem.sendYouHaveSubscribedMessage
|
|
548
|
+
) {
|
|
549
|
+
logger.debug("Sending webhook notification for new subscriber.", {
|
|
550
|
+
projectId: createdItem.projectId?.toString(),
|
|
551
|
+
} as LogAttributes);
|
|
552
|
+
|
|
553
|
+
StatusPageSubscriberWebhookUtil.sendWebhookNotification({
|
|
554
|
+
webhookUrl: URL.fromString(createdItem.subscriberWebhook.toString()),
|
|
555
|
+
payload: {
|
|
556
|
+
eventType: "SubscriberSubscribed",
|
|
557
|
+
statusPageId: createdItem.statusPageId.toString(),
|
|
558
|
+
statusPageName: statusPageName,
|
|
559
|
+
statusPageUrl: statusPageURL,
|
|
560
|
+
unsubscribeUrl: unsubscribeLink,
|
|
561
|
+
data: {
|
|
562
|
+
message: `You have been subscribed to ${statusPageName}.`,
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
})
|
|
566
|
+
.then(() => {
|
|
567
|
+
logger.debug("Webhook notification sent successfully.", {
|
|
568
|
+
projectId: createdItem.projectId?.toString(),
|
|
569
|
+
} as LogAttributes);
|
|
570
|
+
})
|
|
571
|
+
.catch((err: Error) => {
|
|
572
|
+
logger.error("Error sending webhook notification:", {
|
|
573
|
+
projectId: createdItem.projectId?.toString(),
|
|
574
|
+
} as LogAttributes);
|
|
575
|
+
logger.error(err, {
|
|
576
|
+
projectId: createdItem.projectId?.toString(),
|
|
577
|
+
} as LogAttributes);
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
543
581
|
// if Microsoft Teams incoming webhook is provided and sendYouHaveSubscribedMessage is true, then send a message to the Teams channel.
|
|
544
582
|
if (
|
|
545
583
|
createdItem.microsoftTeamsIncomingWebhookUrl &&
|
|
@@ -1345,6 +1383,69 @@ Stay informed about service availability! 🚀`;
|
|
|
1345
1383
|
return statusPages;
|
|
1346
1384
|
}
|
|
1347
1385
|
|
|
1386
|
+
@CaptureSpan()
|
|
1387
|
+
public async testSubscriberWebhook(data: {
|
|
1388
|
+
webhookUrl: string;
|
|
1389
|
+
statusPageId: ObjectID;
|
|
1390
|
+
}): Promise<void> {
|
|
1391
|
+
// basic validation - must be a valid URL
|
|
1392
|
+
let parsedUrl: URL;
|
|
1393
|
+
try {
|
|
1394
|
+
parsedUrl = URL.fromString(data.webhookUrl);
|
|
1395
|
+
} catch {
|
|
1396
|
+
throw new BadDataException("Invalid Webhook URL");
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// get the status page info
|
|
1400
|
+
const statusPage: StatusPage | null = await StatusPageService.findOneById({
|
|
1401
|
+
id: data.statusPageId,
|
|
1402
|
+
props: {
|
|
1403
|
+
isRoot: true,
|
|
1404
|
+
},
|
|
1405
|
+
select: {
|
|
1406
|
+
name: true,
|
|
1407
|
+
pageTitle: true,
|
|
1408
|
+
projectId: true,
|
|
1409
|
+
_id: true,
|
|
1410
|
+
},
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
if (!statusPage) {
|
|
1414
|
+
throw new BadDataException("Status page not found");
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
const statusPageName: string =
|
|
1418
|
+
statusPage.pageTitle || statusPage.name || "Status Page";
|
|
1419
|
+
const statusPageURL: string = await StatusPageService.getStatusPageURL(
|
|
1420
|
+
statusPage.id!,
|
|
1421
|
+
);
|
|
1422
|
+
|
|
1423
|
+
try {
|
|
1424
|
+
await StatusPageSubscriberWebhookUtil.sendWebhookNotification({
|
|
1425
|
+
webhookUrl: parsedUrl,
|
|
1426
|
+
payload: {
|
|
1427
|
+
eventType: "TestNotification",
|
|
1428
|
+
statusPageId: statusPage.id!.toString(),
|
|
1429
|
+
statusPageName: statusPageName,
|
|
1430
|
+
statusPageUrl: statusPageURL,
|
|
1431
|
+
unsubscribeUrl: "",
|
|
1432
|
+
data: {
|
|
1433
|
+
message:
|
|
1434
|
+
"This is a test notification from OneUptime. Your webhook is configured correctly.",
|
|
1435
|
+
},
|
|
1436
|
+
},
|
|
1437
|
+
});
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
logger.error("Error sending test webhook notification:", {
|
|
1440
|
+
projectId: statusPage?.projectId?.toString(),
|
|
1441
|
+
} as LogAttributes);
|
|
1442
|
+
logger.error(error, {
|
|
1443
|
+
projectId: statusPage?.projectId?.toString(),
|
|
1444
|
+
} as LogAttributes);
|
|
1445
|
+
throw error;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1348
1449
|
@CaptureSpan()
|
|
1349
1450
|
public async testSlackWebhook(data: {
|
|
1350
1451
|
webhookUrl: string;
|
|
@@ -138,6 +138,44 @@ export default class QueryHelper {
|
|
|
138
138
|
);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Searches the provided entity property names with a single OR-joined ILIKE.
|
|
143
|
+
*
|
|
144
|
+
* IMPORTANT: emit unquoted `alias.propertyName` references and let
|
|
145
|
+
* TypeORM's `replacePropertyNamesForTheWholeQuery` post-processor escape
|
|
146
|
+
* the table alias and translate property names → DB column names. Pre-
|
|
147
|
+
* quoting (e.g. `Incident."title"`) bypasses that pass, which leaves an
|
|
148
|
+
* unquoted `Incident` in the final SQL — Postgres then lowercases it and
|
|
149
|
+
* fails with `missing FROM-clause entry for table "incident"`.
|
|
150
|
+
*/
|
|
151
|
+
@CaptureSpan()
|
|
152
|
+
public static multiSearch(
|
|
153
|
+
entityPropertyNames: Array<string>,
|
|
154
|
+
value: string,
|
|
155
|
+
): FindWhereProperty<any> {
|
|
156
|
+
const trimmed: string = value.toLowerCase().trim();
|
|
157
|
+
const rid: string = Text.generateRandomText(10);
|
|
158
|
+
|
|
159
|
+
return Raw(
|
|
160
|
+
(alias: string) => {
|
|
161
|
+
const tableAlias: string = alias.includes(".")
|
|
162
|
+
? (alias.split(".")[0] as string)
|
|
163
|
+
: alias;
|
|
164
|
+
|
|
165
|
+
const orConditions: string = entityPropertyNames
|
|
166
|
+
.map((field: string) => {
|
|
167
|
+
return `(CAST(${tableAlias}.${field} AS TEXT) ILIKE :${rid})`;
|
|
168
|
+
})
|
|
169
|
+
.join(" OR ");
|
|
170
|
+
|
|
171
|
+
return `(${orConditions})`;
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
[rid]: `%${trimmed}%`,
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
141
179
|
@CaptureSpan()
|
|
142
180
|
public static notContains(name: string): FindWhereProperty<any> {
|
|
143
181
|
name = name.toLowerCase().trim();
|