@oneuptime/common 7.0.4066 → 7.0.4080
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/Server/API/StatusPageAPI.ts +426 -0
- package/Server/EnvironmentConfig.ts +7 -0
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +1 -1
- package/Server/Utils/StartServer.ts +34 -5
- package/Types/Email/EmailTemplateType.ts +1 -0
- package/UI/Components/Button/DropdownButton.tsx +93 -0
- package/UI/Components/Forms/BasicForm.tsx +14 -2
- package/UI/Components/Forms/BasicModelForm.tsx +8 -1
- package/UI/Components/Forms/Fields/FormField.tsx +32 -18
- package/UI/Components/Forms/ModelForm.tsx +13 -3
- package/UI/Components/Forms/Types/Field.ts +7 -1
- package/UI/Images/favicon/status-green.png +0 -0
- package/build/dist/Server/API/StatusPageAPI.js +320 -36
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +2 -1
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +1 -1
- package/build/dist/Server/Utils/StartServer.js +16 -4
- package/build/dist/Server/Utils/StartServer.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/Button/DropdownButton.js +20 -0
- package/build/dist/UI/Components/Button/DropdownButton.js.map +1 -0
- package/build/dist/UI/Components/Forms/BasicForm.js +8 -2
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js +1 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +23 -18
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +2 -2
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/package.json +2 -2
- package/UI/webpack-middleware.js +0 -65
|
@@ -79,6 +79,17 @@ import UptimePrecision from "../../Types/StatusPage/UptimePrecision";
|
|
|
79
79
|
import { Green } from "../../Types/BrandColors";
|
|
80
80
|
import UptimeUtil from "../../Utils/Uptime/UptimeUtil";
|
|
81
81
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
82
|
+
import URL from "Common/Types/API/URL";
|
|
83
|
+
import SMS from "../../Types/SMS/SMS";
|
|
84
|
+
import SmsService from "../Services/SmsService";
|
|
85
|
+
import ProjectCallSMSConfigService from "../Services/ProjectCallSMSConfigService";
|
|
86
|
+
import MailService from "../Services/MailService";
|
|
87
|
+
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
|
|
88
|
+
import Hostname from "../../Types/API/Hostname";
|
|
89
|
+
import Protocol from "../../Types/API/Protocol";
|
|
90
|
+
import DatabaseConfig from "../DatabaseConfig";
|
|
91
|
+
import { FileRoute } from "../../ServiceRoute";
|
|
92
|
+
import ProjectSmtpConfigService from "../Services/ProjectSmtpConfigService";
|
|
82
93
|
|
|
83
94
|
export default class StatusPageAPI extends BaseAPI<
|
|
84
95
|
StatusPage,
|
|
@@ -87,6 +98,178 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
87
98
|
public constructor() {
|
|
88
99
|
super(StatusPage, StatusPageService);
|
|
89
100
|
|
|
101
|
+
// get title, description of the page. This is used for SEO.
|
|
102
|
+
this.router.get(
|
|
103
|
+
`${new this.entityType().getCrudApiPath()?.toString()}/seo/:statusPageIdOrDomain`,
|
|
104
|
+
UserMiddleware.getUserMiddleware,
|
|
105
|
+
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
106
|
+
const statusPageIdOrDomain: string = req.params[
|
|
107
|
+
"statusPageIdOrDomain"
|
|
108
|
+
] as string;
|
|
109
|
+
|
|
110
|
+
let statusPageId: ObjectID | null = null;
|
|
111
|
+
|
|
112
|
+
if (statusPageIdOrDomain && statusPageIdOrDomain.includes(".")) {
|
|
113
|
+
// then this is a domain and not the status page id. We need to get the status page id from the domain.
|
|
114
|
+
|
|
115
|
+
const statusPageDomain: StatusPageDomain | null =
|
|
116
|
+
await StatusPageDomainService.findOneBy({
|
|
117
|
+
query: {
|
|
118
|
+
fullDomain: statusPageIdOrDomain,
|
|
119
|
+
domain: {
|
|
120
|
+
isVerified: true,
|
|
121
|
+
} as any,
|
|
122
|
+
},
|
|
123
|
+
select: {
|
|
124
|
+
statusPageId: true,
|
|
125
|
+
},
|
|
126
|
+
props: {
|
|
127
|
+
isRoot: true,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!statusPageDomain || !statusPageDomain.statusPageId) {
|
|
132
|
+
return Response.sendErrorResponse(
|
|
133
|
+
req,
|
|
134
|
+
res,
|
|
135
|
+
new NotFoundException("Status Page not found"),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
statusPageId = statusPageDomain.statusPageId;
|
|
140
|
+
} else {
|
|
141
|
+
// then this is a status page id. We need to get the status page id from the id.
|
|
142
|
+
try {
|
|
143
|
+
statusPageId = new ObjectID(statusPageIdOrDomain);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
return Response.sendErrorResponse(
|
|
146
|
+
req,
|
|
147
|
+
res,
|
|
148
|
+
new NotFoundException("Status Page not found"),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const statusPage: StatusPage | null = await StatusPageService.findOneBy(
|
|
154
|
+
{
|
|
155
|
+
query: {
|
|
156
|
+
_id: statusPageId,
|
|
157
|
+
},
|
|
158
|
+
select: {
|
|
159
|
+
pageTitle: true,
|
|
160
|
+
pageDescription: true,
|
|
161
|
+
name: true,
|
|
162
|
+
},
|
|
163
|
+
props: {
|
|
164
|
+
isRoot: true,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (!statusPage) {
|
|
170
|
+
return Response.sendErrorResponse(
|
|
171
|
+
req,
|
|
172
|
+
res,
|
|
173
|
+
new NotFoundException("Status Page not found"),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return Response.sendJsonObjectResponse(req, res, {
|
|
178
|
+
title: statusPage.pageTitle || statusPage.name,
|
|
179
|
+
description: statusPage.pageDescription,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// favicon api.
|
|
185
|
+
this.router.get(
|
|
186
|
+
`${new this.entityType().getCrudApiPath()?.toString()}/favicon/:statusPageIdOrDomain`,
|
|
187
|
+
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
188
|
+
const statusPageIdOrDomain: string = req.params[
|
|
189
|
+
"statusPageIdOrDomain"
|
|
190
|
+
] as string;
|
|
191
|
+
|
|
192
|
+
let statusPageId: ObjectID | null = null;
|
|
193
|
+
|
|
194
|
+
if (statusPageIdOrDomain && statusPageIdOrDomain.includes(".")) {
|
|
195
|
+
// then this is a domain and not the status page id. We need to get the status page id from the domain.
|
|
196
|
+
|
|
197
|
+
const statusPageDomain: StatusPageDomain | null =
|
|
198
|
+
await StatusPageDomainService.findOneBy({
|
|
199
|
+
query: {
|
|
200
|
+
fullDomain: statusPageIdOrDomain,
|
|
201
|
+
domain: {
|
|
202
|
+
isVerified: true,
|
|
203
|
+
} as any,
|
|
204
|
+
},
|
|
205
|
+
select: {
|
|
206
|
+
statusPageId: true,
|
|
207
|
+
},
|
|
208
|
+
props: {
|
|
209
|
+
isRoot: true,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (!statusPageDomain || !statusPageDomain.statusPageId) {
|
|
214
|
+
return Response.sendErrorResponse(
|
|
215
|
+
req,
|
|
216
|
+
res,
|
|
217
|
+
new NotFoundException("Status Page not found"),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
statusPageId = statusPageDomain.statusPageId;
|
|
222
|
+
} else {
|
|
223
|
+
// then this is a status page id. We need to get the status page id from the id.
|
|
224
|
+
try {
|
|
225
|
+
statusPageId = new ObjectID(statusPageIdOrDomain);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
return Response.sendErrorResponse(
|
|
228
|
+
req,
|
|
229
|
+
res,
|
|
230
|
+
new NotFoundException("Status Page not found"),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const statusPage: StatusPage | null = await StatusPageService.findOneBy(
|
|
236
|
+
{
|
|
237
|
+
query: {
|
|
238
|
+
_id: statusPageId,
|
|
239
|
+
},
|
|
240
|
+
select: {
|
|
241
|
+
faviconFile: {
|
|
242
|
+
file: true,
|
|
243
|
+
_id: true,
|
|
244
|
+
type: true,
|
|
245
|
+
name: true,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
props: {
|
|
249
|
+
isRoot: true,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (!statusPage || !statusPage.faviconFile) {
|
|
255
|
+
logger.debug("Favicon file not found. Returning default favicon.");
|
|
256
|
+
|
|
257
|
+
// return default favicon.
|
|
258
|
+
return Response.sendFileByPath(
|
|
259
|
+
req,
|
|
260
|
+
res,
|
|
261
|
+
`/usr/src/Common/UI/Images/favicon/status-green.png`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
logger.debug(
|
|
266
|
+
`Favicon file found. Sending file: ${statusPage.faviconFile.name}`,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
return Response.sendFileResponse(req, res, statusPage.faviconFile!);
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
|
|
90
273
|
// confirm subscription api
|
|
91
274
|
this.router.get(
|
|
92
275
|
`${new this.entityType()
|
|
@@ -1344,6 +1527,22 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
1344
1527
|
},
|
|
1345
1528
|
);
|
|
1346
1529
|
|
|
1530
|
+
this.router.post(
|
|
1531
|
+
`${new this.entityType()
|
|
1532
|
+
.getCrudApiPath()
|
|
1533
|
+
?.toString()}/manage-subscription/:statusPageId`,
|
|
1534
|
+
UserMiddleware.getUserMiddleware,
|
|
1535
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
1536
|
+
try {
|
|
1537
|
+
await this.manageExistingSubscription(req);
|
|
1538
|
+
|
|
1539
|
+
return Response.sendEmptySuccessResponse(req, res);
|
|
1540
|
+
} catch (err) {
|
|
1541
|
+
next(err);
|
|
1542
|
+
}
|
|
1543
|
+
},
|
|
1544
|
+
);
|
|
1545
|
+
|
|
1347
1546
|
this.router.post(
|
|
1348
1547
|
`${new this.entityType()
|
|
1349
1548
|
.getCrudApiPath()
|
|
@@ -1941,6 +2140,233 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
1941
2140
|
return response;
|
|
1942
2141
|
}
|
|
1943
2142
|
|
|
2143
|
+
@CaptureSpan()
|
|
2144
|
+
public async manageExistingSubscription(req: ExpressRequest): Promise<void> {
|
|
2145
|
+
const objectId: ObjectID = new ObjectID(
|
|
2146
|
+
req.params["statusPageId"] as string,
|
|
2147
|
+
);
|
|
2148
|
+
|
|
2149
|
+
logger.debug(`Managing Existing Subscription for Status Page: ${objectId}`);
|
|
2150
|
+
|
|
2151
|
+
if (
|
|
2152
|
+
!(await this.service.hasReadAccess(
|
|
2153
|
+
objectId,
|
|
2154
|
+
await CommonAPI.getDatabaseCommonInteractionProps(req),
|
|
2155
|
+
req,
|
|
2156
|
+
))
|
|
2157
|
+
) {
|
|
2158
|
+
logger.debug(`No read access to status page with ID: ${objectId}`);
|
|
2159
|
+
throw new NotAuthenticatedException(
|
|
2160
|
+
"You are not authenticated to access this status page",
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
2165
|
+
query: {
|
|
2166
|
+
_id: objectId.toString(),
|
|
2167
|
+
},
|
|
2168
|
+
select: {
|
|
2169
|
+
_id: true,
|
|
2170
|
+
projectId: true,
|
|
2171
|
+
enableEmailSubscribers: true,
|
|
2172
|
+
enableSmsSubscribers: true,
|
|
2173
|
+
allowSubscribersToChooseResources: true,
|
|
2174
|
+
allowSubscribersToChooseEventTypes: true,
|
|
2175
|
+
showSubscriberPageOnStatusPage: true,
|
|
2176
|
+
},
|
|
2177
|
+
props: {
|
|
2178
|
+
isRoot: true,
|
|
2179
|
+
},
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
if (!statusPage) {
|
|
2183
|
+
logger.debug(`Status page not found with ID: ${objectId}`);
|
|
2184
|
+
throw new BadDataException("Status Page not found");
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
if (!statusPage.showSubscriberPageOnStatusPage) {
|
|
2188
|
+
logger.debug(
|
|
2189
|
+
`Subscriber page not enabled for status page with ID: ${objectId}`,
|
|
2190
|
+
);
|
|
2191
|
+
throw new BadDataException(
|
|
2192
|
+
"Subscribes not enabled for this status page.",
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
logger.debug(`Status page found: ${JSON.stringify(statusPage)}`);
|
|
2197
|
+
|
|
2198
|
+
if (
|
|
2199
|
+
req.body.data["subscriberEmail"] &&
|
|
2200
|
+
!statusPage.enableEmailSubscribers
|
|
2201
|
+
) {
|
|
2202
|
+
logger.debug(
|
|
2203
|
+
`Email subscribers not enabled for status page with ID: ${objectId}`,
|
|
2204
|
+
);
|
|
2205
|
+
throw new BadDataException(
|
|
2206
|
+
"Email subscribers not enabled for this status page.",
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
if (req.body.data["subscriberPhone"] && !statusPage.enableSmsSubscribers) {
|
|
2211
|
+
logger.debug(
|
|
2212
|
+
`SMS subscribers not enabled for status page with ID: ${objectId}`,
|
|
2213
|
+
);
|
|
2214
|
+
throw new BadDataException(
|
|
2215
|
+
"SMS subscribers not enabled for this status page.",
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// if no email or phone, throw error.
|
|
2220
|
+
|
|
2221
|
+
if (
|
|
2222
|
+
!req.body.data["subscriberEmail"] &&
|
|
2223
|
+
!req.body.data["subscriberPhone"]
|
|
2224
|
+
) {
|
|
2225
|
+
logger.debug(
|
|
2226
|
+
`No email or phone provided for subscription to status page with ID: ${objectId}`,
|
|
2227
|
+
);
|
|
2228
|
+
throw new BadDataException(
|
|
2229
|
+
"Email or phone is required to subscribe to this status page.",
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
const email: Email | undefined = req.body.data["subscriberEmail"]
|
|
2234
|
+
? new Email(req.body.data["subscriberEmail"] as string)
|
|
2235
|
+
: undefined;
|
|
2236
|
+
|
|
2237
|
+
const phone: Phone | undefined = req.body.data["subscriberPhone"]
|
|
2238
|
+
? new Phone(req.body.data["subscriberPhone"] as string)
|
|
2239
|
+
: undefined;
|
|
2240
|
+
|
|
2241
|
+
let statusPageSubscriber: StatusPageSubscriber | null = null;
|
|
2242
|
+
|
|
2243
|
+
if (email) {
|
|
2244
|
+
logger.debug(`Setting subscriber email: ${email}`);
|
|
2245
|
+
statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
|
|
2246
|
+
query: {
|
|
2247
|
+
subscriberEmail: email,
|
|
2248
|
+
statusPageId: objectId,
|
|
2249
|
+
},
|
|
2250
|
+
select: {
|
|
2251
|
+
_id: true,
|
|
2252
|
+
subscriberEmail: true,
|
|
2253
|
+
},
|
|
2254
|
+
props: {
|
|
2255
|
+
isRoot: true,
|
|
2256
|
+
},
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
if (phone) {
|
|
2261
|
+
logger.debug(`Setting subscriber phone: ${phone}`);
|
|
2262
|
+
statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
|
|
2263
|
+
query: {
|
|
2264
|
+
subscriberPhone: phone,
|
|
2265
|
+
statusPageId: objectId,
|
|
2266
|
+
},
|
|
2267
|
+
select: {
|
|
2268
|
+
_id: true,
|
|
2269
|
+
subscriberPhone: true,
|
|
2270
|
+
},
|
|
2271
|
+
props: {
|
|
2272
|
+
isRoot: true,
|
|
2273
|
+
},
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
if (!statusPageSubscriber) {
|
|
2278
|
+
// not found, return bad data
|
|
2279
|
+
logger.debug(
|
|
2280
|
+
`Subscriber not found for email: ${email} or phone: ${phone}`,
|
|
2281
|
+
);
|
|
2282
|
+
|
|
2283
|
+
let emailOrPhone: string = "email";
|
|
2284
|
+
if (phone) {
|
|
2285
|
+
emailOrPhone = "phone";
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
throw new BadDataException(
|
|
2289
|
+
`Subscription not found for this status page. Please make sure your ${emailOrPhone} is correct.`,
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
const statusPageURL: string =
|
|
2294
|
+
await StatusPageService.getStatusPageURL(objectId);
|
|
2295
|
+
|
|
2296
|
+
const manageUrlink: string = StatusPageSubscriberService.getUnsubscribeLink(
|
|
2297
|
+
URL.fromString(statusPageURL),
|
|
2298
|
+
statusPageSubscriber.id!,
|
|
2299
|
+
).toString();
|
|
2300
|
+
|
|
2301
|
+
const statusPages: Array<StatusPage> =
|
|
2302
|
+
await StatusPageSubscriberService.getStatusPagesToSendNotification([
|
|
2303
|
+
objectId,
|
|
2304
|
+
]);
|
|
2305
|
+
|
|
2306
|
+
for (const statusPage of statusPages) {
|
|
2307
|
+
// send email to subscriber or sms if phone is provided.
|
|
2308
|
+
|
|
2309
|
+
if (email) {
|
|
2310
|
+
const host: Hostname = await DatabaseConfig.getHost();
|
|
2311
|
+
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
2312
|
+
|
|
2313
|
+
MailService.sendMail(
|
|
2314
|
+
{
|
|
2315
|
+
toEmail: email,
|
|
2316
|
+
templateType:
|
|
2317
|
+
EmailTemplateType.ManageExistingStatusPageSubscriberSubscription,
|
|
2318
|
+
vars: {
|
|
2319
|
+
statusPageName: statusPage.name || "Status Page",
|
|
2320
|
+
statusPageUrl: statusPageURL,
|
|
2321
|
+
logoUrl: statusPage.logoFileId
|
|
2322
|
+
? new URL(httpProtocol, host)
|
|
2323
|
+
.addRoute(FileRoute)
|
|
2324
|
+
.addRoute("/image/" + statusPage.logoFileId)
|
|
2325
|
+
.toString()
|
|
2326
|
+
: "",
|
|
2327
|
+
isPublicStatusPage: statusPage.isPublicStatusPage
|
|
2328
|
+
? "true"
|
|
2329
|
+
: "false",
|
|
2330
|
+
subscriberEmailNotificationFooterText:
|
|
2331
|
+
statusPage.subscriberEmailNotificationFooterText || "",
|
|
2332
|
+
|
|
2333
|
+
manageSubscriptionUrl: manageUrlink,
|
|
2334
|
+
},
|
|
2335
|
+
subject:
|
|
2336
|
+
"Manage your Subscription for" +
|
|
2337
|
+
(statusPage.name || "Status Page"),
|
|
2338
|
+
},
|
|
2339
|
+
{
|
|
2340
|
+
mailServer: ProjectSmtpConfigService.toEmailServer(
|
|
2341
|
+
statusPage.smtpConfig,
|
|
2342
|
+
),
|
|
2343
|
+
projectId: statusPage.projectId!,
|
|
2344
|
+
},
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
if (phone) {
|
|
2349
|
+
const sms: SMS = {
|
|
2350
|
+
message: `You have selected to manage your subscription for the status page: ${statusPage.name}. You can manage your subscription here: ${manageUrlink}`,
|
|
2351
|
+
to: phone,
|
|
2352
|
+
};
|
|
2353
|
+
// send sms here.
|
|
2354
|
+
SmsService.sendSms(sms, {
|
|
2355
|
+
projectId: statusPage.projectId,
|
|
2356
|
+
customTwilioConfig: ProjectCallSMSConfigService.toTwilioConfig(
|
|
2357
|
+
statusPage.callSmsConfig,
|
|
2358
|
+
),
|
|
2359
|
+
}).catch((err: Error) => {
|
|
2360
|
+
logger.error(err);
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
logger.debug(
|
|
2365
|
+
`Subscription management link sent to subscriber with ID: ${statusPageSubscriber.id}`,
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
|
|
1944
2370
|
@CaptureSpan()
|
|
1945
2371
|
public async subscribeToStatusPage(req: ExpressRequest): Promise<void> {
|
|
1946
2372
|
const objectId: ObjectID = new ObjectID(
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
AdminDashboardRoute,
|
|
4
4
|
DashboardRoute,
|
|
5
5
|
AppApiRoute,
|
|
6
|
+
StatusPageApiRoute,
|
|
6
7
|
} from "Common/ServiceRoute";
|
|
7
8
|
import BillingConfig from "./BillingConfig";
|
|
8
9
|
import Hostname from "Common/Types/API/Hostname";
|
|
@@ -306,6 +307,12 @@ export const AdminDashboardClientURL: URL = new URL(
|
|
|
306
307
|
|
|
307
308
|
export const AppApiClientUrl: URL = new URL(HttpProtocol, Host, AppApiRoute);
|
|
308
309
|
|
|
310
|
+
export const StatusPageApiClientUrl: URL = new URL(
|
|
311
|
+
HttpProtocol,
|
|
312
|
+
Host,
|
|
313
|
+
StatusPageApiRoute,
|
|
314
|
+
);
|
|
315
|
+
|
|
309
316
|
export const DashboardClientUrl: URL = new URL(
|
|
310
317
|
HttpProtocol,
|
|
311
318
|
Host,
|
|
@@ -238,7 +238,7 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
|
|
238
238
|
onCallDutyPolicyFeedEventType:
|
|
239
239
|
OnCallDutyPolicyFeedEventType.RosterHandoff,
|
|
240
240
|
displayColor: Green500,
|
|
241
|
-
feedInfoInMarkdown:
|
|
241
|
+
feedInfoInMarkdown: `🚫 **${await UserService.getUserMarkdownString(
|
|
242
242
|
{
|
|
243
243
|
userId: sendEmailToUserId,
|
|
244
244
|
projectId: projectId!,
|
|
@@ -142,6 +142,10 @@ export interface InitFuctionOptions {
|
|
|
142
142
|
port?: Port | undefined;
|
|
143
143
|
isFrontendApp?: boolean;
|
|
144
144
|
statusOptions: StatusAPIOptions;
|
|
145
|
+
getVariablesToRenderIndexPage?: (
|
|
146
|
+
req: ExpressRequest,
|
|
147
|
+
res: ExpressResponse,
|
|
148
|
+
) => Promise<JSONObject>;
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
type InitFunction = (
|
|
@@ -200,11 +204,36 @@ const init: InitFunction = async (
|
|
|
200
204
|
},
|
|
201
205
|
);
|
|
202
206
|
|
|
203
|
-
app.get(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
app.get(
|
|
208
|
+
["/*", `/${appName}/*`],
|
|
209
|
+
async (_req: ExpressRequest, res: ExpressResponse) => {
|
|
210
|
+
logger.debug("Rendering index page");
|
|
211
|
+
|
|
212
|
+
let variables: JSONObject = {};
|
|
213
|
+
|
|
214
|
+
if (data.getVariablesToRenderIndexPage) {
|
|
215
|
+
logger.debug("Getting variables to render index page");
|
|
216
|
+
try {
|
|
217
|
+
const variablesToRenderIndexPage: JSONObject =
|
|
218
|
+
await data.getVariablesToRenderIndexPage(_req, res);
|
|
219
|
+
variables = {
|
|
220
|
+
...variables,
|
|
221
|
+
...variablesToRenderIndexPage,
|
|
222
|
+
};
|
|
223
|
+
} catch (error) {
|
|
224
|
+
logger.error(error);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
logger.debug("Rendering index page with variables: ");
|
|
229
|
+
logger.debug(variables);
|
|
230
|
+
|
|
231
|
+
return res.render("/usr/src/app/views/index.ejs", {
|
|
232
|
+
enableGoogleTagManager: IsBillingEnabled || false,
|
|
233
|
+
...variables,
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
);
|
|
208
237
|
}
|
|
209
238
|
|
|
210
239
|
return app;
|
|
@@ -16,6 +16,7 @@ enum EmailTemplateType {
|
|
|
16
16
|
SubscriberIncidentNoteCreated = "SubscriberIncidentNoteCreated.hbs",
|
|
17
17
|
SubscriberIncidentStateChanged = "SubscriberIncidentStateChanged.hbs",
|
|
18
18
|
SubscriberScheduledMaintenanceEventCreated = "SubscriberScheduledMaintenanceEventCreated.hbs",
|
|
19
|
+
ManageExistingStatusPageSubscriberSubscription = "ManageExistingStatusPageSubscriberSubscription.hbs",
|
|
19
20
|
SubscriberScheduledMaintenanceEventStateChanged = "SubscriberScheduledMaintenanceEventStateChanged.hbs",
|
|
20
21
|
StatusPageForgotPassword = "StatusPageForgotPassword.hbs",
|
|
21
22
|
StatusPagePasswordChanged = "StatusPagePasswordChanged.hbs",
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React, { ReactElement, useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface DropdownOption {
|
|
4
|
+
label: string;
|
|
5
|
+
onClick: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface DropdownButtonProps {
|
|
9
|
+
title: string;
|
|
10
|
+
dropdownOptions: DropdownOption[];
|
|
11
|
+
onButtonClick: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ComponentProps {
|
|
15
|
+
title: string;
|
|
16
|
+
dropdownOptions: DropdownOption[];
|
|
17
|
+
onButtonClick: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DropdownButton: React.FC<DropdownButtonProps> = ({
|
|
21
|
+
title,
|
|
22
|
+
dropdownOptions,
|
|
23
|
+
onButtonClick,
|
|
24
|
+
}: ComponentProps): ReactElement => {
|
|
25
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
26
|
+
|
|
27
|
+
const toggleDropdown: VoidFunction = () => {
|
|
28
|
+
setIsOpen(!isOpen);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="inline-flex rounded-md shadow-sm">
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
className="relative inline-flex items-center rounded-l-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
|
|
36
|
+
onClick={onButtonClick}
|
|
37
|
+
>
|
|
38
|
+
{title}
|
|
39
|
+
</button>
|
|
40
|
+
<div className="relative -ml-px block">
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
className="relative inline-flex items-center rounded-r-md bg-white px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
|
|
44
|
+
id="option-menu-button"
|
|
45
|
+
aria-expanded={isOpen}
|
|
46
|
+
aria-haspopup="true"
|
|
47
|
+
onClick={toggleDropdown}
|
|
48
|
+
>
|
|
49
|
+
<span className="sr-only">Open options</span>
|
|
50
|
+
<svg
|
|
51
|
+
className="size-5"
|
|
52
|
+
viewBox="0 0 20 20"
|
|
53
|
+
fill="currentColor"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
>
|
|
56
|
+
<path
|
|
57
|
+
fillRule="evenodd"
|
|
58
|
+
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
|
|
59
|
+
clipRule="evenodd"
|
|
60
|
+
/>
|
|
61
|
+
</svg>
|
|
62
|
+
</button>
|
|
63
|
+
|
|
64
|
+
{isOpen && (
|
|
65
|
+
<div
|
|
66
|
+
className="absolute right-0 z-10 -mr-1 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-none"
|
|
67
|
+
role="menu"
|
|
68
|
+
aria-orientation="vertical"
|
|
69
|
+
aria-labelledby="option-menu-button"
|
|
70
|
+
>
|
|
71
|
+
<div className="py-1" role="none">
|
|
72
|
+
{dropdownOptions.map((option: DropdownOption, index: number) => {
|
|
73
|
+
return (
|
|
74
|
+
<a
|
|
75
|
+
key={index}
|
|
76
|
+
href="#"
|
|
77
|
+
className="block px-4 py-2 text-sm text-gray-700"
|
|
78
|
+
role="menuitem"
|
|
79
|
+
onClick={option.onClick}
|
|
80
|
+
>
|
|
81
|
+
{option.label}
|
|
82
|
+
</a>
|
|
83
|
+
);
|
|
84
|
+
})}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default DropdownButton;
|
|
@@ -60,7 +60,12 @@ export interface BaseComponentProps<T> {
|
|
|
60
60
|
initialValues?: FormValues<T> | undefined;
|
|
61
61
|
values?: FormValues<T> | undefined;
|
|
62
62
|
onValidate?: undefined | ((values: FormValues<T>) => JSONObject);
|
|
63
|
-
onChange?:
|
|
63
|
+
onChange?:
|
|
64
|
+
| undefined
|
|
65
|
+
| ((
|
|
66
|
+
values: FormValues<T>,
|
|
67
|
+
setNewFormValues: (newValues: FormValues<T>) => void,
|
|
68
|
+
) => void);
|
|
64
69
|
fields: Fields<T>;
|
|
65
70
|
steps?: undefined | Array<FormStep<T>>;
|
|
66
71
|
submitButtonText?: undefined | string;
|
|
@@ -352,7 +357,10 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
|
|
352
357
|
setCurrentValue(refCurrentValue.current);
|
|
353
358
|
|
|
354
359
|
if (props.onChange && isInitialValuesSet.current) {
|
|
355
|
-
props.onChange(refCurrentValue.current)
|
|
360
|
+
props.onChange(refCurrentValue.current, (values: FormValues<T>) => {
|
|
361
|
+
refCurrentValue.current = values;
|
|
362
|
+
setCurrentValue(refCurrentValue.current);
|
|
363
|
+
});
|
|
356
364
|
}
|
|
357
365
|
};
|
|
358
366
|
|
|
@@ -657,6 +665,10 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
|
|
657
665
|
disableAutofocus={
|
|
658
666
|
props.disableAutofocus || false
|
|
659
667
|
}
|
|
668
|
+
setFormValues={(values: FormValues<T>) => {
|
|
669
|
+
refCurrentValue.current = values;
|
|
670
|
+
setCurrentValue(refCurrentValue.current);
|
|
671
|
+
}}
|
|
660
672
|
/>
|
|
661
673
|
}
|
|
662
674
|
{field.footerElement}
|