@oneuptime/common 7.0.3387 → 7.0.3392
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/StatusPageSubscriber.ts +57 -0
- package/Server/API/StatusPageAPI.ts +60 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1734435866602-MigrationName.ts +23 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/StatusPageService.ts +4 -10
- package/Server/Services/StatusPageSubscriberService.ts +242 -13
- package/Types/Email/EmailTemplateType.ts +1 -0
- package/UI/Components/Pill/Pill.tsx +34 -22
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +59 -0
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +70 -32
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1734435866602-MigrationName.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1734435866602-MigrationName.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/Services/StatusPageService.js +3 -9
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +182 -11
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/UI/Components/Pill/Pill.js +19 -11
- package/build/dist/UI/Components/Pill/Pill.js.map +1 -1
- package/package.json +2 -2
|
@@ -434,6 +434,63 @@ export default class StatusPageSubscriber extends BaseModel {
|
|
|
434
434
|
})
|
|
435
435
|
public deletedByUserId?: ObjectID = undefined;
|
|
436
436
|
|
|
437
|
+
@ColumnAccessControl({
|
|
438
|
+
create: [
|
|
439
|
+
Permission.ProjectOwner,
|
|
440
|
+
Permission.ProjectAdmin,
|
|
441
|
+
Permission.ProjectMember,
|
|
442
|
+
Permission.CreateStatusPageSubscriber,
|
|
443
|
+
Permission.Public,
|
|
444
|
+
],
|
|
445
|
+
read: [
|
|
446
|
+
Permission.ProjectOwner,
|
|
447
|
+
Permission.ProjectAdmin,
|
|
448
|
+
Permission.ProjectMember,
|
|
449
|
+
Permission.ReadStatusPageSubscriber,
|
|
450
|
+
],
|
|
451
|
+
update: [
|
|
452
|
+
Permission.ProjectOwner,
|
|
453
|
+
Permission.ProjectAdmin,
|
|
454
|
+
Permission.ProjectMember,
|
|
455
|
+
Permission.EditStatusPageSubscriber,
|
|
456
|
+
],
|
|
457
|
+
})
|
|
458
|
+
@TableColumn({
|
|
459
|
+
isDefaultValueColumn: true,
|
|
460
|
+
type: TableColumnType.Boolean,
|
|
461
|
+
title: "Is Subscription Confirmed",
|
|
462
|
+
description:
|
|
463
|
+
"Has subscriber confirmed their subscription? (for example, by clicking on a confirmation link in an email)",
|
|
464
|
+
})
|
|
465
|
+
@Column({
|
|
466
|
+
type: ColumnType.Boolean,
|
|
467
|
+
default: false,
|
|
468
|
+
})
|
|
469
|
+
public isSubscriptionConfirmed?: boolean = undefined;
|
|
470
|
+
|
|
471
|
+
@ColumnAccessControl({
|
|
472
|
+
create: [
|
|
473
|
+
Permission.ProjectOwner,
|
|
474
|
+
Permission.ProjectAdmin,
|
|
475
|
+
Permission.ProjectMember,
|
|
476
|
+
Permission.CreateStatusPageSubscriber,
|
|
477
|
+
],
|
|
478
|
+
read: [],
|
|
479
|
+
update: [],
|
|
480
|
+
})
|
|
481
|
+
@TableColumn({
|
|
482
|
+
isDefaultValueColumn: false,
|
|
483
|
+
type: TableColumnType.ShortText,
|
|
484
|
+
title: "Subscription Confirmation Token",
|
|
485
|
+
description:
|
|
486
|
+
"Token used to confirm subscription. This is a random token that is sent to the subscriber's email address to confirm their subscription.",
|
|
487
|
+
})
|
|
488
|
+
@Column({
|
|
489
|
+
type: ColumnType.ShortText,
|
|
490
|
+
nullable: true,
|
|
491
|
+
})
|
|
492
|
+
public subscriptionConfirmationToken?: string = undefined;
|
|
493
|
+
|
|
437
494
|
@ColumnAccessControl({
|
|
438
495
|
create: [
|
|
439
496
|
Permission.ProjectOwner,
|
|
@@ -82,6 +82,66 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
82
82
|
public constructor() {
|
|
83
83
|
super(StatusPage, StatusPageService);
|
|
84
84
|
|
|
85
|
+
// confirm subscription api
|
|
86
|
+
this.router.get(
|
|
87
|
+
`${new this.entityType()
|
|
88
|
+
.getCrudApiPath()
|
|
89
|
+
?.toString()}/confirm-subscription/:statusPageSubscriberId`,
|
|
90
|
+
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
91
|
+
const token: string = req.query["verification-token"] as string;
|
|
92
|
+
|
|
93
|
+
const statusPageSubscriberId: ObjectID = new ObjectID(
|
|
94
|
+
req.params["statusPageSubscriberId"] as string,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const subscriber: StatusPageSubscriber | null =
|
|
98
|
+
await StatusPageSubscriberService.findOneBy({
|
|
99
|
+
query: {
|
|
100
|
+
_id: statusPageSubscriberId,
|
|
101
|
+
subscriptionConfirmationToken: token,
|
|
102
|
+
},
|
|
103
|
+
select: {
|
|
104
|
+
isSubscriptionConfirmed: true,
|
|
105
|
+
},
|
|
106
|
+
props: {
|
|
107
|
+
isRoot: true,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!subscriber) {
|
|
112
|
+
return Response.sendErrorResponse(
|
|
113
|
+
req,
|
|
114
|
+
res,
|
|
115
|
+
new NotFoundException(
|
|
116
|
+
"Subscriber not found or confirmation token is invalid",
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// check if subscription confirmed already.
|
|
122
|
+
|
|
123
|
+
if (subscriber.isSubscriptionConfirmed) {
|
|
124
|
+
return Response.sendEmptySuccessResponse(req, res);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await StatusPageSubscriberService.updateOneById({
|
|
128
|
+
id: statusPageSubscriberId,
|
|
129
|
+
data: {
|
|
130
|
+
isSubscriptionConfirmed: true,
|
|
131
|
+
},
|
|
132
|
+
props: {
|
|
133
|
+
isRoot: true,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await StatusPageSubscriberService.sendYouHaveSubscribedEmail({
|
|
138
|
+
subscriberId: statusPageSubscriberId,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return Response.sendEmptySuccessResponse(req, res);
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
85
145
|
// CNAME verification api
|
|
86
146
|
this.router.get(
|
|
87
147
|
`${new this.entityType()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class MigrationName1734435866602 implements MigrationInterface {
|
|
4
|
+
public name = "MigrationName1734435866602";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(
|
|
8
|
+
`ALTER TABLE "StatusPageSubscriber" ADD "isSubscriptionConfirmed" boolean NOT NULL DEFAULT false`,
|
|
9
|
+
);
|
|
10
|
+
await queryRunner.query(
|
|
11
|
+
`ALTER TABLE "StatusPageSubscriber" ADD "subscriptionConfirmationToken" character varying`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
16
|
+
await queryRunner.query(
|
|
17
|
+
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "subscriptionConfirmationToken"`,
|
|
18
|
+
);
|
|
19
|
+
await queryRunner.query(
|
|
20
|
+
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "isSubscriptionConfirmed"`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -83,6 +83,7 @@ import { MigrationName1731433309124 } from "./1731433309124-MigrationName";
|
|
|
83
83
|
import { MigrationName1731435267537 } from "./1731435267537-MigrationName";
|
|
84
84
|
import { MigrationName1731435514287 } from "./1731435514287-MigrationName";
|
|
85
85
|
import { MigrationName1732553444010 } from "./1732553444010-MigrationName";
|
|
86
|
+
import { MigrationName1734435866602 } from "./1734435866602-MigrationName";
|
|
86
87
|
|
|
87
88
|
export default [
|
|
88
89
|
InitialMigration,
|
|
@@ -170,4 +171,5 @@ export default [
|
|
|
170
171
|
MigrationName1731435267537,
|
|
171
172
|
MigrationName1731435514287,
|
|
172
173
|
MigrationName1732553444010,
|
|
174
|
+
MigrationName1734435866602,
|
|
173
175
|
];
|
|
@@ -454,8 +454,8 @@ export class Service extends DatabaseService<StatusPage> {
|
|
|
454
454
|
}
|
|
455
455
|
|
|
456
456
|
public async getStatusPageURL(statusPageId: ObjectID): Promise<string> {
|
|
457
|
-
const
|
|
458
|
-
await StatusPageDomainService.
|
|
457
|
+
const domain: StatusPageDomain | null =
|
|
458
|
+
await StatusPageDomainService.findOneBy({
|
|
459
459
|
query: {
|
|
460
460
|
statusPageId: statusPageId,
|
|
461
461
|
isSslProvisioned: true,
|
|
@@ -463,21 +463,15 @@ export class Service extends DatabaseService<StatusPage> {
|
|
|
463
463
|
select: {
|
|
464
464
|
fullDomain: true,
|
|
465
465
|
},
|
|
466
|
-
skip: 0,
|
|
467
|
-
limit: LIMIT_PER_PROJECT,
|
|
468
466
|
props: {
|
|
469
467
|
isRoot: true,
|
|
470
468
|
ignoreHooks: true,
|
|
471
469
|
},
|
|
472
470
|
});
|
|
473
471
|
|
|
474
|
-
let statusPageURL: string =
|
|
475
|
-
.map((d: StatusPageDomain) => {
|
|
476
|
-
return d.fullDomain;
|
|
477
|
-
})
|
|
478
|
-
.join(", ");
|
|
472
|
+
let statusPageURL: string = domain?.fullDomain || "";
|
|
479
473
|
|
|
480
|
-
if (
|
|
474
|
+
if (!statusPageURL) {
|
|
481
475
|
const host: Hostname = await DatabaseConfig.getHost();
|
|
482
476
|
|
|
483
477
|
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
@@ -29,6 +29,7 @@ import StatusPageResource from "Common/Models/DatabaseModels/StatusPageResource"
|
|
|
29
29
|
import Model from "Common/Models/DatabaseModels/StatusPageSubscriber";
|
|
30
30
|
import PositiveNumber from "../../Types/PositiveNumber";
|
|
31
31
|
import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
|
|
32
|
+
import NumberUtil from "../../Utils/Number";
|
|
32
33
|
|
|
33
34
|
export class Service extends DatabaseService<Model> {
|
|
34
35
|
public constructor() {
|
|
@@ -160,6 +161,22 @@ export class Service extends DatabaseService<Model> {
|
|
|
160
161
|
|
|
161
162
|
data.data.projectId = statuspage.projectId;
|
|
162
163
|
|
|
164
|
+
const isEmailSubscriber: boolean = Boolean(data.data.subscriberEmail);
|
|
165
|
+
const isSubscriptionConfirmed: boolean = Boolean(
|
|
166
|
+
data.data.isSubscriptionConfirmed,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (isEmailSubscriber && !isSubscriptionConfirmed) {
|
|
170
|
+
data.data.isSubscriptionConfirmed = false;
|
|
171
|
+
} else {
|
|
172
|
+
data.data.isSubscriptionConfirmed = true; // if the subscriber is not email, then set it to true for SMS subscribers.
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
data.data.subscriptionConfirmationToken = NumberUtil.getRandomNumber(
|
|
176
|
+
100000,
|
|
177
|
+
999999,
|
|
178
|
+
).toString();
|
|
179
|
+
|
|
163
180
|
return { createBy: data, carryForward: statuspage };
|
|
164
181
|
}
|
|
165
182
|
|
|
@@ -180,10 +197,6 @@ export class Service extends DatabaseService<Model> {
|
|
|
180
197
|
onCreate.carryForward.name ||
|
|
181
198
|
"Status Page";
|
|
182
199
|
|
|
183
|
-
const host: Hostname = await DatabaseConfig.getHost();
|
|
184
|
-
|
|
185
|
-
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
186
|
-
|
|
187
200
|
const unsubscribeLink: string = this.getUnsubscribeLink(
|
|
188
201
|
URL.fromString(statusPageURL),
|
|
189
202
|
createdItem.id!,
|
|
@@ -237,28 +250,235 @@ export class Service extends DatabaseService<Model> {
|
|
|
237
250
|
if (
|
|
238
251
|
createdItem.statusPageId &&
|
|
239
252
|
createdItem.subscriberEmail &&
|
|
240
|
-
createdItem._id
|
|
241
|
-
createdItem.sendYouHaveSubscribedMessage
|
|
253
|
+
createdItem._id
|
|
242
254
|
) {
|
|
243
255
|
// Call mail service and send an email.
|
|
244
256
|
|
|
245
257
|
// get status page domain for this status page.
|
|
246
258
|
// if the domain is not found, use the internal status page preview link.
|
|
247
259
|
|
|
260
|
+
const isSubcriptionConfirmed: boolean = Boolean(
|
|
261
|
+
createdItem.isSubscriptionConfirmed,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
if (!isSubcriptionConfirmed) {
|
|
265
|
+
await this.sendConfirmSubscriptionEmail({
|
|
266
|
+
subscriberId: createdItem.id!,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (isSubcriptionConfirmed && createdItem.sendYouHaveSubscribedMessage) {
|
|
271
|
+
await this.sendYouHaveSubscribedEmail({
|
|
272
|
+
subscriberId: createdItem.id!,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return createdItem;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
public async sendConfirmSubscriptionEmail(data: {
|
|
281
|
+
subscriberId: ObjectID;
|
|
282
|
+
}): Promise<void> {
|
|
283
|
+
// get subscriber
|
|
284
|
+
const subscriber: Model | null = await this.findOneBy({
|
|
285
|
+
query: {
|
|
286
|
+
_id: data.subscriberId,
|
|
287
|
+
},
|
|
288
|
+
select: {
|
|
289
|
+
statusPageId: true,
|
|
290
|
+
subscriberEmail: true,
|
|
291
|
+
subscriberPhone: true,
|
|
292
|
+
projectId: true,
|
|
293
|
+
subscriptionConfirmationToken: true,
|
|
294
|
+
sendYouHaveSubscribedMessage: true,
|
|
295
|
+
},
|
|
296
|
+
props: {
|
|
297
|
+
isRoot: true,
|
|
298
|
+
ignoreHooks: true,
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// get status page
|
|
303
|
+
if (!subscriber || !subscriber.statusPageId) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
308
|
+
query: {
|
|
309
|
+
_id: subscriber.statusPageId.toString(),
|
|
310
|
+
},
|
|
311
|
+
select: {
|
|
312
|
+
logoFileId: true,
|
|
313
|
+
isPublicStatusPage: true,
|
|
314
|
+
pageTitle: true,
|
|
315
|
+
name: true,
|
|
316
|
+
smtpConfig: {
|
|
317
|
+
_id: true,
|
|
318
|
+
hostname: true,
|
|
319
|
+
port: true,
|
|
320
|
+
username: true,
|
|
321
|
+
password: true,
|
|
322
|
+
fromEmail: true,
|
|
323
|
+
fromName: true,
|
|
324
|
+
secure: true,
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
props: {
|
|
328
|
+
isRoot: true,
|
|
329
|
+
ignoreHooks: true,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (!statusPage || !statusPage.id) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const statusPageURL: string = await StatusPageService.getStatusPageURL(
|
|
338
|
+
statusPage.id,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const statusPageName: string =
|
|
342
|
+
statusPage.pageTitle || statusPage.name || "Status Page";
|
|
343
|
+
|
|
344
|
+
const host: Hostname = await DatabaseConfig.getHost();
|
|
345
|
+
|
|
346
|
+
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
347
|
+
|
|
348
|
+
const confirmSubscriptionLink: string = this.getConfirmSubscriptionLink({
|
|
349
|
+
statusPageUrl: statusPageURL,
|
|
350
|
+
confirmationToken: subscriber.subscriptionConfirmationToken || "",
|
|
351
|
+
statusPageSubscriberId: subscriber.id!,
|
|
352
|
+
}).toString();
|
|
353
|
+
|
|
354
|
+
if (
|
|
355
|
+
subscriber.statusPageId &&
|
|
356
|
+
subscriber.subscriberEmail &&
|
|
357
|
+
subscriber._id
|
|
358
|
+
) {
|
|
359
|
+
MailService.sendMail(
|
|
360
|
+
{
|
|
361
|
+
toEmail: subscriber.subscriberEmail,
|
|
362
|
+
templateType: EmailTemplateType.ConfirmStatusPageSubscription,
|
|
363
|
+
vars: {
|
|
364
|
+
statusPageName: statusPageName,
|
|
365
|
+
logoUrl: statusPage.logoFileId
|
|
366
|
+
? new URL(httpProtocol, host)
|
|
367
|
+
.addRoute(FileRoute)
|
|
368
|
+
.addRoute("/image/" + statusPage.logoFileId)
|
|
369
|
+
.toString()
|
|
370
|
+
: "",
|
|
371
|
+
statusPageUrl: statusPageURL,
|
|
372
|
+
isPublicStatusPage: statusPage.isPublicStatusPage
|
|
373
|
+
? "true"
|
|
374
|
+
: "false",
|
|
375
|
+
confirmationUrl: confirmSubscriptionLink,
|
|
376
|
+
},
|
|
377
|
+
subject: "Confirm your subscription to " + statusPageName,
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
projectId: subscriber.projectId,
|
|
381
|
+
mailServer: ProjectSMTPConfigService.toEmailServer(
|
|
382
|
+
statusPage.smtpConfig,
|
|
383
|
+
),
|
|
384
|
+
},
|
|
385
|
+
).catch((err: Error) => {
|
|
386
|
+
logger.error(err);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
public async sendYouHaveSubscribedEmail(data: {
|
|
392
|
+
subscriberId: ObjectID;
|
|
393
|
+
}): Promise<void> {
|
|
394
|
+
// get subscriber
|
|
395
|
+
const subscriber: Model | null = await this.findOneBy({
|
|
396
|
+
query: {
|
|
397
|
+
_id: data.subscriberId,
|
|
398
|
+
},
|
|
399
|
+
select: {
|
|
400
|
+
statusPageId: true,
|
|
401
|
+
subscriberEmail: true,
|
|
402
|
+
subscriberPhone: true,
|
|
403
|
+
projectId: true,
|
|
404
|
+
sendYouHaveSubscribedMessage: true,
|
|
405
|
+
},
|
|
406
|
+
props: {
|
|
407
|
+
isRoot: true,
|
|
408
|
+
ignoreHooks: true,
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// get status page
|
|
413
|
+
if (!subscriber || !subscriber.statusPageId) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
418
|
+
query: {
|
|
419
|
+
_id: subscriber.statusPageId.toString(),
|
|
420
|
+
},
|
|
421
|
+
select: {
|
|
422
|
+
logoFileId: true,
|
|
423
|
+
isPublicStatusPage: true,
|
|
424
|
+
pageTitle: true,
|
|
425
|
+
name: true,
|
|
426
|
+
smtpConfig: {
|
|
427
|
+
_id: true,
|
|
428
|
+
hostname: true,
|
|
429
|
+
port: true,
|
|
430
|
+
username: true,
|
|
431
|
+
password: true,
|
|
432
|
+
fromEmail: true,
|
|
433
|
+
fromName: true,
|
|
434
|
+
secure: true,
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
props: {
|
|
438
|
+
isRoot: true,
|
|
439
|
+
ignoreHooks: true,
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (!statusPage || !statusPage.id) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const statusPageURL: string = await StatusPageService.getStatusPageURL(
|
|
448
|
+
statusPage.id,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const statusPageName: string =
|
|
452
|
+
statusPage.pageTitle || statusPage.name || "Status Page";
|
|
453
|
+
|
|
454
|
+
const host: Hostname = await DatabaseConfig.getHost();
|
|
455
|
+
|
|
456
|
+
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
457
|
+
|
|
458
|
+
const unsubscribeLink: string = this.getUnsubscribeLink(
|
|
459
|
+
URL.fromString(statusPageURL),
|
|
460
|
+
subscriber.id!,
|
|
461
|
+
).toString();
|
|
462
|
+
|
|
463
|
+
if (
|
|
464
|
+
subscriber.statusPageId &&
|
|
465
|
+
subscriber.subscriberEmail &&
|
|
466
|
+
subscriber._id
|
|
467
|
+
) {
|
|
248
468
|
MailService.sendMail(
|
|
249
469
|
{
|
|
250
|
-
toEmail:
|
|
470
|
+
toEmail: subscriber.subscriberEmail,
|
|
251
471
|
templateType: EmailTemplateType.SubscribedToStatusPage,
|
|
252
472
|
vars: {
|
|
253
473
|
statusPageName: statusPageName,
|
|
254
|
-
logoUrl:
|
|
474
|
+
logoUrl: statusPage.logoFileId
|
|
255
475
|
? new URL(httpProtocol, host)
|
|
256
476
|
.addRoute(FileRoute)
|
|
257
|
-
.addRoute("/image/" +
|
|
477
|
+
.addRoute("/image/" + statusPage.logoFileId)
|
|
258
478
|
.toString()
|
|
259
479
|
: "",
|
|
260
480
|
statusPageUrl: statusPageURL,
|
|
261
|
-
isPublicStatusPage:
|
|
481
|
+
isPublicStatusPage: statusPage.isPublicStatusPage
|
|
262
482
|
? "true"
|
|
263
483
|
: "false",
|
|
264
484
|
unsubscribeUrl: unsubscribeLink,
|
|
@@ -266,17 +486,25 @@ export class Service extends DatabaseService<Model> {
|
|
|
266
486
|
subject: "You have been subscribed to " + statusPageName,
|
|
267
487
|
},
|
|
268
488
|
{
|
|
269
|
-
projectId:
|
|
489
|
+
projectId: subscriber.projectId,
|
|
270
490
|
mailServer: ProjectSMTPConfigService.toEmailServer(
|
|
271
|
-
|
|
491
|
+
statusPage.smtpConfig,
|
|
272
492
|
),
|
|
273
493
|
},
|
|
274
494
|
).catch((err: Error) => {
|
|
275
495
|
logger.error(err);
|
|
276
496
|
});
|
|
277
497
|
}
|
|
498
|
+
}
|
|
278
499
|
|
|
279
|
-
|
|
500
|
+
public getConfirmSubscriptionLink(data: {
|
|
501
|
+
statusPageUrl: string;
|
|
502
|
+
confirmationToken: string;
|
|
503
|
+
statusPageSubscriberId: ObjectID;
|
|
504
|
+
}): URL {
|
|
505
|
+
return URL.fromString(data.statusPageUrl).addRoute(
|
|
506
|
+
`/confirm-subscription/${data.statusPageSubscriberId.toString()}?verification-token=${data.confirmationToken}`,
|
|
507
|
+
);
|
|
280
508
|
}
|
|
281
509
|
|
|
282
510
|
public async getSubscribersByStatusPage(
|
|
@@ -287,6 +515,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
287
515
|
query: {
|
|
288
516
|
statusPageId: statusPageId,
|
|
289
517
|
isUnsubscribed: false,
|
|
518
|
+
isSubscriptionConfirmed: true,
|
|
290
519
|
},
|
|
291
520
|
select: {
|
|
292
521
|
_id: true,
|
|
@@ -3,6 +3,7 @@ enum EmailTemplateType {
|
|
|
3
3
|
ProbeOffline = "ProbeOffline.hbs",
|
|
4
4
|
SignupWelcomeEmail = "SignupWelcomeEmail.hbs",
|
|
5
5
|
ProbeConnectionStatusChange = "ProbeConnectionStatusChange.hbs",
|
|
6
|
+
ConfirmStatusPageSubscription = "ConfirmStatusPageSubscription.hbs",
|
|
6
7
|
EmailVerified = "EmailVerified.hbs",
|
|
7
8
|
PasswordChanged = "PasswordChanged.hbs",
|
|
8
9
|
ProbeOwnerAdded = "ProbeOwnerAdded.hbs",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Black } from "Common/Types/BrandColors";
|
|
2
2
|
import Color from "Common/Types/Color";
|
|
3
3
|
import React, { CSSProperties, FunctionComponent, ReactElement } from "react";
|
|
4
|
+
import Tooltip from "../Tooltip/Tooltip";
|
|
5
|
+
import { GetReactElementFunction } from "../../Types/FunctionTypes";
|
|
4
6
|
|
|
5
7
|
export enum PillSize {
|
|
6
8
|
Small = "10px",
|
|
@@ -15,6 +17,7 @@ export interface ComponentProps {
|
|
|
15
17
|
size?: PillSize | undefined;
|
|
16
18
|
style?: CSSProperties;
|
|
17
19
|
isMinimal?: boolean | undefined;
|
|
20
|
+
tooltip?: string | undefined;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
const Pill: FunctionComponent<ComponentProps> = (
|
|
@@ -39,29 +42,38 @@ const Pill: FunctionComponent<ComponentProps> = (
|
|
|
39
42
|
</span>
|
|
40
43
|
);
|
|
41
44
|
}
|
|
42
|
-
return (
|
|
43
|
-
<span
|
|
44
|
-
data-testid="pill"
|
|
45
|
-
className="rounded-full p-1 pl-3 pr-3"
|
|
46
|
-
style={{
|
|
47
|
-
// https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
46
|
+
const getPillElement: GetReactElementFunction = (): ReactElement => {
|
|
47
|
+
return (
|
|
48
|
+
<span
|
|
49
|
+
data-testid="pill"
|
|
50
|
+
className="rounded-full p-1 pl-3 pr-3"
|
|
51
|
+
style={{
|
|
52
|
+
// https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
|
|
53
|
+
|
|
54
|
+
color:
|
|
55
|
+
props.style?.color || Color.shouldUseDarkText(props.color || Black)
|
|
56
|
+
? "#000000"
|
|
57
|
+
: "#ffffff",
|
|
58
|
+
backgroundColor:
|
|
59
|
+
props.style?.backgroundColor || props.color
|
|
60
|
+
? props.color.toString()
|
|
61
|
+
: Black.toString(),
|
|
62
|
+
fontSize: props.size ? props.size.toString() : PillSize.Normal,
|
|
63
|
+
...props.style,
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{" "}
|
|
67
|
+
{props.text}{" "}
|
|
68
|
+
</span>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (props.tooltip) {
|
|
73
|
+
return <Tooltip text={props.tooltip}>{getPillElement()}</Tooltip>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return getPillElement();
|
|
65
77
|
};
|
|
66
78
|
|
|
67
79
|
export default Pill;
|
|
@@ -47,6 +47,8 @@ let StatusPageSubscriber = class StatusPageSubscriber extends BaseModel {
|
|
|
47
47
|
this.createdByUserId = undefined;
|
|
48
48
|
this.deletedByUser = undefined;
|
|
49
49
|
this.deletedByUserId = undefined;
|
|
50
|
+
this.isSubscriptionConfirmed = undefined;
|
|
51
|
+
this.subscriptionConfirmationToken = undefined;
|
|
50
52
|
this.isUnsubscribed = undefined;
|
|
51
53
|
this.sendYouHaveSubscribedMessage = undefined;
|
|
52
54
|
this.isSubscribedToAllResources = undefined;
|
|
@@ -400,6 +402,63 @@ __decorate([
|
|
|
400
402
|
}),
|
|
401
403
|
__metadata("design:type", ObjectID)
|
|
402
404
|
], StatusPageSubscriber.prototype, "deletedByUserId", void 0);
|
|
405
|
+
__decorate([
|
|
406
|
+
ColumnAccessControl({
|
|
407
|
+
create: [
|
|
408
|
+
Permission.ProjectOwner,
|
|
409
|
+
Permission.ProjectAdmin,
|
|
410
|
+
Permission.ProjectMember,
|
|
411
|
+
Permission.CreateStatusPageSubscriber,
|
|
412
|
+
Permission.Public,
|
|
413
|
+
],
|
|
414
|
+
read: [
|
|
415
|
+
Permission.ProjectOwner,
|
|
416
|
+
Permission.ProjectAdmin,
|
|
417
|
+
Permission.ProjectMember,
|
|
418
|
+
Permission.ReadStatusPageSubscriber,
|
|
419
|
+
],
|
|
420
|
+
update: [
|
|
421
|
+
Permission.ProjectOwner,
|
|
422
|
+
Permission.ProjectAdmin,
|
|
423
|
+
Permission.ProjectMember,
|
|
424
|
+
Permission.EditStatusPageSubscriber,
|
|
425
|
+
],
|
|
426
|
+
}),
|
|
427
|
+
TableColumn({
|
|
428
|
+
isDefaultValueColumn: true,
|
|
429
|
+
type: TableColumnType.Boolean,
|
|
430
|
+
title: "Is Subscription Confirmed",
|
|
431
|
+
description: "Has subscriber confirmed their subscription? (for example, by clicking on a confirmation link in an email)",
|
|
432
|
+
}),
|
|
433
|
+
Column({
|
|
434
|
+
type: ColumnType.Boolean,
|
|
435
|
+
default: false,
|
|
436
|
+
}),
|
|
437
|
+
__metadata("design:type", Boolean)
|
|
438
|
+
], StatusPageSubscriber.prototype, "isSubscriptionConfirmed", void 0);
|
|
439
|
+
__decorate([
|
|
440
|
+
ColumnAccessControl({
|
|
441
|
+
create: [
|
|
442
|
+
Permission.ProjectOwner,
|
|
443
|
+
Permission.ProjectAdmin,
|
|
444
|
+
Permission.ProjectMember,
|
|
445
|
+
Permission.CreateStatusPageSubscriber,
|
|
446
|
+
],
|
|
447
|
+
read: [],
|
|
448
|
+
update: [],
|
|
449
|
+
}),
|
|
450
|
+
TableColumn({
|
|
451
|
+
isDefaultValueColumn: false,
|
|
452
|
+
type: TableColumnType.ShortText,
|
|
453
|
+
title: "Subscription Confirmation Token",
|
|
454
|
+
description: "Token used to confirm subscription. This is a random token that is sent to the subscriber's email address to confirm their subscription.",
|
|
455
|
+
}),
|
|
456
|
+
Column({
|
|
457
|
+
type: ColumnType.ShortText,
|
|
458
|
+
nullable: true,
|
|
459
|
+
}),
|
|
460
|
+
__metadata("design:type", String)
|
|
461
|
+
], StatusPageSubscriber.prototype, "subscriptionConfirmationToken", void 0);
|
|
403
462
|
__decorate([
|
|
404
463
|
ColumnAccessControl({
|
|
405
464
|
create: [
|