diff --git a/Common/Models/DatabaseModels/StatusPageSubscriber.ts b/Common/Models/DatabaseModels/StatusPageSubscriber.ts index c69fce7a0c..4297d03956 100644 --- a/Common/Models/DatabaseModels/StatusPageSubscriber.ts +++ b/Common/Models/DatabaseModels/StatusPageSubscriber.ts @@ -434,7 +434,6 @@ export default class StatusPageSubscriber extends BaseModel { }) public deletedByUserId?: ObjectID = undefined; - @ColumnAccessControl({ create: [ Permission.ProjectOwner, @@ -460,7 +459,8 @@ export default class StatusPageSubscriber extends BaseModel { 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)", + description: + "Has subscriber confirmed their subscription? (for example, by clicking on a confirmation link in an email)", }) @Column({ type: ColumnType.Boolean, @@ -468,24 +468,21 @@ export default class StatusPageSubscriber extends BaseModel { }) public isSubscriptionConfirmed?: boolean = undefined; - @ColumnAccessControl({ - create: [ - ], - read: [ - ], - update: [ - ], + 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.", + 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 + nullable: true, }) public subscriptionConfirmationToken?: string = undefined; diff --git a/Common/Server/API/StatusPageAPI.ts b/Common/Server/API/StatusPageAPI.ts index f3c3275c8e..aa7aebcc44 100644 --- a/Common/Server/API/StatusPageAPI.ts +++ b/Common/Server/API/StatusPageAPI.ts @@ -82,7 +82,6 @@ export default class StatusPageAPI extends BaseAPI< public constructor() { super(StatusPage, StatusPageService); - // confirm subscription api this.router.get( `${new this.entityType() @@ -102,7 +101,7 @@ export default class StatusPageAPI extends BaseAPI< subscriptionConfirmationToken: token, }, select: { - isSubscriptionConfirmed: true, + isSubscriptionConfirmed: true, }, props: { isRoot: true, @@ -113,11 +112,13 @@ export default class StatusPageAPI extends BaseAPI< return Response.sendErrorResponse( req, res, - new NotFoundException("Subscriber not found or confirmation token is invalid"), + new NotFoundException( + "Subscriber not found or confirmation token is invalid", + ), ); } - // check if subscription confirmed already. + // check if subscription confirmed already. if (subscriber.isSubscriptionConfirmed) { return Response.sendEmptySuccessResponse(req, res); diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1734435866602-MigrationName.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1734435866602-MigrationName.ts index fdff8f4ee1..92993364d0 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1734435866602-MigrationName.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1734435866602-MigrationName.ts @@ -1,16 +1,23 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class MigrationName1734435866602 implements MigrationInterface { - public name = 'MigrationName1734435866602' + 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"`); - } + 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 b8284d0b7d..b8f68b0c6c 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts @@ -171,5 +171,5 @@ export default [ MigrationName1731435267537, MigrationName1731435514287, MigrationName1732553444010, - MigrationName1734435866602 + MigrationName1734435866602, ]; diff --git a/Common/Server/Services/StatusPageSubscriberService.ts b/Common/Server/Services/StatusPageSubscriberService.ts index d3353ffcfe..9fda5de82a 100644 --- a/Common/Server/Services/StatusPageSubscriberService.ts +++ b/Common/Server/Services/StatusPageSubscriberService.ts @@ -161,16 +161,21 @@ export class Service extends DatabaseService { data.data.projectId = statuspage.projectId; - const isEmailSubscriber: boolean = !!data.data.subscriberEmail; - const isSubscriptionConfirmed: boolean = !!data.data.isSubscriptionConfirmed; + const isEmailSubscriber: boolean = Boolean(data.data.subscriberEmail); + const isSubscriptionConfirmed: boolean = Boolean( + data.data.isSubscriptionConfirmed, + ); if (isEmailSubscriber && !isSubscriptionConfirmed) { data.data.isSubscriptionConfirmed = false; - }else{ + } 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(); + data.data.subscriptionConfirmationToken = NumberUtil.getRandomNumber( + 100000, + 999999, + ).toString(); return { createBy: data, carryForward: statuspage }; } @@ -192,14 +197,11 @@ export class Service extends DatabaseService { onCreate.carryForward.name || "Status Page"; - const unsubscribeLink: string = this.getUnsubscribeLink( URL.fromString(statusPageURL), createdItem.id!, ).toString(); - - if ( createdItem.statusPageId && createdItem.subscriberPhone && @@ -250,23 +252,22 @@ export class Service extends DatabaseService { createdItem.subscriberEmail && 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; + const isSubcriptionConfirmed: boolean = Boolean( + createdItem.isSubscriptionConfirmed, + ); if (!isSubcriptionConfirmed) { - await this.sendConfirmSubscriptionEmail({ subscriberId: createdItem.id!, }); } if (isSubcriptionConfirmed && createdItem.sendYouHaveSubscribedMessage) { - await this.sendYouHaveSubscribedEmail({ subscriberId: createdItem.id!, }); @@ -276,7 +277,6 @@ export class Service extends DatabaseService { return createdItem; } - public async sendConfirmSubscriptionEmail(data: { subscriberId: ObjectID; }): Promise { @@ -338,20 +338,19 @@ export class Service extends DatabaseService { statusPage.id, ); - const statusPageName: string = statusPage.pageTitle || statusPage.name || "Status Page"; + 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(); + const confirmSubscriptionLink: string = this.getConfirmSubscriptionLink({ + statusPageUrl: statusPageURL, + confirmationToken: subscriber.subscriptionConfirmationToken || "", + statusPageSubscriberId: subscriber.id!, + statusPageId: subscriber.statusPageId, + }).toString(); if ( subscriber.statusPageId && @@ -365,11 +364,10 @@ export class Service extends DatabaseService { vars: { statusPageName: statusPageName, logoUrl: statusPage.logoFileId - ? new URL(httpProtocol - , host) - .addRoute(FileRoute) - .addRoute("/image/" + statusPage.logoFileId) - .toString() + ? new URL(httpProtocol, host) + .addRoute(FileRoute) + .addRoute("/image/" + statusPage.logoFileId) + .toString() : "", statusPageUrl: statusPageURL, isPublicStatusPage: statusPage.isPublicStatusPage @@ -387,17 +385,13 @@ export class Service extends DatabaseService { }, ).catch((err: Error) => { logger.error(err); - } - ); + }); } } - public async sendYouHaveSubscribedEmail(data: { subscriberId: ObjectID; }): Promise { - - // get subscriber const subscriber: Model | null = await this.findOneBy({ query: { @@ -421,7 +415,7 @@ export class Service extends DatabaseService { return; } - const statusPage: StatusPage | null = await StatusPageService.findOneBy({ + const statusPage: StatusPage | null = await StatusPageService.findOneBy({ query: { _id: subscriber.statusPageId.toString(), }, @@ -447,16 +441,16 @@ export class Service extends DatabaseService { }, }); - if(!statusPage || !statusPage.id) { + if (!statusPage || !statusPage.id) { return; } - const statusPageURL: string = await StatusPageService.getStatusPageURL( statusPage.id, ); - const statusPageName: string = statusPage.pageTitle ||statusPage.name || "Status Page"; + const statusPageName: string = + statusPage.pageTitle || statusPage.name || "Status Page"; const host: Hostname = await DatabaseConfig.getHost(); @@ -480,9 +474,9 @@ export class Service extends DatabaseService { statusPageName: statusPageName, logoUrl: statusPage.logoFileId ? new URL(httpProtocol, host) - .addRoute(FileRoute) - .addRoute("/image/" + statusPage.logoFileId) - .toString() + .addRoute(FileRoute) + .addRoute("/image/" + statusPage.logoFileId) + .toString() : "", statusPageUrl: statusPageURL, isPublicStatusPage: statusPage.isPublicStatusPage @@ -504,12 +498,11 @@ export class Service extends DatabaseService { } } - public getConfirmSubscriptionLink(data: { statusPageUrl: string; confirmationToken: string; - statusPageSubscriberId: ObjectID - statusPageId: ObjectID + statusPageSubscriberId: ObjectID; + statusPageId: ObjectID; }): URL { return URL.fromString(data.statusPageUrl).addRoute( `/confirm-subscription/${data.statusPageId.toString()}/${data.statusPageSubscriberId.toString()}?token=${data.confirmationToken}`, diff --git a/Common/UI/Components/Pill/Pill.tsx b/Common/UI/Components/Pill/Pill.tsx index dc706d6fb6..f601cff714 100644 --- a/Common/UI/Components/Pill/Pill.tsx +++ b/Common/UI/Components/Pill/Pill.tsx @@ -2,6 +2,7 @@ import { Black } from "Common/Types/BrandColors"; import Color from "Common/Types/Color"; import React, { CSSProperties, FunctionComponent, ReactElement } from "react"; import Tooltip from "../Tooltip/Tooltip"; +import { GetReactElementFunction } from "../../Types/FunctionTypes"; export enum PillSize { Small = "10px", @@ -42,7 +43,7 @@ const Pill: FunctionComponent = ( ); } - const getPillElement = (): ReactElement => { + const getPillElement: GetReactElementFunction = (): ReactElement => { return ( = ( }; if (props.tooltip) { - return ( - - {getPillElement()} - - ); - }; + return {getPillElement()}; + } return getPillElement(); -} +}; export default Pill; diff --git a/Dashboard/src/Pages/StatusPages/View/EmailSubscribers.tsx b/Dashboard/src/Pages/StatusPages/View/EmailSubscribers.tsx index 3b556e188b..0eac96c6f9 100644 --- a/Dashboard/src/Pages/StatusPages/View/EmailSubscribers.tsx +++ b/Dashboard/src/Pages/StatusPages/View/EmailSubscribers.tsx @@ -133,7 +133,8 @@ const StatusPageDelete: FunctionComponent = ( isSubscriptionConfirmed: true, }, title: "Send Confirmation Email", - description: "Send a confirmation email to this subscriber with a link to confirm subscription.", + description: + "Send a confirmation email to this subscriber with a link to confirm subscription.", fieldType: FormFieldSchemaType.Toggle, required: false, doNotShowWhenEditing: true, @@ -313,10 +314,16 @@ const StatusPageDelete: FunctionComponent = ( return ; } - if(!item["isSubscriptionConfirmed"]) { - return ; + if (!item["isSubscriptionConfirmed"]) { + return ( + + ); } - + return ; }, }, diff --git a/StatusPage/src/App.tsx b/StatusPage/src/App.tsx index 7f34f18a00..81848e6c19 100644 --- a/StatusPage/src/App.tsx +++ b/StatusPage/src/App.tsx @@ -34,6 +34,7 @@ import { useNavigate, useParams, } from "react-router-dom"; +import ConfirmSubscription from "./Pages/Subscribe/ConfirmSubscription"; const App: () => JSX.Element = () => { Navigation.setNavigateHook(useNavigate()); @@ -378,6 +379,18 @@ const App: () => JSX.Element = () => { } /> + { + onPageLoadComplete(); + }} + /> + } + /> + {/* Preview */} JSX.Element = () => { } /> + { + onPageLoadComplete(); + }} + pageRoute={ + RouteMap[PageMap.PREVIEW_CONFIRM_SUBSCRIPTION] as Route + } + /> + } + /> + {/* 👇️ only match this when no other routes match */} = ( - _props: SubscribePageProps, +const SubscribePage: FunctionComponent = ( + _props: PageComponentProps, ): 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"); + 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)); } - StatusPageUtil.checkIfUserHasLoggedIn(); - - - return ( - - {isLaoding ? : <>} - - {error ? : <>} - - - - ); + 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/Worker/DataMigrations/AddIsSubscriptionConfirmedToSubscribers.ts b/Worker/DataMigrations/AddIsSubscriptionConfirmedToSubscribers.ts index e01f267f28..3c1192ee86 100644 --- a/Worker/DataMigrations/AddIsSubscriptionConfirmedToSubscribers.ts +++ b/Worker/DataMigrations/AddIsSubscriptionConfirmedToSubscribers.ts @@ -5,41 +5,45 @@ import StatusPageSubscriber from "Common/Models/DatabaseModels/StatusPageSubscri import StatusPageSubscriberService from "Common/Server/Services/StatusPageSubscriberService"; export default class AddIsSubscriptionConfirmedToSubscribers extends DataMigrationBase { - public constructor() { - super("AddIsSubscriptionConfirmedToSubscribers"); - } + public constructor() { + super("AddIsSubscriptionConfirmedToSubscribers"); + } - public override async migrate(): Promise { - // get all the users with email isVerified true. + 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, - }, - }); + 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, - } - }); - } + 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; - } + public override async rollback(): Promise { + return; + } }