From bb7917551f5c18c1daf2d65093cbfcebc54bfddd Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Tue, 17 Dec 2024 12:42:22 +0000 Subject: [PATCH] Add confirmation functionality for status page subscriptions and update related templates --- .../ConfirmStatusPageSubscription.hbs | 17 ++ .../Templates/SubscribedToStatusPage.hbs | 2 +- .../DatabaseModels/StatusPageSubscriber.ts | 55 ++++ Common/Server/API/StatusPageAPI.ts | 55 ++++ .../1734435866602-MigrationName.ts | 16 ++ .../Postgres/SchemaMigrations/Index.ts | 2 + Common/Server/Services/StatusPageService.ts | 14 +- .../Services/StatusPageSubscriberService.ts | 265 +++++++++++++++++- Common/Types/Email/EmailTemplateType.ts | 1 + .../Pages/Subscribe/ConfirmSubscription.tsx | 119 ++++++++ StatusPage/src/Utils/PageMap.ts | 2 + StatusPage/src/Utils/RouteMap.ts | 5 + ...AddIsSubscriptionConfirmedToSubscribers.ts | 45 +++ Worker/DataMigrations/Index.ts | 2 + 14 files changed, 575 insertions(+), 25 deletions(-) create mode 100644 App/FeatureSet/Notification/Templates/ConfirmStatusPageSubscription.hbs create mode 100644 Common/Server/Infrastructure/Postgres/SchemaMigrations/1734435866602-MigrationName.ts create mode 100644 StatusPage/src/Pages/Subscribe/ConfirmSubscription.tsx create mode 100644 Worker/DataMigrations/AddIsSubscriptionConfirmedToSubscribers.ts diff --git a/App/FeatureSet/Notification/Templates/ConfirmStatusPageSubscription.hbs b/App/FeatureSet/Notification/Templates/ConfirmStatusPageSubscription.hbs new file mode 100644 index 0000000000..965be7fda7 --- /dev/null +++ b/App/FeatureSet/Notification/Templates/ConfirmStatusPageSubscription.hbs @@ -0,0 +1,17 @@ +{{> Start this}} + +{{> CustomLogo this}} +{{> EmailTitle title=(concat statusPageName " - Please confirm your subscription" ) }} + +{{> InfoBlock info="You will be the first to hear from us when there are any incidents, announcements or scheduled maintenance events."}} + +{{> ButtonBlock buttonUrl=confirmationUrl buttonText="Confirm Subscription"}} + +{{> InfoBlock info="You can also view the status page by visiting this link:"}} +{{> InfoBlock info=statusPageUrl}} + +{{> UnsubscribeBlock this}} + +{{> VerticalSpace this}} + +{{> End this}} diff --git a/App/FeatureSet/Notification/Templates/SubscribedToStatusPage.hbs b/App/FeatureSet/Notification/Templates/SubscribedToStatusPage.hbs index e352fe1523..16d7117da3 100644 --- a/App/FeatureSet/Notification/Templates/SubscribedToStatusPage.hbs +++ b/App/FeatureSet/Notification/Templates/SubscribedToStatusPage.hbs @@ -7,7 +7,7 @@ {{> ButtonBlock buttonUrl=statusPageUrl buttonText="Go to Status Page"}} -{{> InfoBlock info="You can also view the status page by visiting these link:"}} +{{> InfoBlock info="You can also view the status page by visiting this link:"}} {{> InfoBlock info=statusPageUrl}} {{> UnsubscribeBlock this}} diff --git a/Common/Models/DatabaseModels/StatusPageSubscriber.ts b/Common/Models/DatabaseModels/StatusPageSubscriber.ts index a15a6c8e06..c69fce7a0c 100644 --- a/Common/Models/DatabaseModels/StatusPageSubscriber.ts +++ b/Common/Models/DatabaseModels/StatusPageSubscriber.ts @@ -434,6 +434,61 @@ export default class StatusPageSubscriber extends BaseModel { }) public deletedByUserId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CreateStatusPageSubscriber, + Permission.Public, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadStatusPageSubscriber, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.EditStatusPageSubscriber, + ], + }) + @TableColumn({ + isDefaultValueColumn: true, + type: TableColumnType.Boolean, + title: "Is Subscription Confirmed", + description: "Has subscriber confirmed their subscription? (for example, by clicking on a confirmation link in an email)", + }) + @Column({ + type: ColumnType.Boolean, + default: false, + }) + public isSubscriptionConfirmed?: boolean = undefined; + + + @ColumnAccessControl({ + create: [ + ], + read: [ + ], + update: [ + ], + }) + @TableColumn({ + isDefaultValueColumn: false, + type: TableColumnType.ShortText, + title: "Subscription Confirmation Token", + description: "Token used to confirm subscription. This is a random token that is sent to the subscriber's email address to confirm their subscription.", + }) + @Column({ + type: ColumnType.ShortText, + nullable: true + }) + public subscriptionConfirmationToken?: string = undefined; + @ColumnAccessControl({ create: [ Permission.ProjectOwner, diff --git a/Common/Server/API/StatusPageAPI.ts b/Common/Server/API/StatusPageAPI.ts index 5d20c8dadd..f3c3275c8e 100644 --- a/Common/Server/API/StatusPageAPI.ts +++ b/Common/Server/API/StatusPageAPI.ts @@ -82,6 +82,61 @@ export default class StatusPageAPI extends BaseAPI< public constructor() { super(StatusPage, StatusPageService); + + // confirm subscription api + this.router.get( + `${new this.entityType() + .getCrudApiPath() + ?.toString()}/confirm-subscription/:statusPageSubscriberId`, + async (req: ExpressRequest, res: ExpressResponse) => { + const token: string = req.query["token"] as string; + + const statusPageSubscriberId: ObjectID = new ObjectID( + req.params["statusPageSubscriberId"] as string, + ); + + const subscriber: StatusPageSubscriber | null = + await StatusPageSubscriberService.findOneBy({ + query: { + _id: statusPageSubscriberId, + subscriptionConfirmationToken: token, + }, + select: { + isSubscriptionConfirmed: true, + }, + props: { + isRoot: true, + }, + }); + + if (!subscriber) { + return Response.sendErrorResponse( + req, + res, + new NotFoundException("Subscriber not found or confirmation token is invalid"), + ); + } + + // check if subscription confirmed already. + + if (subscriber.isSubscriptionConfirmed) { + return Response.sendEmptySuccessResponse(req, res); + } + + await StatusPageSubscriberService.updateOneById({ + id: statusPageSubscriberId, + data: { + isSubscriptionConfirmed: true, + }, + props: { + isRoot: true, + }, + }); + + return Response.sendEmptySuccessResponse(req, res); + }, + ); + // CNAME verification api this.router.get( `${new this.entityType() diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1734435866602-MigrationName.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1734435866602-MigrationName.ts new file mode 100644 index 0000000000..fdff8f4ee1 --- /dev/null +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1734435866602-MigrationName.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MigrationName1734435866602 implements MigrationInterface { + public name = 'MigrationName1734435866602' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "StatusPageSubscriber" ADD "isSubscriptionConfirmed" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "StatusPageSubscriber" ADD "subscriptionConfirmationToken" character varying`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "subscriptionConfirmationToken"`); + await queryRunner.query(`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "isSubscriptionConfirmed"`); + } + +} diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts index d3468db4a9..b8284d0b7d 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts @@ -83,6 +83,7 @@ import { MigrationName1731433309124 } from "./1731433309124-MigrationName"; import { MigrationName1731435267537 } from "./1731435267537-MigrationName"; import { MigrationName1731435514287 } from "./1731435514287-MigrationName"; import { MigrationName1732553444010 } from "./1732553444010-MigrationName"; +import { MigrationName1734435866602 } from "./1734435866602-MigrationName"; export default [ InitialMigration, @@ -170,4 +171,5 @@ export default [ MigrationName1731435267537, MigrationName1731435514287, MigrationName1732553444010, + MigrationName1734435866602 ]; diff --git a/Common/Server/Services/StatusPageService.ts b/Common/Server/Services/StatusPageService.ts index 2f08324be6..d4f657afb2 100755 --- a/Common/Server/Services/StatusPageService.ts +++ b/Common/Server/Services/StatusPageService.ts @@ -454,8 +454,8 @@ export class Service extends DatabaseService { } public async getStatusPageURL(statusPageId: ObjectID): Promise { - const domains: Array = - await StatusPageDomainService.findBy({ + const domain: StatusPageDomain | null = + await StatusPageDomainService.findOneBy({ query: { statusPageId: statusPageId, isSslProvisioned: true, @@ -463,21 +463,15 @@ export class Service extends DatabaseService { select: { fullDomain: true, }, - skip: 0, - limit: LIMIT_PER_PROJECT, props: { isRoot: true, ignoreHooks: true, }, }); - let statusPageURL: string = domains - .map((d: StatusPageDomain) => { - return d.fullDomain; - }) - .join(", "); + let statusPageURL: string = domain?.fullDomain || ""; - if (domains.length === 0) { + if (!statusPageURL) { const host: Hostname = await DatabaseConfig.getHost(); const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol(); diff --git a/Common/Server/Services/StatusPageSubscriberService.ts b/Common/Server/Services/StatusPageSubscriberService.ts index 06cc2e2c7e..8df96c788c 100644 --- a/Common/Server/Services/StatusPageSubscriberService.ts +++ b/Common/Server/Services/StatusPageSubscriberService.ts @@ -29,6 +29,7 @@ import StatusPageResource from "Common/Models/DatabaseModels/StatusPageResource" import Model from "Common/Models/DatabaseModels/StatusPageSubscriber"; import PositiveNumber from "../../Types/PositiveNumber"; import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType"; +import NumberUtil from "../../Utils/Number"; export class Service extends DatabaseService { public constructor() { @@ -160,6 +161,17 @@ export class Service extends DatabaseService { data.data.projectId = statuspage.projectId; + const isEmailSubscriber: boolean = !!data.data.subscriberEmail; + const isSubscriptionConfirmed: boolean = !!data.data.isSubscriptionConfirmed; + + if (isEmailSubscriber && !isSubscriptionConfirmed) { + data.data.isSubscriptionConfirmed = false; + }else{ + data.data.isSubscriptionConfirmed = true; // if the subscriber is not email, then set it to true for SMS subscribers. + } + + data.data.subscriptionConfirmationToken = NumberUtil.getRandomNumber(100000, 999999).toString(); + return { createBy: data, carryForward: statuspage }; } @@ -180,15 +192,14 @@ export class Service extends DatabaseService { onCreate.carryForward.name || "Status Page"; - const host: Hostname = await DatabaseConfig.getHost(); - - const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol(); const unsubscribeLink: string = this.getUnsubscribeLink( URL.fromString(statusPageURL), createdItem.id!, ).toString(); + + if ( createdItem.statusPageId && createdItem.subscriberPhone && @@ -237,28 +248,244 @@ export class Service extends DatabaseService { if ( createdItem.statusPageId && createdItem.subscriberEmail && - createdItem._id && - createdItem.sendYouHaveSubscribedMessage + createdItem._id ) { + // Call mail service and send an email. // get status page domain for this status page. // if the domain is not found, use the internal status page preview link. + const isSubcriptionConfirmed: boolean = !!createdItem.isSubscriptionConfirmed; + + if (!isSubcriptionConfirmed) { + + await this.sendConfirmSubscriptionEmail({ + subscriberId: createdItem.id!, + }); + } + + if (isSubcriptionConfirmed && createdItem.sendYouHaveSubscribedMessage) { + + await this.sendYouHaveSubscribedEmail({ + subscriberId: createdItem.id!, + }); + } + } + + return createdItem; + } + + + public async sendConfirmSubscriptionEmail(data: { + subscriberId: ObjectID; + }): Promise { + // get subscriber + const subscriber: Model | null = await this.findOneBy({ + query: { + _id: data.subscriberId, + }, + select: { + statusPageId: true, + subscriberEmail: true, + subscriberPhone: true, + projectId: true, + subscriptionConfirmationToken: true, + sendYouHaveSubscribedMessage: true, + }, + props: { + isRoot: true, + ignoreHooks: true, + }, + }); + + // get status page + if (!subscriber || !subscriber.statusPageId) { + return; + } + + const statusPage: StatusPage | null = await StatusPageService.findOneBy({ + query: { + _id: subscriber.statusPageId.toString(), + }, + select: { + logoFileId: true, + isPublicStatusPage: true, + pageTitle: true, + name: true, + smtpConfig: { + _id: true, + hostname: true, + port: true, + username: true, + password: true, + fromEmail: true, + fromName: true, + secure: true, + }, + }, + props: { + isRoot: true, + ignoreHooks: true, + }, + }); + + if (!statusPage || !statusPage.id) { + return; + } + + const statusPageURL: string = await StatusPageService.getStatusPageURL( + statusPage.id, + ); + + const statusPageName: string = statusPage.pageTitle || statusPage.name || "Status Page"; + + const host: Hostname = await DatabaseConfig.getHost(); + + const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol(); + + const confirmSubscriptionLink: string = this.getConfirmSubscriptionLink( + { + statusPageUrl: statusPageURL, + confirmationToken: subscriber.subscriptionConfirmationToken || "", + statusPageSubscriberId: subscriber.id!, + statusPageId: subscriber.statusPageId, + } + ).toString(); + + if ( + subscriber.statusPageId && + subscriber.subscriberEmail && + subscriber._id + ) { MailService.sendMail( { - toEmail: createdItem.subscriberEmail, + toEmail: subscriber.subscriberEmail, + templateType: EmailTemplateType.ConfirmStatusPageSubscription, + vars: { + statusPageName: statusPageName, + logoUrl: statusPage.logoFileId + ? new URL(httpProtocol + , host) + .addRoute(FileRoute) + .addRoute("/image/" + statusPage.logoFileId) + .toString() + : "", + statusPageUrl: statusPageURL, + isPublicStatusPage: statusPage.isPublicStatusPage + ? "true" + : "false", + confirmSubscriptionUrl: confirmSubscriptionLink, + }, + subject: "Confirm your subscription to " + statusPageName, + }, + { + projectId: subscriber.projectId, + mailServer: ProjectSMTPConfigService.toEmailServer( + statusPage.smtpConfig, + ), + }, + ).catch((err: Error) => { + logger.error(err); + } + ); + } + } + + + public async sendYouHaveSubscribedEmail(data: { + subscriberId: ObjectID; + }): Promise { + + + // get subscriber + const subscriber: Model | null = await this.findOneBy({ + query: { + _id: data.subscriberId, + }, + select: { + statusPageId: true, + subscriberEmail: true, + subscriberPhone: true, + projectId: true, + sendYouHaveSubscribedMessage: true, + }, + props: { + isRoot: true, + ignoreHooks: true, + }, + }); + + // get status page + if (!subscriber || !subscriber.statusPageId) { + return; + } + + const statusPage: StatusPage | null = await StatusPageService.findOneBy({ + query: { + _id: subscriber.statusPageId.toString(), + }, + select: { + logoFileId: true, + isPublicStatusPage: true, + pageTitle: true, + name: true, + smtpConfig: { + _id: true, + hostname: true, + port: true, + username: true, + password: true, + fromEmail: true, + fromName: true, + secure: true, + }, + }, + props: { + isRoot: true, + ignoreHooks: true, + }, + }); + + if(!statusPage || !statusPage.id) { + return; + } + + + const statusPageURL: string = await StatusPageService.getStatusPageURL( + statusPage.id, + ); + + const statusPageName: string = statusPage.pageTitle ||statusPage.name || "Status Page"; + + const host: Hostname = await DatabaseConfig.getHost(); + + const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol(); + + const unsubscribeLink: string = this.getUnsubscribeLink( + URL.fromString(statusPageURL), + subscriber.id!, + ).toString(); + + if ( + subscriber.statusPageId && + subscriber.subscriberEmail && + subscriber._id + ) { + MailService.sendMail( + { + toEmail: subscriber.subscriberEmail, templateType: EmailTemplateType.SubscribedToStatusPage, vars: { statusPageName: statusPageName, - logoUrl: onCreate.carryForward.logoFileId + logoUrl: statusPage.logoFileId ? new URL(httpProtocol, host) - .addRoute(FileRoute) - .addRoute("/image/" + onCreate.carryForward.logoFileId) - .toString() + .addRoute(FileRoute) + .addRoute("/image/" + statusPage.logoFileId) + .toString() : "", statusPageUrl: statusPageURL, - isPublicStatusPage: onCreate.carryForward.isPublicStatusPage + isPublicStatusPage: statusPage.isPublicStatusPage ? "true" : "false", unsubscribeUrl: unsubscribeLink, @@ -266,17 +493,27 @@ export class Service extends DatabaseService { subject: "You have been subscribed to " + statusPageName, }, { - projectId: createdItem.projectId, + projectId: subscriber.projectId, mailServer: ProjectSMTPConfigService.toEmailServer( - onCreate.carryForward.smtpConfig, + statusPage.smtpConfig, ), }, ).catch((err: Error) => { logger.error(err); }); } + } - return createdItem; + + public getConfirmSubscriptionLink(data: { + statusPageUrl: string; + confirmationToken: string; + statusPageSubscriberId: ObjectID + statusPageId: ObjectID + }): URL { + return URL.fromString(data.statusPageUrl).addRoute( + `/confirm-subscription/${data.statusPageId.toString()}/${data.statusPageSubscriberId.toString()}?token=${data.confirmationToken}`, + ); } public async getSubscribersByStatusPage( diff --git a/Common/Types/Email/EmailTemplateType.ts b/Common/Types/Email/EmailTemplateType.ts index a72ee5f05c..edf427e236 100644 --- a/Common/Types/Email/EmailTemplateType.ts +++ b/Common/Types/Email/EmailTemplateType.ts @@ -3,6 +3,7 @@ enum EmailTemplateType { ProbeOffline = "ProbeOffline.hbs", SignupWelcomeEmail = "SignupWelcomeEmail.hbs", ProbeConnectionStatusChange = "ProbeConnectionStatusChange.hbs", + ConfirmStatusPageSubscription = "ConfirmStatusPageSubscription.hbs", EmailVerified = "EmailVerified.hbs", PasswordChanged = "PasswordChanged.hbs", ProbeOwnerAdded = "ProbeOwnerAdded.hbs", diff --git a/StatusPage/src/Pages/Subscribe/ConfirmSubscription.tsx b/StatusPage/src/Pages/Subscribe/ConfirmSubscription.tsx new file mode 100644 index 0000000000..2f5affccbe --- /dev/null +++ b/StatusPage/src/Pages/Subscribe/ConfirmSubscription.tsx @@ -0,0 +1,119 @@ +import Page from "../../Components/Page/Page"; +import API from "../../Utils/API"; +import { STATUS_PAGE_API_URL } from "../../Utils/Config"; +import PageMap from "../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../Utils/RouteMap"; +import StatusPageUtil from "../../Utils/StatusPage"; +import { SubscribePageProps } from "./SubscribePageUtils"; +import Route from "Common/Types/API/Route"; +import URL from "Common/Types/API/URL"; +import BadDataException from "Common/Types/Exception/BadDataException"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import ObjectID from "Common/Types/ObjectID"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import LocalStorage from "Common/UI/Utils/LocalStorage"; +import React, { + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import Navigation from "Common/UI/Utils/Navigation"; +import HTTPResponse from "Common/Types/API/HTTPResponse"; +import { JSONObject } from "Common/Types/JSON"; +import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse"; + +const SubscribePage: FunctionComponent = ( + _props: SubscribePageProps, +): ReactElement => { + + const id: ObjectID = LocalStorage.getItem("statusPageId") as ObjectID; + + + const [isLaoding, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + const confirmSubscription: PromiseVoidFunction = + async (): Promise => { + try { + setIsLoading(true); + + const statusPageSubscriberId: string = Navigation.getLastParamAsObjectID().toString(); + const token: string | null = Navigation.getQueryStringByName('token'); + + if (!token) { + setError("Token is required"); + return; + } + + if (!statusPageSubscriberId) { + setError("Subscriber ID is required"); + return; + } + + // hit the confirm subscription endpoint + const response: HTTPResponse | HTTPErrorResponse = await API.get( + URL.fromString(STATUS_PAGE_API_URL.toString()) + .addRoute(`/confirm-subscription/${statusPageSubscriberId}`) + .addQueryParam("token", token)); + + + if (response instanceof HTTPErrorResponse) { + throw response; + } + + + setError("Subscription confirmed successfully"); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + + setIsLoading(false); + }; + + useEffect(() => { + confirmSubscription().catch((error: Error) => { + setError(error.message); + }); + }, []); + + if (!id) { + throw new BadDataException("Status Page ID is required"); + } + + StatusPageUtil.checkIfUserHasLoggedIn(); + + + return ( + + {isLaoding ? : <>} + + {error ? : <>} + + + + ); +}; + +export default SubscribePage; diff --git a/StatusPage/src/Utils/PageMap.ts b/StatusPage/src/Utils/PageMap.ts index 977dbae60a..8f1b4bc615 100644 --- a/StatusPage/src/Utils/PageMap.ts +++ b/StatusPage/src/Utils/PageMap.ts @@ -9,6 +9,7 @@ enum PageMap { RSS = "RSS", SUBSCRIBE_EMAIL = "SUBSCRIBE_EMAIL", + CONFIRM_SUBSCRIPTION = "CONFIRM_SUBSCRIPTION", SUBSCRIBE_SMS = "SUBSCRIBE_SMS", SUBSCRIBE_WEBHOOKS = "SUBSCRIBE_WEBHOOKS", UPDATE_SUBSCRIPTION = "UPDATE_SUBSCRIPTION", @@ -25,6 +26,7 @@ enum PageMap { PREVIEW_RSS = "PREVIEW_RSS", PREVIEW_SUBSCRIBE_EMAIL = "PREVIEW_SUBSCRIBE_EMAIL", + PREVIEW_CONFIRM_SUBSCRIPTION = "PREVIEW_CONFIRM_SUBSCRIPTION", PREVIEW_SUBSCRIBE_SMS = "PREVIEW_SUBSCRIBE_SMS", PREVIEW_SUBSCRIBE_WEBHOOKS = "PREVIEW_SUBSCRIBE_WEBHOOKS", PREVIEW_UPDATE_SUBSCRIPTION = "PREVIEW_UPDATE_SUBSCRIPTION", diff --git a/StatusPage/src/Utils/RouteMap.ts b/StatusPage/src/Utils/RouteMap.ts index b13004b404..75755c1ae5 100644 --- a/StatusPage/src/Utils/RouteMap.ts +++ b/StatusPage/src/Utils/RouteMap.ts @@ -18,6 +18,7 @@ const RouteMap: Dictionary = { [PageMap.SUBSCRIBE_SMS]: new Route(`/subscribe/sms`), [PageMap.SUBSCRIBE_WEBHOOKS]: new Route(`/subscribe/webhooks`), [PageMap.UPDATE_SUBSCRIPTION]: new Route(`/update-subscription/:id`), + [PageMap.CONFIRM_SUBSCRIPTION]: new Route(`/confirm-subscription/:id`), [PageMap.LOGIN]: new Route(`/login`), [PageMap.SSO]: new Route(`/sso`), @@ -85,6 +86,10 @@ const RouteMap: Dictionary = { [PageMap.PREVIEW_UPDATE_SUBSCRIPTION]: new Route( `/status-page/${RouteParams.StatusPageId}/update-subscription/:id`, ), + + [PageMap.PREVIEW_CONFIRM_SUBSCRIPTION]: new Route( + `/status-page/${RouteParams.StatusPageId}/confirm-subscription/:id`, + ), }; export class RouteUtil { diff --git a/Worker/DataMigrations/AddIsSubscriptionConfirmedToSubscribers.ts b/Worker/DataMigrations/AddIsSubscriptionConfirmedToSubscribers.ts new file mode 100644 index 0000000000..e01f267f28 --- /dev/null +++ b/Worker/DataMigrations/AddIsSubscriptionConfirmedToSubscribers.ts @@ -0,0 +1,45 @@ +import DataMigrationBase from "./DataMigrationBase"; +import NumberUtil from "Common/Utils/Number"; +import LIMIT_MAX from "Common/Types/Database/LimitMax"; +import StatusPageSubscriber from "Common/Models/DatabaseModels/StatusPageSubscriber"; +import StatusPageSubscriberService from "Common/Server/Services/StatusPageSubscriberService"; + +export default class AddIsSubscriptionConfirmedToSubscribers extends DataMigrationBase { + public constructor() { + super("AddIsSubscriptionConfirmedToSubscribers"); + } + + public override async migrate(): Promise { + // get all the users with email isVerified true. + + const subscribers: Array = await StatusPageSubscriberService.findBy({ + query: {}, + select: { + _id: true, + }, + skip: 0, + limit: LIMIT_MAX, + props: { + isRoot: true, + }, + }); + + for (const subscriber of subscribers) { + // update subscriber with isSubscriptionConfirmed true. + await StatusPageSubscriberService.updateOneById({ + id: subscriber.id!, + data: { + isSubscriptionConfirmed: true, + subscriptionConfirmationToken: NumberUtil.getRandomNumber(100000, 999999).toString(), + }, + props: { + isRoot: true, + } + }); + } + } + + public override async rollback(): Promise { + return; + } +} diff --git a/Worker/DataMigrations/Index.ts b/Worker/DataMigrations/Index.ts index 8663087de6..7be157ebf2 100644 --- a/Worker/DataMigrations/Index.ts +++ b/Worker/DataMigrations/Index.ts @@ -38,6 +38,7 @@ import AddDefaultCopilotActionTypes from "./AddDefaultCopilotActionTypes"; import AddDefaultAlertSeverityAndStateToExistingProjects from "./AddDefaultAlertSeverityAndStateToExistingProjects"; import RefreshDefaultUserNotificationSetting from "./RefreshUserNotificationSetting"; import AddServiceTypeColumnToMetricsTable from "./AddServiceTypeColumnToMetricTable"; +import AddIsSubscriptionConfirmedToSubscribers from "./AddIsSubscriptionConfirmedToSubscribers"; // This is the order in which the migrations will be run. Add new migrations to the end of the array. @@ -81,6 +82,7 @@ const DataMigrations: Array = [ new AddDefaultAlertSeverityAndStateToExistingProjects(), new RefreshDefaultUserNotificationSetting(), new AddServiceTypeColumnToMetricsTable(), + new AddIsSubscriptionConfirmedToSubscribers(), ]; export default DataMigrations;