diff --git a/Common/Types/Email/EmailTemplateType.ts b/Common/Types/Email/EmailTemplateType.ts index 1ac46c7de1..1bfafc014a 100644 --- a/Common/Types/Email/EmailTemplateType.ts +++ b/Common/Types/Email/EmailTemplateType.ts @@ -5,6 +5,7 @@ enum EmailTemplateType { PasswordChanged = 'PasswordChanged.hbs', InviteMember = 'InviteMember.hbs', EmailChanged = 'EmailChanged.hbs', + SubscribedToStatusPage = 'SubscribedToStatusPage.hbs', } export default EmailTemplateType; diff --git a/CommonServer/API/StatusPageAPI.ts b/CommonServer/API/StatusPageAPI.ts index cbd0944430..7703dbfcc6 100644 --- a/CommonServer/API/StatusPageAPI.ts +++ b/CommonServer/API/StatusPageAPI.ts @@ -122,7 +122,6 @@ export default class StatusPageAPI extends BaseAPI< - this.router.post( `/${new this.entityType() .getCrudApiPath() @@ -453,7 +452,7 @@ export default class StatusPageAPI extends BaseAPI< if (monitorsOnStatusPage.length > 0) { activeIncidents = await IncidentService.findBy({ query: { - monitors: QueryHelper.in(monitorsOnStatusPage), + monitors: monitorsOnStatusPage as any, currentIncidentState: { isResolvedState: false, } as any, @@ -978,14 +977,14 @@ export default class StatusPageAPI extends BaseAPI< let query: Query = { startsAt: QueryHelper.inBetween(last14Days, today), - statusPages: QueryHelper.in([statusPageId]), + statusPages: [statusPageId] as any, projectId: statusPage.projectId!, }; if (scheduledMaintenanceId) { query = { _id: scheduledMaintenanceId.toString(), - statusPages: QueryHelper.in([statusPageId]), + statusPages: [statusPageId] as any, projectId: statusPage.projectId!, }; } @@ -1144,14 +1143,14 @@ export default class StatusPageAPI extends BaseAPI< const last14Days: Date = OneUptimeDate.getSomeDaysAgo(14); let query: Query = { - statusPages: QueryHelper.in([statusPageId]), + statusPages: [statusPageId] as any, showAnnouncementAt: QueryHelper.inBetween(last14Days, today), projectId: statusPage.projectId!, }; if (announcementId) { query = { - statusPages: QueryHelper.in([statusPageId]), + statusPages: [statusPageId] as any, _id: announcementId.toString(), projectId: statusPage.projectId!, }; @@ -1282,14 +1281,14 @@ export default class StatusPageAPI extends BaseAPI< const last14Days: Date = OneUptimeDate.getSomeDaysAgo(14); let incidentQuery: Query = { - monitors: QueryHelper.in(monitorsOnStatusPage), + monitors: monitorsOnStatusPage as any, projectId: statusPage.projectId!, createdAt: QueryHelper.inBetween(last14Days, today), }; if (incidentId) { incidentQuery = { - monitors: QueryHelper.in(monitorsOnStatusPage), + monitors: monitorsOnStatusPage as any, projectId: statusPage.projectId!, _id: incidentId.toString(), }; diff --git a/CommonServer/Services/StatusPageSubscriberService.ts b/CommonServer/Services/StatusPageSubscriberService.ts index bdc310d52f..ce7af11f12 100644 --- a/CommonServer/Services/StatusPageSubscriberService.ts +++ b/CommonServer/Services/StatusPageSubscriberService.ts @@ -1,10 +1,109 @@ import PostgresDatabase from '../Infrastructure/PostgresDatabase'; import Model from 'Model/Models/StatusPageSubscriber'; -import DatabaseService from './DatabaseService'; +import DatabaseService, { OnCreate } from './DatabaseService'; +import CreateBy from '../Types/Database/CreateBy'; +import BadDataException from 'Common/Types/Exception/BadDataException'; +import StatusPageService from './StatusPageService'; +import MailService from './MailService'; +import EmailTemplateType from 'Common/Types/Email/EmailTemplateType'; +import StatusPageDomainService from './StatusPageDomainService'; +import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax'; +import URL from 'Common/Types/API/URL'; +import { Domain, HttpProtocol } from '../Config'; +import logger from '../Utils/Logger'; export class Service extends DatabaseService { public constructor(postgresDatabase?: PostgresDatabase) { super(Model, postgresDatabase); } + + protected override async onBeforeCreate( + data: CreateBy + ): Promise> { + + if (!data.data.statusPageId) { + throw new BadDataException("Status Page ID is required.") + } + + const statuspage = await StatusPageService.findOneById({ + id: data.data.statusPageId, + select: { + projectId: true, + pageTitle: true, + name: true, + isPublicStatusPage: true + }, + props: { + isRoot: true, + ignoreHooks: true, + } + }); + + if (!statuspage || !statuspage.projectId) { + throw new BadDataException("Status Page not found"); + } + + data.data.projectId = statuspage.projectId; + + return { createBy: data, carryForward: statuspage }; + + } + + + protected override async onCreateSuccess( + onCreate: OnCreate, + createdItem: Model + ): Promise { + + if (createdItem.statusPageId && 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 sttaus page preview link. + + const domains = await StatusPageDomainService.findBy({ + query: { + statusPageId: createdItem.statusPageId, + isSslProvisioned: true + }, + select: { + fullDomain: true + }, + skip: 0, + limit: LIMIT_PER_PROJECT, + props: { + isRoot: true, + ignoreHooks: true, + } + }); + + + let statusPageURL: string = domains.map((d) => d.fullDomain).join(", "); + + if (domains.length === 0) { + // 'https://local.oneuptime.com/status-page/40092fb5-cc33-4995-b532-b4e49c441c98' + statusPageURL = new URL(HttpProtocol, Domain).addRoute("/status-page/" + createdItem.statusPageId.toString()).toString(); + } + + const statusPageName: string = onCreate.carryForward.pageTitle || onCreate.carryForward.name || 'Status Page'; + + MailService.sendMail({ + toEmail: createdItem.subscriberEmail, + templateType: EmailTemplateType.SubscribedToStatusPage, + vars: { + statusPageName: statusPageName, + statusPageUrl: statusPageURL, + isPublicStatusPage: onCreate.carryForward.isPublicStatusPage, + unsubscribeUrl: new URL(HttpProtocol, Domain).addRoute("/status-page-subscriber/unsubscribe/" + createdItem._id.toString()).toString() + }, + subject: 'You have been subscribed to ' + statusPageName, + }).catch((err: Error) => { + logger.error(err); + }); + } + + return createdItem; + } } export default new Service(); diff --git a/CommonUI/.env.tpl b/CommonUI/.env.tpl index 5e039b0aad..b0d3fe7e45 100644 --- a/CommonUI/.env.tpl +++ b/CommonUI/.env.tpl @@ -14,4 +14,4 @@ STATUS_PAGE_ROUTE={{ .Env.STATUS_PAGE_ROUTE }} IS_SERVER=false STATUS_PAGE_CNAME_RECORD={{ .Env.STATUS_PAGE_CNAME_RECORD }} DOMAIN={{ .Env.DOMAIN }} - +HTTP_PROTOCOL={{ .Env.HTTP_PROTOCOL }} diff --git a/CommonUI/src/Components/Loader/PageLoader.tsx b/CommonUI/src/Components/Loader/PageLoader.tsx index 0eb8c8e38d..d203ae9497 100644 --- a/CommonUI/src/Components/Loader/PageLoader.tsx +++ b/CommonUI/src/Components/Loader/PageLoader.tsx @@ -11,7 +11,7 @@ const PageLoader: FunctionComponent = ( ): ReactElement => { if (props.isVisible) { return ( -
+
); diff --git a/CommonUI/src/Config.ts b/CommonUI/src/Config.ts index 6f24ab79ef..21da9cef67 100644 --- a/CommonUI/src/Config.ts +++ b/CommonUI/src/Config.ts @@ -9,11 +9,7 @@ export const env: Function = (key: string): string => { return process.env[key] || ''; }; -export const HTTP_PROTOCOL: Protocol = window.location.protocol.includes( - 'https' -) - ? Protocol.HTTPS - : Protocol.HTTP; +export const HTTP_PROTOCOL: Protocol = env('HTTP_PROTOCOL') === "http" ? Protocol.HTTP : Protocol.HTTPS; export const DOMAIN: string = env('DOMAIN') || ''; diff --git a/Dashboard/src/Components/Header/ProjectPicker.tsx b/Dashboard/src/Components/Header/ProjectPicker.tsx index e33145f26b..a3ef42a22c 100644 --- a/Dashboard/src/Components/Header/ProjectPicker.tsx +++ b/Dashboard/src/Components/Header/ProjectPicker.tsx @@ -31,6 +31,29 @@ const DashboardProjectPicker: FunctionComponent = ( null ); + const getFooter = (): ReactElement => { + + if (!BILLING_ENABLED) { + return <>; + } + + return (
{ + setIsSubscriptionPlanYearly( + !isSubsriptionPlanYearly + ); + refreshFields(); + }} + > + {isSubsriptionPlanYearly ? ( + Switch to monthly pricing? + ) : ( + Switch to yearly pricing? + )} +
); + } + const [isSubsriptionPlanYearly, setIsSubscriptionPlanYearly] = useState(true); @@ -216,21 +239,7 @@ const DashboardProjectPicker: FunctionComponent = ( formType: FormType.Create, }} footer={ -
{ - setIsSubscriptionPlanYearly( - !isSubsriptionPlanYearly - ); - refreshFields(); - }} - > - {isSubsriptionPlanYearly ? ( - Switch to monthly pricing? - ) : ( - Switch to yearly pricing? - )} -
+ getFooter() } /> ) : ( diff --git a/Dashboard/src/Pages/StatusPages/View/EmailSubscribers.tsx b/Dashboard/src/Pages/StatusPages/View/EmailSubscribers.tsx index 2aa4687fd0..7cf4d179f7 100644 --- a/Dashboard/src/Pages/StatusPages/View/EmailSubscribers.tsx +++ b/Dashboard/src/Pages/StatusPages/View/EmailSubscribers.tsx @@ -103,7 +103,7 @@ const StatusPageDelete: FunctionComponent = ( }, title: 'Email', description: - 'An email will be sent to this email for status page updates.', + 'Status page updates will be sent to this email.', fieldType: FormFieldSchemaType.Email, required: true, placeholder: 'subscriber@company.com', diff --git a/Dashboard/src/Pages/StatusPages/View/SideMenu.tsx b/Dashboard/src/Pages/StatusPages/View/SideMenu.tsx index a072685f28..01b7e5fa14 100644 --- a/Dashboard/src/Pages/StatusPages/View/SideMenu.tsx +++ b/Dashboard/src/Pages/StatusPages/View/SideMenu.tsx @@ -81,7 +81,7 @@ const DashboardSideMenu: FunctionComponent = ( }} icon={IconProp.Email} /> - = ( ), }} icon={IconProp.Webhook} - /> + /> */} + + + + + + You have changed your email. + + + + + + + + + + + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + +
+
+
+ +

You have been subscribed to {{statusPageName}}

+ +
+
+
+
+
+ + + + + + + + + + + + +
+
+
+ + You have been subscribed to {{statusPageName}}. You will be the first to hear from us when there are any incidents, announcements or scheduled maintenance events. + + + {{#if isPublicStatusPage}} + You can also view the status page by visiting these link: {{statusPageUrl}} + {{/if}} + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+ + You can unsubscribe at any time by viisting this link: {{unsubscribeUrl}} + +
+
+
+
+ + + + + + + + + + + + + +
+
+
+ + + + + + + + +
+ + Verify Email + +
+ + +
+
+
+
+
+ + + + + + + + + + + + +
+
+
+ + + Thanks, have a great day. + + + +
+
+
+
+ + + + + + + + + + + +
+
+
+ + + OneUptime Team. + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
 
+
+ + + + \ No newline at end of file diff --git a/Mail/package-lock.json b/Mail/package-lock.json index a6ff45f3ea..e953882dfd 100644 --- a/Mail/package-lock.json +++ b/Mail/package-lock.json @@ -35,6 +35,7 @@ "moment": "^2.29.2", "nanoid": "^3.3.2", "nanoid-dictionary": "^4.3.0", + "posthog-js": "^1.37.0", "process": "^0.11.10", "reflect-metadata": "^0.1.13", "slugify": "^1.6.5", @@ -43,7 +44,7 @@ }, "devDependencies": { "@faker-js/faker": "^6.3.1", - "@types/jest": "^27.4.1", + "@types/jest": "^27.5.2", "@types/node": "^17.0.22", "jest": "^27.5.1", "ts-jest": "^27.1.4" @@ -4226,8 +4227,9 @@ "pg": "^8.7.3", "redis": "^4.2.0", "socket.io": "^4.4.1", - "typeorm": "^0.3.6", - "typeorm-extension": "^2.1.0", + "stripe": "^10.17.0", + "typeorm": "^0.3.10", + "typeorm-extension": "^2.2.13", "winston": "^3.6.0" }, "devDependencies": { @@ -17480,7 +17482,7 @@ "requires": { "@faker-js/faker": "^6.3.1", "@types/crypto-js": "^4.1.1", - "@types/jest": "^27.4.1", + "@types/jest": "^27.5.2", "@types/nanoid-dictionary": "^4.2.0", "@types/node": "^17.0.22", "@types/uuid": "^8.3.4", @@ -17490,6 +17492,7 @@ "moment": "^2.29.2", "nanoid": "^3.3.2", "nanoid-dictionary": "^4.3.0", + "posthog-js": "^1.37.0", "process": "^0.11.10", "reflect-metadata": "^0.1.13", "slugify": "^1.6.5", @@ -20132,9 +20135,10 @@ "pg": "^8.7.3", "redis": "^4.2.0", "socket.io": "^4.4.1", + "stripe": "^10.17.0", "ts-jest": "^27.1.4", - "typeorm": "^0.3.6", - "typeorm-extension": "^2.1.0", + "typeorm": "^0.3.10", + "typeorm-extension": "^2.2.13", "winston": "^3.6.0" }, "dependencies": { @@ -22814,7 +22818,7 @@ "requires": { "@faker-js/faker": "^6.3.1", "@types/crypto-js": "^4.1.1", - "@types/jest": "^27.4.1", + "@types/jest": "^27.5.2", "@types/nanoid-dictionary": "^4.2.0", "@types/node": "^17.0.22", "@types/uuid": "^8.3.4", @@ -22824,6 +22828,7 @@ "moment": "^2.29.2", "nanoid": "^3.3.2", "nanoid-dictionary": "^4.3.0", + "posthog-js": "^1.37.0", "process": "^0.11.10", "reflect-metadata": "^0.1.13", "slugify": "^1.6.5", @@ -27965,7 +27970,7 @@ "requires": { "@faker-js/faker": "^6.3.1", "@types/crypto-js": "^4.1.1", - "@types/jest": "^27.4.1", + "@types/jest": "^27.5.2", "@types/nanoid-dictionary": "^4.2.0", "@types/node": "^17.0.22", "@types/uuid": "^8.3.4", @@ -27975,6 +27980,7 @@ "moment": "^2.29.2", "nanoid": "^3.3.2", "nanoid-dictionary": "^4.3.0", + "posthog-js": "^1.37.0", "process": "^0.11.10", "reflect-metadata": "^0.1.13", "slugify": "^1.6.5", @@ -34509,7 +34515,7 @@ "requires": { "@faker-js/faker": "^6.3.1", "@types/crypto-js": "^4.1.1", - "@types/jest": "^27.4.1", + "@types/jest": "^27.5.2", "@types/nanoid-dictionary": "^4.2.0", "@types/node": "^17.0.22", "@types/uuid": "^8.3.4", @@ -34519,6 +34525,7 @@ "moment": "^2.29.2", "nanoid": "^3.3.2", "nanoid-dictionary": "^4.3.0", + "posthog-js": "^1.37.0", "process": "^0.11.10", "reflect-metadata": "^0.1.13", "slugify": "^1.6.5", diff --git a/Model/Models/StatusPageSubscriber.ts b/Model/Models/StatusPageSubscriber.ts index 6ee0d1dd38..452437ddf3 100644 --- a/Model/Models/StatusPageSubscriber.ts +++ b/Model/Models/StatusPageSubscriber.ts @@ -24,7 +24,7 @@ import CanAccessIfCanReadOn from 'Common/Types/Database/CanAccessIfCanReadOn'; @CanAccessIfCanReadOn('statusPage') @TenantColumn('projectId') @TableAccessControl({ - create: [Permission.ProjectOwner, Permission.CanCreateStatusPageSubscriber], + create: [Permission.ProjectOwner, Permission.CanCreateStatusPageSubscriber, Permission.Public], read: [Permission.ProjectOwner, Permission.CanReadStatusPageSubscriber], delete: [Permission.ProjectOwner, Permission.CanDeleteStatusPageSubscriber], update: [Permission.ProjectOwner, Permission.CanEditStatusPageSubscriber], @@ -132,6 +132,7 @@ export default class StatusPageSubscriber extends BaseModel { create: [ Permission.ProjectOwner, Permission.CanCreateStatusPageSubscriber, + Permission.Public ], read: [Permission.ProjectOwner, Permission.CanReadStatusPageSubscriber], update: [ @@ -152,6 +153,7 @@ export default class StatusPageSubscriber extends BaseModel { create: [ Permission.ProjectOwner, Permission.CanCreateStatusPageSubscriber, + Permission.Public ], read: [Permission.ProjectOwner, Permission.CanReadStatusPageSubscriber], update: [ @@ -172,6 +174,7 @@ export default class StatusPageSubscriber extends BaseModel { create: [ Permission.ProjectOwner, Permission.CanCreateStatusPageSubscriber, + Permission.Public ], read: [Permission.ProjectOwner, Permission.CanReadStatusPageSubscriber], update: [ diff --git a/StatusPage/src/Pages/Subscribe/Subscribe.tsx b/StatusPage/src/Pages/Subscribe/Subscribe.tsx index b2e9631076..6311beea0a 100644 --- a/StatusPage/src/Pages/Subscribe/Subscribe.tsx +++ b/StatusPage/src/Pages/Subscribe/Subscribe.tsx @@ -1,7 +1,6 @@ import React, { FunctionComponent, ReactElement, useState } from 'react'; import PageComponentProps from '../PageComponentProps'; import Page from '../../Components/Page/Page'; -import Tabs from 'CommonUI/src/Components/Tabs/Tabs'; import ModelForm, { FormType } from 'CommonUI/src/Components/Forms/ModelForm'; import StatusPageSubscriber from 'Model/Models/StatusPageSubscriber'; import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; @@ -13,62 +12,68 @@ import BadDataException from 'Common/Types/Exception/BadDataException'; const PageNotFound: FunctionComponent = ( _props: PageComponentProps ): ReactElement => { - const [currentTab, setCurrentTab] = useState('Email'); + const [currentTab, _setCurrentTab] = useState('Email'); return ( -

Subscribe.

- +
+

Subscribe.

+ + {/* { setCurrentTab(tab); }} - /> + /> */} - {currentTab === 'Email' ? ( - - modelType={StatusPageSubscriber} - id="email-form" - on - fields={[ - { - field: { - subscriberEmail: true, - }, - title: 'Email', - description: - 'An email will be sent to this email for status page updates.', - fieldType: FormFieldSchemaType.Email, - required: true, - placeholder: 'subscriber@company.com', - }, - ]} - formType={FormType.Create} - submitButtonText={'Subscribe'} - onBeforeCreate={async (item: StatusPageSubscriber) => { - const id: ObjectID = LocalStorage.getItem( - 'statusPageId' - ) as ObjectID; - if (!id) { - throw new BadDataException( - 'Status Page ID is required' - ); - } - item.statusPageId = id; - return item; - }} - onSuccess={(_value: JSONObject) => { - //LoginUtil.login(value); - }} - maxPrimaryButtonWidth={true} - /> - ) : ( - <> - )} - {currentTab === 'SMS' ? ( + {currentTab === 'Email' ? ( + + modelType={StatusPageSubscriber} + id="email-form" + fields={[ + { + field: { + subscriberEmail: true, + }, + title: 'Email', + description: + 'Status page updates will be sent to this email.', + fieldType: FormFieldSchemaType.Email, + required: true, + placeholder: 'subscriber@company.com', + }, + ]} + formType={FormType.Create} + submitButtonText={'Subscribe'} + onBeforeCreate={async (item: StatusPageSubscriber) => { + const id: ObjectID = LocalStorage.getItem( + 'statusPageId' + ) as ObjectID; + if (!id) { + throw new BadDataException( + 'Status Page ID is required' + ); + } + + item.statusPageId = id; + return item; + }} + onSuccess={(_value: JSONObject) => { + //LoginUtil.login(value); + }} + maxPrimaryButtonWidth={true} + /> + ) : ( + <> + )} + + {/* {currentTab === 'SMS' ? ( modelType={StatusPageSubscriber} id="sms-form" @@ -148,7 +153,9 @@ const PageNotFound: FunctionComponent = ( /> ) : ( <> - )} + )} */} +
+
); };