mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Add confirmation functionality for status page subscriptions and update related templates
This commit is contained in:
@@ -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}}
|
||||
@@ -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}}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1734435866602 implements MigrationInterface {
|
||||
public name = 'MigrationName1734435866602'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.query(`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "subscriptionConfirmationToken"`);
|
||||
await queryRunner.query(`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "isSubscriptionConfirmed"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -454,8 +454,8 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
}
|
||||
|
||||
public async getStatusPageURL(statusPageId: ObjectID): Promise<string> {
|
||||
const domains: Array<StatusPageDomain> =
|
||||
await StatusPageDomainService.findBy({
|
||||
const domain: StatusPageDomain | null =
|
||||
await StatusPageDomainService.findOneBy({
|
||||
query: {
|
||||
statusPageId: statusPageId,
|
||||
isSslProvisioned: true,
|
||||
@@ -463,21 +463,15 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
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();
|
||||
|
||||
@@ -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<Model> {
|
||||
public constructor() {
|
||||
@@ -160,6 +161,17 @@ export class Service extends DatabaseService<Model> {
|
||||
|
||||
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<Model> {
|
||||
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<Model> {
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
|
||||
|
||||
// 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<Model> {
|
||||
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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
119
StatusPage/src/Pages/Subscribe/ConfirmSubscription.tsx
Normal file
119
StatusPage/src/Pages/Subscribe/ConfirmSubscription.tsx
Normal file
@@ -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<SubscribePageProps> = (
|
||||
_props: SubscribePageProps,
|
||||
): ReactElement => {
|
||||
|
||||
const id: ObjectID = LocalStorage.getItem("statusPageId") as ObjectID;
|
||||
|
||||
|
||||
const [isLaoding, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const confirmSubscription: PromiseVoidFunction =
|
||||
async (): Promise<void> => {
|
||||
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<JSONObject> | 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 (
|
||||
<Page
|
||||
title={"Confirm Subscription"}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: "Overview",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
StatusPageUtil.isPreviewPage()
|
||||
? (RouteMap[PageMap.PREVIEW_OVERVIEW] as Route)
|
||||
: (RouteMap[PageMap.OVERVIEW] as Route),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Confirm Subscription",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
StatusPageUtil.isPreviewPage()
|
||||
? (RouteMap[PageMap.PREVIEW_CONFIRM_SUBSCRIPTION] as Route)
|
||||
: (RouteMap[PageMap.CONFIRM_SUBSCRIPTION] as Route),
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{isLaoding ? <PageLoader isVisible={isLaoding} /> : <></>}
|
||||
|
||||
{error ? <ErrorMessage error={error} /> : <></>}
|
||||
|
||||
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubscribePage;
|
||||
@@ -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",
|
||||
|
||||
@@ -18,6 +18,7 @@ const RouteMap: Dictionary<Route> = {
|
||||
[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<Route> = {
|
||||
[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 {
|
||||
|
||||
@@ -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<void> {
|
||||
// get all the users with email isVerified true.
|
||||
|
||||
const subscribers: Array<StatusPageSubscriber> = 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<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -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<DataMigrationBase> = [
|
||||
new AddDefaultAlertSeverityAndStateToExistingProjects(),
|
||||
new RefreshDefaultUserNotificationSetting(),
|
||||
new AddServiceTypeColumnToMetricsTable(),
|
||||
new AddIsSubscriptionConfirmedToSubscribers(),
|
||||
];
|
||||
|
||||
export default DataMigrations;
|
||||
|
||||
Reference in New Issue
Block a user