mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-01-16 23:00:51 +00:00
4257 lines
127 KiB
TypeScript
4257 lines
127 KiB
TypeScript
import UserMiddleware from "../Middleware/UserAuthorization";
|
|
import AcmeChallengeService from "../Services/AcmeChallengeService";
|
|
import IncidentPublicNoteService from "../Services/IncidentPublicNoteService";
|
|
import IncidentService from "../Services/IncidentService";
|
|
import IncidentStateService from "../Services/IncidentStateService";
|
|
import IncidentStateTimelineService from "../Services/IncidentStateTimelineService";
|
|
import MonitorGroupResourceService from "../Services/MonitorGroupResourceService";
|
|
import MonitorGroupService from "../Services/MonitorGroupService";
|
|
import MonitorStatusService from "../Services/MonitorStatusService";
|
|
import ScheduledMaintenancePublicNoteService from "../Services/ScheduledMaintenancePublicNoteService";
|
|
import ScheduledMaintenanceService from "../Services/ScheduledMaintenanceService";
|
|
import ScheduledMaintenanceStateService from "../Services/ScheduledMaintenanceStateService";
|
|
import ScheduledMaintenanceStateTimelineService from "../Services/ScheduledMaintenanceStateTimelineService";
|
|
import StatusPageAnnouncementService from "../Services/StatusPageAnnouncementService";
|
|
import StatusPageDomainService from "../Services/StatusPageDomainService";
|
|
import StatusPageFooterLinkService from "../Services/StatusPageFooterLinkService";
|
|
import StatusPageGroupService from "../Services/StatusPageGroupService";
|
|
import StatusPageHeaderLinkService from "../Services/StatusPageHeaderLinkService";
|
|
import StatusPageHistoryChartBarColorRuleService from "../Services/StatusPageHistoryChartBarColorRuleService";
|
|
import StatusPageResourceService from "../Services/StatusPageResourceService";
|
|
import StatusPageService, {
|
|
Service as StatusPageServiceType,
|
|
} from "../Services/StatusPageService";
|
|
import StatusPageSsoService from "../Services/StatusPageSsoService";
|
|
import StatusPageSubscriberService from "../Services/StatusPageSubscriberService";
|
|
import Query from "../Types/Database/Query";
|
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
import Select from "../Types/Database/Select";
|
|
import {
|
|
ExpressRequest,
|
|
ExpressResponse,
|
|
NextFunction,
|
|
} from "../Utils/Express";
|
|
import logger from "../Utils/Logger";
|
|
import Response from "../Utils/Response";
|
|
import BaseAPI from "./BaseAPI";
|
|
import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
import ArrayUtil from "../../Utils/Array";
|
|
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
import OneUptimeDate from "../../Types/Date";
|
|
import Dictionary from "../../Types/Dictionary";
|
|
import Email from "../../Types/Email";
|
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
|
|
import NotFoundException from "../../Types/Exception/NotFoundException";
|
|
import { JSONObject } from "../../Types/JSON";
|
|
import JSONFunctions from "../../Types/JSONFunctions";
|
|
import ObjectID from "../../Types/ObjectID";
|
|
import Phone from "../../Types/Phone";
|
|
import PositiveNumber from "../../Types/PositiveNumber";
|
|
import HashedString from "../../Types/HashedString";
|
|
import AcmeChallenge from "../../Models/DatabaseModels/AcmeChallenge";
|
|
import Incident from "../../Models/DatabaseModels/Incident";
|
|
import IncidentPublicNote from "../../Models/DatabaseModels/IncidentPublicNote";
|
|
import IncidentState from "../../Models/DatabaseModels/IncidentState";
|
|
import IncidentStateTimeline from "../../Models/DatabaseModels/IncidentStateTimeline";
|
|
import MonitorGroupResource from "../../Models/DatabaseModels/MonitorGroupResource";
|
|
import MonitorStatus from "../../Models/DatabaseModels/MonitorStatus";
|
|
import MonitorStatusTimeline from "../../Models/DatabaseModels/MonitorStatusTimeline";
|
|
import ScheduledMaintenance from "../../Models/DatabaseModels/ScheduledMaintenance";
|
|
import ScheduledMaintenancePublicNote from "../../Models/DatabaseModels/ScheduledMaintenancePublicNote";
|
|
import ScheduledMaintenanceState from "../../Models/DatabaseModels/ScheduledMaintenanceState";
|
|
import ScheduledMaintenanceStateTimeline from "../../Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
|
|
import StatusPage from "../../Models/DatabaseModels/StatusPage";
|
|
import StatusPageAnnouncement from "../../Models/DatabaseModels/StatusPageAnnouncement";
|
|
import File from "../../Models/DatabaseModels/File";
|
|
import StatusPageDomain from "../../Models/DatabaseModels/StatusPageDomain";
|
|
import StatusPageFooterLink from "../../Models/DatabaseModels/StatusPageFooterLink";
|
|
import StatusPageGroup from "../../Models/DatabaseModels/StatusPageGroup";
|
|
import StatusPageHeaderLink from "../../Models/DatabaseModels/StatusPageHeaderLink";
|
|
import StatusPageHistoryChartBarColorRule from "../../Models/DatabaseModels/StatusPageHistoryChartBarColorRule";
|
|
import StatusPageResource from "../../Models/DatabaseModels/StatusPageResource";
|
|
import StatusPageSSO from "../../Models/DatabaseModels/StatusPageSso";
|
|
import StatusPageSubscriber from "../../Models/DatabaseModels/StatusPageSubscriber";
|
|
import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
|
|
import StatusPageResourceUptimeUtil from "../../Utils/StatusPage/ResourceUptime";
|
|
import UptimePrecision from "../../Types/StatusPage/UptimePrecision";
|
|
import { Green } from "../../Types/BrandColors";
|
|
import UptimeUtil from "../../Utils/Uptime/UptimeUtil";
|
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
import URL from "../../Types/API/URL";
|
|
import SMS from "../../Types/SMS/SMS";
|
|
import SmsService from "../Services/SmsService";
|
|
import ProjectCallSMSConfigService from "../Services/ProjectCallSMSConfigService";
|
|
import MailService from "../Services/MailService";
|
|
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
|
|
import Hostname from "../../Types/API/Hostname";
|
|
import Protocol from "../../Types/API/Protocol";
|
|
import DatabaseConfig from "../DatabaseConfig";
|
|
import CookieUtil from "../Utils/Cookie";
|
|
import { EncryptionSecret } from "../EnvironmentConfig";
|
|
import { StatusPageApiRoute } from "../../ServiceRoute";
|
|
import ProjectSmtpConfigService from "../Services/ProjectSmtpConfigService";
|
|
import ForbiddenException from "../../Types/Exception/ForbiddenException";
|
|
import SlackUtil from "../Utils/Workspace/Slack/Slack";
|
|
import { MASTER_PASSWORD_INVALID_MESSAGE } from "../../Types/StatusPage/MasterPassword";
|
|
|
|
type ResolveStatusPageIdOrThrowFunction = (
|
|
statusPageIdOrDomain: string,
|
|
) => Promise<ObjectID>;
|
|
|
|
const resolveStatusPageIdOrThrow: ResolveStatusPageIdOrThrowFunction = async (
|
|
statusPageIdOrDomain: string,
|
|
): Promise<ObjectID> => {
|
|
if (!statusPageIdOrDomain) {
|
|
throw new NotFoundException("Status Page not found");
|
|
}
|
|
|
|
if (statusPageIdOrDomain.includes(".")) {
|
|
const statusPageDomain: StatusPageDomain | null =
|
|
await StatusPageDomainService.findOneBy({
|
|
query: {
|
|
fullDomain: statusPageIdOrDomain,
|
|
domain: {
|
|
isVerified: true,
|
|
} as any,
|
|
},
|
|
select: {
|
|
statusPageId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPageDomain || !statusPageDomain.statusPageId) {
|
|
throw new NotFoundException("Status Page not found");
|
|
}
|
|
|
|
return statusPageDomain.statusPageId;
|
|
}
|
|
|
|
try {
|
|
return new ObjectID(statusPageIdOrDomain);
|
|
} catch (err) {
|
|
logger.error(
|
|
`Error converting statusPageIdOrDomain to ObjectID: ${statusPageIdOrDomain}`,
|
|
);
|
|
logger.error(err);
|
|
throw new NotFoundException("Status Page not found");
|
|
}
|
|
};
|
|
|
|
export default class StatusPageAPI extends BaseAPI<
|
|
StatusPage,
|
|
StatusPageServiceType
|
|
> {
|
|
public constructor() {
|
|
super(StatusPage, StatusPageService);
|
|
|
|
// get title, description of the page. This is used for SEO.
|
|
this.router.get(
|
|
`${new this.entityType().getCrudApiPath()?.toString()}/seo/:statusPageIdOrDomain`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
const statusPageIdOrDomain: string = req.params[
|
|
"statusPageIdOrDomain"
|
|
] as string;
|
|
|
|
let statusPageId: ObjectID | null = null;
|
|
|
|
if (statusPageIdOrDomain && statusPageIdOrDomain.includes(".")) {
|
|
// then this is a domain and not the status page id. We need to get the status page id from the domain.
|
|
|
|
const statusPageDomain: StatusPageDomain | null =
|
|
await StatusPageDomainService.findOneBy({
|
|
query: {
|
|
fullDomain: statusPageIdOrDomain,
|
|
domain: {
|
|
isVerified: true,
|
|
} as any,
|
|
},
|
|
select: {
|
|
statusPageId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPageDomain || !statusPageDomain.statusPageId) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotFoundException("Status Page not found"),
|
|
);
|
|
}
|
|
|
|
statusPageId = statusPageDomain.statusPageId;
|
|
} else {
|
|
// then this is a status page id. We need to get the status page id from the id.
|
|
try {
|
|
statusPageId = new ObjectID(statusPageIdOrDomain);
|
|
} catch (err) {
|
|
logger.error(
|
|
`Error converting statusPageIdOrDomain to ObjectID: ${statusPageIdOrDomain}`,
|
|
);
|
|
logger.error(err);
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotFoundException("Status Page not found"),
|
|
);
|
|
}
|
|
}
|
|
|
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy(
|
|
{
|
|
query: {
|
|
_id: statusPageId,
|
|
},
|
|
select: {
|
|
pageTitle: true,
|
|
pageDescription: true,
|
|
name: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
},
|
|
);
|
|
|
|
if (!statusPage) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotFoundException("Status Page not found"),
|
|
);
|
|
}
|
|
|
|
return Response.sendJsonObjectResponse(req, res, {
|
|
title: statusPage.pageTitle || statusPage.name,
|
|
description: statusPage.pageDescription,
|
|
_id: statusPage._id?.toString(),
|
|
});
|
|
},
|
|
);
|
|
|
|
// favicon api.
|
|
this.router.get(
|
|
`${new this.entityType().getCrudApiPath()?.toString()}/favicon/:statusPageIdOrDomain`,
|
|
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
try {
|
|
const statusPageId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
req.params["statusPageIdOrDomain"] as string,
|
|
);
|
|
|
|
const statusPage: StatusPage | null =
|
|
await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: statusPageId,
|
|
},
|
|
select: {
|
|
faviconFile: {
|
|
file: true,
|
|
_id: true,
|
|
fileType: true,
|
|
name: true,
|
|
},
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage || !statusPage.faviconFile) {
|
|
logger.debug("Favicon file not found. Returning default favicon.");
|
|
|
|
return Response.sendFileByPath(
|
|
req,
|
|
res,
|
|
`/usr/src/Common/UI/Images/favicon/status-green.png`,
|
|
);
|
|
}
|
|
|
|
logger.debug(
|
|
`Favicon file found. Sending file: ${statusPage.faviconFile.name}`,
|
|
);
|
|
|
|
return Response.sendFileResponse(req, res, statusPage.faviconFile);
|
|
} catch (error) {
|
|
if (error instanceof NotFoundException) {
|
|
return Response.sendErrorResponse(req, res, error);
|
|
}
|
|
|
|
logger.error(error);
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotFoundException("Status Page not found"),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.get(
|
|
`${new this.entityType().getCrudApiPath()?.toString()}/logo/:statusPageIdOrDomain`,
|
|
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
try {
|
|
const statusPageId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
req.params["statusPageIdOrDomain"] as string,
|
|
);
|
|
|
|
const statusPage: StatusPage | null =
|
|
await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: statusPageId,
|
|
},
|
|
select: {
|
|
logoFile: {
|
|
file: true,
|
|
_id: true,
|
|
fileType: true,
|
|
name: true,
|
|
},
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage || !statusPage.logoFile) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotFoundException("Status Page logo not found"),
|
|
);
|
|
}
|
|
|
|
return Response.sendFileResponse(req, res, statusPage.logoFile);
|
|
} catch (error) {
|
|
if (error instanceof NotFoundException) {
|
|
return Response.sendErrorResponse(req, res, error);
|
|
}
|
|
|
|
logger.error(error);
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotFoundException("Status Page logo not found"),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.get(
|
|
`${new this.entityType().getCrudApiPath()?.toString()}/cover-image/:statusPageIdOrDomain`,
|
|
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
try {
|
|
const statusPageId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
req.params["statusPageIdOrDomain"] as string,
|
|
);
|
|
|
|
const statusPage: StatusPage | null =
|
|
await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: statusPageId,
|
|
},
|
|
select: {
|
|
coverImageFile: {
|
|
file: true,
|
|
_id: true,
|
|
fileType: true,
|
|
name: true,
|
|
},
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage || !statusPage.coverImageFile) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotFoundException("Status Page cover image not found"),
|
|
);
|
|
}
|
|
|
|
return Response.sendFileResponse(req, res, statusPage.coverImageFile);
|
|
} catch (error) {
|
|
if (error instanceof NotFoundException) {
|
|
return Response.sendErrorResponse(req, res, error);
|
|
}
|
|
|
|
logger.error(error);
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotFoundException("Status Page cover image not found"),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.get(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/incident-public-note/attachment/:statusPageId/:incidentId/:noteId/:fileId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
await this.getIncidentPublicNoteAttachment(req, res);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.get(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/incident/postmortem/attachment/:statusPageId/:incidentId/:fileId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
await this.getIncidentPostmortemAttachment(req, res);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.get(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/scheduled-maintenance-public-note/attachment/:statusPageId/:scheduledMaintenanceId/:noteId/:fileId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
await this.getScheduledMaintenancePublicNoteAttachment(req, res);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.get(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/status-page-announcement/attachment/:statusPageId/:announcementId/:fileId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
await this.getStatusPageAnnouncementAttachment(req, res);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
// embedded overall status badge api
|
|
this.router.get(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/badge/:statusPageId`,
|
|
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
try {
|
|
const statusPageId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
const token: string = req.query["token"] as string;
|
|
|
|
if (!token) {
|
|
return res.status(400).send("Token is required");
|
|
}
|
|
|
|
// Fetch status page with security token
|
|
const statusPage: StatusPage | null =
|
|
await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: statusPageId,
|
|
enableEmbeddedOverallStatus: true,
|
|
embeddedOverallStatusToken: token,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
downtimeMonitorStatuses: {
|
|
_id: true,
|
|
},
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage) {
|
|
return res.status(404).send("Status badge not found or disabled");
|
|
}
|
|
|
|
// Get status page resources and current statuses
|
|
const statusPageResources: Array<StatusPageResource> =
|
|
await StatusPageResourceService.findBy({
|
|
query: {
|
|
statusPageId: statusPageId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
monitor: {
|
|
_id: true,
|
|
currentMonitorStatusId: true,
|
|
},
|
|
monitorGroupId: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// Get monitor statuses
|
|
const monitorStatuses: Array<MonitorStatus> =
|
|
await MonitorStatusService.findBy({
|
|
query: {
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
name: true,
|
|
color: true,
|
|
priority: true,
|
|
isOperationalState: true,
|
|
},
|
|
sort: {
|
|
priority: SortOrder.Ascending,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// Get monitor group current statuses
|
|
const monitorGroupCurrentStatuses: Dictionary<ObjectID> =
|
|
await StatusPageService.getMonitorGroupCurrentStatuses({
|
|
statusPageResources,
|
|
monitorStatuses,
|
|
});
|
|
|
|
// Calculate overall status
|
|
const overallStatus: MonitorStatus | null =
|
|
StatusPageService.getOverallMonitorStatus({
|
|
statusPageResources,
|
|
monitorStatuses,
|
|
monitorGroupCurrentStatuses,
|
|
});
|
|
|
|
// Generate SVG badge
|
|
const statusName: string = overallStatus?.name || "Unknown";
|
|
const statusColor: string =
|
|
overallStatus?.color?.toString() || "#808080";
|
|
|
|
const svg: string = `<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20">
|
|
<linearGradient id="b" x2="0" y2="100%">
|
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
<stop offset="1" stop-opacity=".1"/>
|
|
</linearGradient>
|
|
<mask id="a">
|
|
<rect width="150" height="20" rx="3" fill="#fff"/>
|
|
</mask>
|
|
<g mask="url(#a)">
|
|
<path fill="#555" d="M0 0h50v20H0z"/>
|
|
<path fill="${statusColor}" d="M50 0h100v20H50z"/>
|
|
<path fill="url(#b)" d="M0 0h150v20H0z"/>
|
|
</g>
|
|
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
<text x="25" y="15" fill="#010101" fill-opacity=".3">status</text>
|
|
<text x="25" y="14">status</text>
|
|
<text x="100" y="15" fill="#010101" fill-opacity=".3">${statusName}</text>
|
|
<text x="100" y="14">${statusName}</text>
|
|
</g>
|
|
</svg>`;
|
|
|
|
res.setHeader("Content-Type", "image/svg+xml");
|
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
return res.send(svg);
|
|
} catch (err) {
|
|
logger.error(err);
|
|
return res.status(500).send("Internal Server Error");
|
|
}
|
|
},
|
|
);
|
|
|
|
// confirm subscription api
|
|
this.router.get(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/confirm-subscription/:statusPageSubscriberId`,
|
|
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
const token: string = req.query["verification-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,
|
|
},
|
|
});
|
|
|
|
await StatusPageSubscriberService.sendYouHaveSubscribedEmail({
|
|
subscriberId: statusPageSubscriberId,
|
|
});
|
|
|
|
return Response.sendEmptySuccessResponse(req, res);
|
|
},
|
|
);
|
|
|
|
// CNAME verification api
|
|
this.router.get(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/cname-verification/:token`,
|
|
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
const host: string | undefined = req.get("host");
|
|
|
|
if (!host) {
|
|
throw new BadDataException("Host not found");
|
|
}
|
|
|
|
const token: string = req.params["token"] as string;
|
|
|
|
logger.debug(`CNAME Verification: Host:${host} - Token:${token}`);
|
|
|
|
const domain: StatusPageDomain | null =
|
|
await StatusPageDomainService.findOneBy({
|
|
query: {
|
|
cnameVerificationToken: token,
|
|
fullDomain: host,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!domain) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new BadDataException("Invalid token."),
|
|
);
|
|
}
|
|
|
|
return Response.sendEmptySuccessResponse(req, res);
|
|
},
|
|
);
|
|
|
|
// ACME Challenge Validation.
|
|
this.router.get(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/.well-known/acme-challenge/:token`,
|
|
async (req: ExpressRequest, res: ExpressResponse) => {
|
|
const challenge: AcmeChallenge | null =
|
|
await AcmeChallengeService.findOneBy({
|
|
query: {
|
|
token: req.params["token"] as string,
|
|
},
|
|
select: {
|
|
challenge: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!challenge) {
|
|
return Response.sendErrorResponse(
|
|
req,
|
|
res,
|
|
new NotFoundException("Challenge not found"),
|
|
);
|
|
}
|
|
|
|
return Response.sendTextResponse(
|
|
req,
|
|
res,
|
|
challenge.challenge as string,
|
|
);
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType().getCrudApiPath()?.toString()}/test-email-report`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
const email: Email = new Email(req.body["email"] as string);
|
|
const statusPageId: ObjectID = new ObjectID(
|
|
req.body["statusPageId"].toString() as string,
|
|
);
|
|
|
|
await StatusPageService.sendEmailReport({
|
|
email: email,
|
|
statusPageId,
|
|
});
|
|
|
|
return Response.sendEmptySuccessResponse(req, res);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType().getCrudApiPath()?.toString()}/domain`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
if (!req.body["domain"]) {
|
|
throw new BadDataException("domain is required in request body");
|
|
}
|
|
|
|
const domain: string = req.body["domain"] as string;
|
|
|
|
const statusPageDomain: StatusPageDomain | null =
|
|
await StatusPageDomainService.findOneBy({
|
|
query: {
|
|
fullDomain: domain,
|
|
domain: {
|
|
isVerified: true,
|
|
} as any,
|
|
},
|
|
select: {
|
|
statusPageId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPageDomain) {
|
|
throw new BadDataException("No status page found with this domain");
|
|
}
|
|
|
|
const objectId: ObjectID = statusPageDomain.statusPageId!;
|
|
|
|
return Response.sendJsonObjectResponse(req, res, {
|
|
statusPageId: objectId.toString(),
|
|
});
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/master-page/:statusPageId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
const objectId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
const select: Select<StatusPage> = {
|
|
_id: true,
|
|
slug: true,
|
|
coverImageFileId: true,
|
|
logoFileId: true,
|
|
pageTitle: true,
|
|
pageDescription: true,
|
|
copyrightText: true,
|
|
customCSS: true,
|
|
customJavaScript: true,
|
|
hidePoweredByOneUptimeBranding: true,
|
|
headerHTML: true,
|
|
footerHTML: true,
|
|
enableEmailSubscribers: true,
|
|
enableSlackSubscribers: true,
|
|
enableMicrosoftTeamsSubscribers: true,
|
|
enableSmsSubscribers: true,
|
|
isPublicStatusPage: true,
|
|
enableMasterPassword: true,
|
|
allowSubscribersToChooseResources: true,
|
|
allowSubscribersToChooseEventTypes: true,
|
|
requireSsoForLogin: true,
|
|
coverImageFile: {
|
|
file: true,
|
|
_id: true,
|
|
fileType: true,
|
|
name: true,
|
|
},
|
|
faviconFile: {
|
|
file: true,
|
|
_id: true,
|
|
fileType: true,
|
|
name: true,
|
|
},
|
|
logoFile: {
|
|
file: true,
|
|
_id: true,
|
|
fileType: true,
|
|
name: true,
|
|
},
|
|
showIncidentsOnStatusPage: true,
|
|
showAnnouncementsOnStatusPage: true,
|
|
showScheduledMaintenanceEventsOnStatusPage: true,
|
|
showSubscriberPageOnStatusPage: true,
|
|
};
|
|
|
|
const hasEnabledSSO: PositiveNumber =
|
|
await StatusPageSsoService.countBy({
|
|
query: {
|
|
isEnabled: true,
|
|
statusPageId: objectId,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const item: StatusPage | null = await this.service.findOneById({
|
|
id: objectId,
|
|
select,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!item) {
|
|
throw new BadDataException("Status Page not found");
|
|
}
|
|
|
|
const footerLinks: Array<StatusPageFooterLink> =
|
|
await StatusPageFooterLinkService.findBy({
|
|
query: {
|
|
statusPageId: objectId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
link: true,
|
|
title: true,
|
|
order: true,
|
|
},
|
|
sort: {
|
|
order: SortOrder.Ascending,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const headerLinks: Array<StatusPageHeaderLink> =
|
|
await StatusPageHeaderLinkService.findBy({
|
|
query: {
|
|
statusPageId: objectId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
link: true,
|
|
title: true,
|
|
order: true,
|
|
},
|
|
sort: {
|
|
order: SortOrder.Ascending,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const response: JSONObject = {
|
|
statusPage: BaseModel.toJSON(item, StatusPage),
|
|
footerLinks: BaseModel.toJSONArray(
|
|
footerLinks,
|
|
StatusPageFooterLink,
|
|
),
|
|
headerLinks: BaseModel.toJSONArray(
|
|
headerLinks,
|
|
StatusPageHeaderLink,
|
|
),
|
|
hasEnabledSSO: hasEnabledSSO.toNumber(),
|
|
};
|
|
|
|
return Response.sendJsonObjectResponse(req, res, response);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/master-password/:statusPageId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
if (!req.params["statusPageId"]) {
|
|
throw new BadDataException("Status Page ID not found");
|
|
}
|
|
|
|
const statusPageId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
const password: string | undefined =
|
|
req.body && (req.body["password"] as string);
|
|
|
|
if (!password) {
|
|
throw new BadDataException("Master password is required.");
|
|
}
|
|
|
|
const statusPage: StatusPage | null =
|
|
await StatusPageService.findOneById({
|
|
id: statusPageId,
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
enableMasterPassword: true,
|
|
masterPassword: true,
|
|
isPublicStatusPage: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage) {
|
|
throw new NotFoundException("Status Page not found");
|
|
}
|
|
|
|
if (statusPage.isPublicStatusPage) {
|
|
throw new BadDataException(
|
|
"This status page is already visible to everyone.",
|
|
);
|
|
}
|
|
|
|
if (!statusPage.enableMasterPassword || !statusPage.masterPassword) {
|
|
throw new BadDataException(
|
|
"Master password has not been configured for this status page.",
|
|
);
|
|
}
|
|
|
|
const hashedInput: string = await HashedString.hashValue(
|
|
password,
|
|
EncryptionSecret,
|
|
);
|
|
|
|
if (hashedInput !== statusPage.masterPassword.toString()) {
|
|
throw new BadDataException(MASTER_PASSWORD_INVALID_MESSAGE);
|
|
}
|
|
|
|
CookieUtil.setStatusPageMasterPasswordCookie({
|
|
expressResponse: res,
|
|
statusPageId,
|
|
});
|
|
|
|
return Response.sendEmptySuccessResponse(req, res);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType().getCrudApiPath()?.toString()}/sso/:statusPageId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
const objectId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
const sso: Array<StatusPageSSO> = await StatusPageSsoService.findBy({
|
|
query: {
|
|
statusPageId: objectId,
|
|
isEnabled: true,
|
|
},
|
|
select: {
|
|
signOnURL: true,
|
|
name: true,
|
|
description: true,
|
|
_id: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
return Response.sendEntityArrayResponse(
|
|
req,
|
|
res,
|
|
sso,
|
|
new PositiveNumber(sso.length),
|
|
StatusPageSSO,
|
|
);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
// Get all status page resources for subscriber to subscribe to.
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/resources/:statusPageId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
const statusPageId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
await this.checkHasReadAccess({
|
|
statusPageId: statusPageId,
|
|
req: req,
|
|
});
|
|
|
|
const resources: Array<StatusPageResource> =
|
|
await StatusPageResourceService.findBy({
|
|
query: {
|
|
statusPageId: statusPageId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
displayName: true,
|
|
order: true,
|
|
statusPageGroup: {
|
|
_id: true,
|
|
name: true,
|
|
order: true,
|
|
},
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
return Response.sendEntityArrayResponse(
|
|
req,
|
|
res,
|
|
resources,
|
|
new PositiveNumber(resources.length),
|
|
StatusPageResource,
|
|
);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/uptime/:statusPageId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
// This reosurce ID can be of a status page resource OR a status page group.
|
|
const statusPageResourceId: ObjectID = new ObjectID(
|
|
req.params["statusPageResourceId"] as string,
|
|
);
|
|
|
|
const statusPageId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
if (!statusPageId || !statusPageResourceId) {
|
|
throw new BadDataException("Status Page or Resource not found");
|
|
}
|
|
|
|
await this.checkHasReadAccess({
|
|
statusPageId: statusPageId,
|
|
req: req,
|
|
});
|
|
|
|
/*
|
|
* get start and end date from request body.
|
|
* if no end date is provided then it will be current date.
|
|
* if no start date is provided then it will be 14 days ago from end date.
|
|
*/
|
|
|
|
let startDate: Date = OneUptimeDate.getSomeDaysAgo(14);
|
|
let endDate: Date = OneUptimeDate.getCurrentDate();
|
|
|
|
if (req.body["startDate"]) {
|
|
startDate = OneUptimeDate.fromString(
|
|
req.body["startDate"] as string,
|
|
);
|
|
}
|
|
|
|
if (req.body["endDate"]) {
|
|
endDate = OneUptimeDate.fromString(req.body["endDate"] as string);
|
|
}
|
|
|
|
if (OneUptimeDate.isAfter(startDate, endDate)) {
|
|
throw new BadDataException("Start date cannot be after end date");
|
|
}
|
|
|
|
if (
|
|
OneUptimeDate.getDaysBetweenTwoDatesInclusive(startDate, endDate) >
|
|
90
|
|
) {
|
|
throw new BadDataException(
|
|
"You can only get uptime for 90 days. Please select a date range within 90 days.",
|
|
);
|
|
}
|
|
|
|
const {
|
|
monitorStatuses,
|
|
monitorGroupCurrentStatuses,
|
|
statusPageResources,
|
|
statusPage,
|
|
monitorStatusTimelines,
|
|
statusPageGroups,
|
|
monitorsInGroup,
|
|
} = await this.getStatusPageResourcesAndTimelines({
|
|
statusPageId: statusPageId,
|
|
startDateForMonitorTimeline: startDate,
|
|
endDateForMonitorTimeline: endDate,
|
|
});
|
|
|
|
const downtimeMonitorStatuses: Array<MonitorStatus> =
|
|
statusPage.downtimeMonitorStatuses || [];
|
|
|
|
type ResourceUptime = {
|
|
statusPageResourceId: ObjectID;
|
|
uptimePercent: number | null;
|
|
statusPageResourceName: string;
|
|
currentStatus: MonitorStatus | null;
|
|
};
|
|
|
|
type StatusPageGroupUptime = {
|
|
statusPageGroupId: ObjectID | null;
|
|
uptimePercent: number | null;
|
|
statusPageResourceUptimes: Array<ResourceUptime>;
|
|
statusPageGroupName: string | null;
|
|
currentStatus: MonitorStatus | null;
|
|
};
|
|
|
|
type GetUptimeByStatusPageGroup = (data: {
|
|
statusPageGroup: StatusPageGroup | null;
|
|
}) => StatusPageGroupUptime;
|
|
|
|
const getUptimeByStatusPageGroup: GetUptimeByStatusPageGroup =
|
|
(data: {
|
|
statusPageGroup: StatusPageGroup | null;
|
|
}): StatusPageGroupUptime => {
|
|
const groupUptime: StatusPageGroupUptime = {
|
|
statusPageGroupId:
|
|
data && data.statusPageGroup
|
|
? data.statusPageGroup?.id
|
|
: null,
|
|
uptimePercent: null,
|
|
statusPageResourceUptimes: [],
|
|
statusPageGroupName: data.statusPageGroup?.name || null,
|
|
currentStatus: null,
|
|
};
|
|
|
|
const group: StatusPageGroup | null = data.statusPageGroup;
|
|
|
|
for (const resource of statusPageResources) {
|
|
if (
|
|
(resource.statusPageGroupId &&
|
|
resource.statusPageGroupId.toString() &&
|
|
group &&
|
|
group._id?.toString() &&
|
|
group._id?.toString() ===
|
|
resource.statusPageGroupId.toString()) ||
|
|
(!resource.statusPageGroupId && !group)
|
|
) {
|
|
// if its not a monitor or a monitor group, then continue. This should ideally not happen.
|
|
|
|
if (!resource.monitor && !resource.monitorGroupId) {
|
|
continue;
|
|
}
|
|
|
|
const resourceUptime: ResourceUptime = {
|
|
statusPageResourceId: resource.id!,
|
|
uptimePercent: null,
|
|
statusPageResourceName:
|
|
resource.displayName || resource.monitor?.name || "",
|
|
currentStatus: null,
|
|
};
|
|
|
|
// if its a monitor
|
|
|
|
const precision: UptimePrecision =
|
|
resource.uptimePercentPrecision ||
|
|
UptimePrecision.ONE_DECIMAL;
|
|
|
|
if (resource.monitor) {
|
|
let currentStatus: MonitorStatus | undefined =
|
|
monitorStatuses.find((status: MonitorStatus) => {
|
|
return (
|
|
status._id?.toString() ===
|
|
resource.monitor?.currentMonitorStatusId?.toString()
|
|
);
|
|
});
|
|
|
|
if (!currentStatus) {
|
|
currentStatus = new MonitorStatus();
|
|
currentStatus.name = "Operational";
|
|
currentStatus.color = Green;
|
|
|
|
resourceUptime.currentStatus = currentStatus;
|
|
} else {
|
|
resourceUptime.currentStatus = currentStatus;
|
|
}
|
|
|
|
if (!resource.showCurrentStatus) {
|
|
resourceUptime.currentStatus = null;
|
|
}
|
|
|
|
const resourceStatusTimelines: Array<MonitorStatusTimeline> =
|
|
StatusPageResourceUptimeUtil.getMonitorStatusTimelineForResource(
|
|
{
|
|
statusPageResource: resource,
|
|
monitorStatusTimelines: monitorStatusTimelines,
|
|
monitorsInGroup: monitorsInGroup,
|
|
},
|
|
);
|
|
|
|
if (resource.showUptimePercent) {
|
|
const uptimePercent: number =
|
|
UptimeUtil.calculateUptimePercentage(
|
|
resourceStatusTimelines,
|
|
precision,
|
|
downtimeMonitorStatuses,
|
|
);
|
|
|
|
resourceUptime.uptimePercent = uptimePercent;
|
|
}
|
|
|
|
groupUptime.statusPageResourceUptimes.push(resourceUptime);
|
|
}
|
|
|
|
// if its a monitor group, then...
|
|
|
|
if (resource.monitorGroupId) {
|
|
let currentStatus: MonitorStatus | undefined =
|
|
monitorStatuses.find((status: MonitorStatus) => {
|
|
return (
|
|
status._id?.toString() ===
|
|
monitorGroupCurrentStatuses[
|
|
resource.monitorGroupId?.toString() || ""
|
|
]?.toString()
|
|
);
|
|
});
|
|
|
|
if (!currentStatus) {
|
|
currentStatus = new MonitorStatus();
|
|
currentStatus.name = "Operational";
|
|
currentStatus.color = Green;
|
|
|
|
resourceUptime.currentStatus = currentStatus;
|
|
} else {
|
|
resourceUptime.currentStatus = currentStatus;
|
|
}
|
|
|
|
if (!resource.showCurrentStatus) {
|
|
resourceUptime.currentStatus = null;
|
|
}
|
|
|
|
if (resource.showUptimePercent) {
|
|
const resourceStatusTimelines: Array<MonitorStatusTimeline> =
|
|
StatusPageResourceUptimeUtil.getMonitorStatusTimelineForResource(
|
|
{
|
|
statusPageResource: resource,
|
|
monitorStatusTimelines: monitorStatusTimelines,
|
|
monitorsInGroup: monitorsInGroup,
|
|
},
|
|
);
|
|
|
|
const uptimePercent: number =
|
|
UptimeUtil.calculateUptimePercentage(
|
|
resourceStatusTimelines,
|
|
precision,
|
|
downtimeMonitorStatuses,
|
|
);
|
|
|
|
resourceUptime.uptimePercent = uptimePercent;
|
|
}
|
|
|
|
groupUptime.statusPageResourceUptimes.push(resourceUptime);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (group?.showUptimePercent) {
|
|
// calculate uptime percent for the group.
|
|
const avgUptimePercent: number =
|
|
UptimeUtil.calculateAvgUptimePercentage({
|
|
uptimePercentages: groupUptime.statusPageResourceUptimes
|
|
.filter((resource: ResourceUptime) => {
|
|
return resource.uptimePercent !== null;
|
|
})
|
|
.map((resource: ResourceUptime) => {
|
|
return resource.uptimePercent || 0;
|
|
}),
|
|
precision:
|
|
group.uptimePercentPrecision ||
|
|
UptimePrecision.ONE_DECIMAL,
|
|
});
|
|
|
|
groupUptime.uptimePercent = avgUptimePercent;
|
|
}
|
|
|
|
if (group?.showCurrentStatus) {
|
|
const currentStatuses: Array<MonitorStatus> =
|
|
groupUptime.statusPageResourceUptimes
|
|
.filter((resourceUptime: ResourceUptime) => {
|
|
return resourceUptime.currentStatus !== null;
|
|
})
|
|
.map((resourceUptime: ResourceUptime) => {
|
|
return resourceUptime.currentStatus!;
|
|
});
|
|
|
|
const worstStatus: MonitorStatus | null =
|
|
StatusPageResourceUptimeUtil.getWorstMonitorStatus({
|
|
monitorStatuses: currentStatuses,
|
|
});
|
|
|
|
groupUptime.currentStatus = worstStatus;
|
|
}
|
|
|
|
return groupUptime;
|
|
};
|
|
|
|
const groupUptimes: Array<StatusPageGroupUptime> = [];
|
|
|
|
for (const group of statusPageGroups) {
|
|
groupUptimes.push(
|
|
getUptimeByStatusPageGroup({ statusPageGroup: group }),
|
|
);
|
|
}
|
|
|
|
return Response.sendJsonObjectResponse(req, res, {
|
|
statusPageResourceUptimes: [
|
|
...getUptimeByStatusPageGroup({ statusPageGroup: null })
|
|
.statusPageResourceUptimes,
|
|
],
|
|
groupUptimes: groupUptimes,
|
|
startDate: startDate,
|
|
endDate: endDate,
|
|
});
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/overview/:statusPageId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
const statusPageId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
await this.checkHasReadAccess({
|
|
statusPageId: statusPageId,
|
|
req: req,
|
|
});
|
|
|
|
const startDate: Date = OneUptimeDate.getSomeDaysAgo(90);
|
|
const endDate: Date = OneUptimeDate.getCurrentDate();
|
|
|
|
const {
|
|
monitorStatuses,
|
|
monitorGroupCurrentStatuses,
|
|
statusPageResources,
|
|
statusPage,
|
|
monitorsOnStatusPage,
|
|
monitorStatusTimelines,
|
|
statusPageGroups,
|
|
monitorsInGroup,
|
|
} = await this.getStatusPageResourcesAndTimelines({
|
|
statusPageId: statusPageId,
|
|
startDateForMonitorTimeline: startDate,
|
|
endDateForMonitorTimeline: endDate,
|
|
});
|
|
|
|
// check if status page has active incident.
|
|
let activeIncidents: Array<Incident> = [];
|
|
if (monitorsOnStatusPage.length > 0) {
|
|
let select: Select<Incident> = {
|
|
createdAt: true,
|
|
declaredAt: true,
|
|
updatedAt: true,
|
|
title: true,
|
|
description: true,
|
|
_id: true,
|
|
postmortemNote: true,
|
|
postmortemPostedAt: true,
|
|
showPostmortemOnStatusPage: true,
|
|
postmortemAttachments: {
|
|
_id: true,
|
|
name: true,
|
|
},
|
|
incidentSeverity: {
|
|
name: true,
|
|
color: true,
|
|
},
|
|
currentIncidentState: {
|
|
_id: true,
|
|
name: true,
|
|
color: true,
|
|
order: true,
|
|
},
|
|
monitors: {
|
|
_id: true,
|
|
},
|
|
};
|
|
|
|
if (statusPage.showIncidentLabelsOnStatusPage) {
|
|
select = {
|
|
...select,
|
|
labels: {
|
|
name: true,
|
|
color: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
const unresolvedIncidentStates: Array<IncidentState> =
|
|
await IncidentStateService.getUnresolvedIncidentStates(
|
|
statusPage.projectId!,
|
|
{
|
|
isRoot: true,
|
|
},
|
|
);
|
|
|
|
const unresolvedIncidentStateIds: Array<ObjectID> =
|
|
unresolvedIncidentStates.map((state: IncidentState) => {
|
|
return state.id!;
|
|
});
|
|
|
|
if (statusPage.showIncidentsOnStatusPage) {
|
|
activeIncidents = await IncidentService.findBy({
|
|
query: {
|
|
monitors: monitorsOnStatusPage as any,
|
|
currentIncidentStateId: QueryHelper.any(
|
|
unresolvedIncidentStateIds,
|
|
),
|
|
isVisibleOnStatusPage: true,
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: select,
|
|
sort: {
|
|
declaredAt: SortOrder.Descending,
|
|
createdAt: SortOrder.Descending,
|
|
},
|
|
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
const incidentsOnStatusPage: Array<ObjectID> = activeIncidents.map(
|
|
(incident: Incident) => {
|
|
return incident.id!;
|
|
},
|
|
);
|
|
|
|
let incidentPublicNotes: Array<IncidentPublicNote> = [];
|
|
|
|
if (incidentsOnStatusPage.length > 0) {
|
|
incidentPublicNotes = await IncidentPublicNoteService.findBy({
|
|
query: {
|
|
incidentId: QueryHelper.any(incidentsOnStatusPage),
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
note: true,
|
|
incidentId: true,
|
|
postedAt: true,
|
|
attachments: {
|
|
_id: true,
|
|
name: true,
|
|
},
|
|
},
|
|
sort: {
|
|
postedAt: SortOrder.Descending, // new note first
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
let incidentStateTimelines: Array<IncidentStateTimeline> = [];
|
|
|
|
if (incidentsOnStatusPage.length > 0) {
|
|
incidentStateTimelines = await IncidentStateTimelineService.findBy({
|
|
query: {
|
|
incidentId: QueryHelper.any(incidentsOnStatusPage),
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
createdAt: true,
|
|
startsAt: true,
|
|
incidentId: true,
|
|
incidentState: {
|
|
_id: true,
|
|
name: true,
|
|
color: true,
|
|
isCreatedState: true,
|
|
isResolvedState: true,
|
|
isAcknowledgedState: true,
|
|
},
|
|
},
|
|
|
|
sort: {
|
|
startsAt: SortOrder.Descending, // newer state changes first
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
// check if status page has active announcement.
|
|
|
|
const today: Date = OneUptimeDate.getCurrentDate();
|
|
|
|
let activeAnnouncements: Array<StatusPageAnnouncement> = [];
|
|
|
|
if (statusPage.showAnnouncementsOnStatusPage) {
|
|
activeAnnouncements = await StatusPageAnnouncementService.findBy({
|
|
query: {
|
|
statusPages: statusPageId as any,
|
|
showAnnouncementAt: QueryHelper.lessThan(today),
|
|
endAnnouncementAt: QueryHelper.greaterThanOrNull(today),
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
createdAt: true,
|
|
title: true,
|
|
description: true,
|
|
_id: true,
|
|
showAnnouncementAt: true,
|
|
endAnnouncementAt: true,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
// check if status page has active scheduled events.
|
|
|
|
let scheduledEventsSelect: Select<ScheduledMaintenance> = {
|
|
createdAt: true,
|
|
title: true,
|
|
description: true,
|
|
_id: true,
|
|
endsAt: true,
|
|
startsAt: true,
|
|
currentScheduledMaintenanceState: {
|
|
name: true,
|
|
color: true,
|
|
isScheduledState: true,
|
|
isResolvedState: true,
|
|
isOngoingState: true,
|
|
},
|
|
monitors: {
|
|
_id: true,
|
|
},
|
|
};
|
|
|
|
if (statusPage.showScheduledEventLabelsOnStatusPage) {
|
|
scheduledEventsSelect = {
|
|
...scheduledEventsSelect,
|
|
labels: {
|
|
name: true,
|
|
color: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
let scheduledMaintenanceEvents: Array<ScheduledMaintenance> = [];
|
|
|
|
if (statusPage.showScheduledMaintenanceEventsOnStatusPage) {
|
|
scheduledMaintenanceEvents =
|
|
await ScheduledMaintenanceService.findBy({
|
|
query: {
|
|
currentScheduledMaintenanceState: {
|
|
isOngoingState: true,
|
|
} as any,
|
|
statusPages: statusPageId as any,
|
|
projectId: statusPage.projectId!,
|
|
isVisibleOnStatusPage: true,
|
|
},
|
|
select: scheduledEventsSelect,
|
|
sort: {
|
|
startsAt: SortOrder.Ascending,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
let futureScheduledMaintenanceEvents: Array<ScheduledMaintenance> =
|
|
[];
|
|
|
|
if (statusPage.showScheduledMaintenanceEventsOnStatusPage) {
|
|
futureScheduledMaintenanceEvents =
|
|
await ScheduledMaintenanceService.findBy({
|
|
query: {
|
|
currentScheduledMaintenanceState: {
|
|
isScheduledState: true,
|
|
} as any,
|
|
statusPages: statusPageId as any,
|
|
projectId: statusPage.projectId!,
|
|
isVisibleOnStatusPage: true,
|
|
},
|
|
select: scheduledEventsSelect,
|
|
sort: {
|
|
startsAt: SortOrder.Ascending,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
futureScheduledMaintenanceEvents.forEach(
|
|
(event: ScheduledMaintenance) => {
|
|
scheduledMaintenanceEvents.push(event);
|
|
},
|
|
);
|
|
|
|
const scheduledMaintenanceEventsOnStatusPage: Array<ObjectID> =
|
|
scheduledMaintenanceEvents.map((event: ScheduledMaintenance) => {
|
|
return event.id!;
|
|
});
|
|
|
|
let scheduledMaintenanceEventsPublicNotes: Array<ScheduledMaintenancePublicNote> =
|
|
[];
|
|
|
|
if (scheduledMaintenanceEventsOnStatusPage.length > 0) {
|
|
scheduledMaintenanceEventsPublicNotes =
|
|
await ScheduledMaintenancePublicNoteService.findBy({
|
|
query: {
|
|
scheduledMaintenanceId: QueryHelper.any(
|
|
scheduledMaintenanceEventsOnStatusPage,
|
|
),
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
postedAt: true,
|
|
note: true,
|
|
scheduledMaintenanceId: true,
|
|
attachments: {
|
|
_id: true,
|
|
name: true,
|
|
},
|
|
},
|
|
sort: {
|
|
postedAt: SortOrder.Ascending,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
let scheduledMaintenanceStateTimelines: Array<ScheduledMaintenanceStateTimeline> =
|
|
[];
|
|
|
|
if (scheduledMaintenanceEventsOnStatusPage.length > 0) {
|
|
scheduledMaintenanceStateTimelines =
|
|
await ScheduledMaintenanceStateTimelineService.findBy({
|
|
query: {
|
|
scheduledMaintenanceId: QueryHelper.any(
|
|
scheduledMaintenanceEventsOnStatusPage,
|
|
),
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
createdAt: true,
|
|
startsAt: true,
|
|
scheduledMaintenanceId: true,
|
|
scheduledMaintenanceState: {
|
|
_id: true,
|
|
color: true,
|
|
name: true,
|
|
isScheduledState: true,
|
|
isResolvedState: true,
|
|
isOngoingState: true,
|
|
},
|
|
},
|
|
|
|
sort: {
|
|
startsAt: SortOrder.Descending, // newer state changes first
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
// get all status page bar chart rules
|
|
const statusPageHistoryChartBarColorRules: Array<StatusPageHistoryChartBarColorRule> =
|
|
await StatusPageHistoryChartBarColorRuleService.findBy({
|
|
query: {
|
|
statusPageId: statusPageId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
barColor: true,
|
|
order: true,
|
|
statusPageId: true,
|
|
uptimePercentGreaterThanOrEqualTo: true,
|
|
},
|
|
sort: {
|
|
order: SortOrder.Ascending,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const overallStatus: MonitorStatus | null =
|
|
StatusPageService.getOverallMonitorStatus({
|
|
statusPageResources,
|
|
monitorStatuses,
|
|
monitorGroupCurrentStatuses,
|
|
});
|
|
|
|
const response: JSONObject = {
|
|
overallStatus: overallStatus
|
|
? BaseModel.toJSON(overallStatus, MonitorStatus)
|
|
: null,
|
|
|
|
scheduledMaintenanceEventsPublicNotes: BaseModel.toJSONArray(
|
|
scheduledMaintenanceEventsPublicNotes,
|
|
ScheduledMaintenancePublicNote,
|
|
),
|
|
statusPageHistoryChartBarColorRules: BaseModel.toJSONArray(
|
|
statusPageHistoryChartBarColorRules,
|
|
StatusPageHistoryChartBarColorRule,
|
|
),
|
|
scheduledMaintenanceEvents: BaseModel.toJSONArray(
|
|
scheduledMaintenanceEvents,
|
|
ScheduledMaintenance,
|
|
),
|
|
activeAnnouncements: BaseModel.toJSONArray(
|
|
activeAnnouncements,
|
|
StatusPageAnnouncement,
|
|
),
|
|
incidentPublicNotes: BaseModel.toJSONArray(
|
|
incidentPublicNotes,
|
|
IncidentPublicNote,
|
|
),
|
|
|
|
activeIncidents: BaseModel.toJSONArray(activeIncidents, Incident),
|
|
|
|
monitorStatusTimelines: BaseModel.toJSONArray(
|
|
monitorStatusTimelines,
|
|
MonitorStatusTimeline,
|
|
),
|
|
resourceGroups: BaseModel.toJSONArray(
|
|
statusPageGroups,
|
|
StatusPageGroup,
|
|
),
|
|
monitorStatuses: BaseModel.toJSONArray(
|
|
monitorStatuses,
|
|
MonitorStatus,
|
|
),
|
|
statusPageResources: BaseModel.toJSONArray(
|
|
statusPageResources,
|
|
StatusPageResource,
|
|
),
|
|
incidentStateTimelines: BaseModel.toJSONArray(
|
|
incidentStateTimelines,
|
|
IncidentStateTimeline,
|
|
),
|
|
statusPage: BaseModel.toJSONObject(statusPage, StatusPage),
|
|
scheduledMaintenanceStateTimelines: BaseModel.toJSONArray(
|
|
scheduledMaintenanceStateTimelines,
|
|
ScheduledMaintenanceStateTimeline,
|
|
),
|
|
|
|
monitorGroupCurrentStatuses: JSONFunctions.serialize(
|
|
monitorGroupCurrentStatuses,
|
|
),
|
|
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
|
};
|
|
|
|
return Response.sendJsonObjectResponse(req, res, response);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.put(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/update-subscription/:statusPageId/:subscriberId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
await this.subscribeToStatusPage(req);
|
|
return Response.sendEmptySuccessResponse(req, res);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/get-subscription/:statusPageId/:subscriberId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
const subscriber: StatusPageSubscriber =
|
|
await this.getSubscriber(req);
|
|
|
|
return Response.sendEntityResponse(
|
|
req,
|
|
res,
|
|
subscriber,
|
|
StatusPageSubscriber,
|
|
);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/subscribe/:statusPageId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
await this.subscribeToStatusPage(req);
|
|
|
|
return Response.sendEmptySuccessResponse(req, res);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/manage-subscription/:statusPageId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
await this.manageExistingSubscription(req);
|
|
|
|
return Response.sendEmptySuccessResponse(req, res);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/incidents/:statusPageId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
const objectId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
const response: JSONObject = await this.getIncidents(
|
|
objectId,
|
|
null,
|
|
req,
|
|
);
|
|
|
|
return Response.sendJsonObjectResponse(req, res, response);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/scheduled-maintenance-events/:statusPageId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
const objectId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
const response: JSONObject = await this.getScheduledMaintenanceEvents(
|
|
objectId,
|
|
null,
|
|
|
|
req,
|
|
);
|
|
|
|
return Response.sendJsonObjectResponse(req, res, response);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/announcements/:statusPageId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
const objectId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
const response: JSONObject = await this.getAnnouncements(
|
|
objectId,
|
|
null,
|
|
|
|
req,
|
|
);
|
|
|
|
return Response.sendJsonObjectResponse(req, res, response);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/incidents/:statusPageId/:incidentId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
const objectId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
const incidentId: ObjectID = new ObjectID(
|
|
req.params["incidentId"] as string,
|
|
);
|
|
|
|
const response: JSONObject = await this.getIncidents(
|
|
objectId,
|
|
incidentId,
|
|
req,
|
|
);
|
|
|
|
return Response.sendJsonObjectResponse(req, res, response);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/scheduled-maintenance-events/:statusPageId/:scheduledMaintenanceId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
const objectId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
const scheduledMaintenanceId: ObjectID = new ObjectID(
|
|
req.params["scheduledMaintenanceId"] as string,
|
|
);
|
|
|
|
const response: JSONObject = await this.getScheduledMaintenanceEvents(
|
|
objectId,
|
|
scheduledMaintenanceId,
|
|
|
|
req,
|
|
);
|
|
|
|
return Response.sendJsonObjectResponse(req, res, response);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.router.post(
|
|
`${new this.entityType()
|
|
.getCrudApiPath()
|
|
?.toString()}/announcements/:statusPageId/:announcementId`,
|
|
UserMiddleware.getUserMiddleware,
|
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
try {
|
|
const objectId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
const announcementId: ObjectID = new ObjectID(
|
|
req.params["announcementId"] as string,
|
|
);
|
|
|
|
const response: JSONObject = await this.getAnnouncements(
|
|
objectId,
|
|
announcementId,
|
|
|
|
req,
|
|
);
|
|
|
|
return Response.sendJsonObjectResponse(req, res, response);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async getScheduledMaintenanceEvents(
|
|
statusPageId: ObjectID,
|
|
scheduledMaintenanceId: ObjectID | null,
|
|
req: ExpressRequest,
|
|
): Promise<JSONObject> {
|
|
await this.checkHasReadAccess({
|
|
statusPageId: statusPageId,
|
|
req: req,
|
|
});
|
|
|
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: statusPageId.toString(),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
showScheduledEventHistoryInDays: true,
|
|
showScheduledEventLabelsOnStatusPage: true,
|
|
showScheduledMaintenanceEventsOnStatusPage: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage) {
|
|
throw new BadDataException("Status Page not found");
|
|
}
|
|
|
|
if (!statusPage.showScheduledMaintenanceEventsOnStatusPage) {
|
|
throw new BadDataException(
|
|
"Scheduled Maintenance Events are not enabled on this status page",
|
|
);
|
|
}
|
|
|
|
// get monitors on status page.
|
|
const statusPageResources: Array<StatusPageResource> =
|
|
await StatusPageService.getStatusPageResources({
|
|
statusPageId: statusPageId,
|
|
});
|
|
|
|
// check if status page has active scheduled events.
|
|
const today: Date = OneUptimeDate.getCurrentDate();
|
|
const historyDays: Date = OneUptimeDate.getSomeDaysAgo(
|
|
statusPage.showScheduledEventHistoryInDays || 14,
|
|
);
|
|
|
|
let query: Query<ScheduledMaintenance> = {
|
|
startsAt: QueryHelper.inBetween(historyDays, today),
|
|
statusPages: [statusPageId] as any,
|
|
projectId: statusPage.projectId!,
|
|
isVisibleOnStatusPage: true,
|
|
};
|
|
|
|
if (scheduledMaintenanceId) {
|
|
query = {
|
|
_id: scheduledMaintenanceId.toString(),
|
|
statusPages: [statusPageId] as any,
|
|
projectId: statusPage.projectId!,
|
|
};
|
|
}
|
|
|
|
let scheduledEventsSelect: Select<ScheduledMaintenance> = {
|
|
createdAt: true,
|
|
title: true,
|
|
description: true,
|
|
_id: true,
|
|
endsAt: true,
|
|
startsAt: true,
|
|
currentScheduledMaintenanceState: {
|
|
name: true,
|
|
color: true,
|
|
isScheduledState: true,
|
|
isResolvedState: true,
|
|
isOngoingState: true,
|
|
order: true,
|
|
},
|
|
monitors: {
|
|
_id: true,
|
|
},
|
|
};
|
|
|
|
if (statusPage.showScheduledEventLabelsOnStatusPage) {
|
|
scheduledEventsSelect = {
|
|
...scheduledEventsSelect,
|
|
labels: {
|
|
name: true,
|
|
color: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
const scheduledMaintenanceEvents: Array<ScheduledMaintenance> =
|
|
await ScheduledMaintenanceService.findBy({
|
|
query: query,
|
|
select: scheduledEventsSelect,
|
|
sort: {
|
|
startsAt: SortOrder.Descending,
|
|
},
|
|
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let futureScheduledMaintenanceEvents: Array<ScheduledMaintenance> = [];
|
|
|
|
// If there is no scheduledMaintenanceId, then fetch all future scheduled events.
|
|
if (!scheduledMaintenanceId) {
|
|
futureScheduledMaintenanceEvents =
|
|
await ScheduledMaintenanceService.findBy({
|
|
query: {
|
|
currentScheduledMaintenanceState: {
|
|
isScheduledState: true,
|
|
} as any,
|
|
statusPages: [statusPageId] as any,
|
|
projectId: statusPage.projectId!,
|
|
isVisibleOnStatusPage: true,
|
|
},
|
|
select: scheduledEventsSelect,
|
|
sort: {
|
|
createdAt: SortOrder.Ascending,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
futureScheduledMaintenanceEvents.forEach(
|
|
(event: ScheduledMaintenance) => {
|
|
scheduledMaintenanceEvents.push(event);
|
|
},
|
|
);
|
|
}
|
|
|
|
const scheduledMaintenanceEventsOnStatusPage: Array<ObjectID> =
|
|
scheduledMaintenanceEvents.map((event: ScheduledMaintenance) => {
|
|
return event.id!;
|
|
});
|
|
|
|
let scheduledMaintenanceEventsPublicNotes: Array<ScheduledMaintenancePublicNote> =
|
|
[];
|
|
|
|
if (scheduledMaintenanceEventsOnStatusPage.length > 0) {
|
|
scheduledMaintenanceEventsPublicNotes =
|
|
await ScheduledMaintenancePublicNoteService.findBy({
|
|
query: {
|
|
scheduledMaintenanceId: QueryHelper.any(
|
|
scheduledMaintenanceEventsOnStatusPage,
|
|
),
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
postedAt: true,
|
|
note: true,
|
|
scheduledMaintenanceId: true,
|
|
attachments: {
|
|
_id: true,
|
|
name: true,
|
|
},
|
|
},
|
|
sort: {
|
|
postedAt: SortOrder.Ascending,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
let scheduledMaintenanceStateTimelines: Array<ScheduledMaintenanceStateTimeline> =
|
|
[];
|
|
|
|
if (scheduledMaintenanceEventsOnStatusPage.length > 0) {
|
|
scheduledMaintenanceStateTimelines =
|
|
await ScheduledMaintenanceStateTimelineService.findBy({
|
|
query: {
|
|
scheduledMaintenanceId: QueryHelper.any(
|
|
scheduledMaintenanceEventsOnStatusPage,
|
|
),
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
createdAt: true,
|
|
startsAt: true,
|
|
scheduledMaintenanceId: true,
|
|
scheduledMaintenanceState: {
|
|
name: true,
|
|
color: true,
|
|
isScheduledState: true,
|
|
isResolvedState: true,
|
|
isOngoingState: true,
|
|
},
|
|
},
|
|
|
|
sort: {
|
|
startsAt: SortOrder.Descending, // newer state changes first
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
const monitorGroupIds: Array<ObjectID> = statusPageResources
|
|
.map((resource: StatusPageResource) => {
|
|
return resource.monitorGroupId!;
|
|
})
|
|
.filter((id: ObjectID) => {
|
|
return Boolean(id); // remove nulls
|
|
});
|
|
|
|
// get monitors in the group.
|
|
const monitorsInGroup: Dictionary<Array<ObjectID>> = {};
|
|
|
|
// get monitor status charts.
|
|
const monitorsOnStatusPage: Array<ObjectID> = statusPageResources
|
|
.map((monitor: StatusPageResource) => {
|
|
return monitor.monitorId!;
|
|
})
|
|
.filter((id: ObjectID) => {
|
|
return Boolean(id); // remove nulls
|
|
});
|
|
|
|
for (const monitorGroupId of monitorGroupIds) {
|
|
// get monitors in the group.
|
|
|
|
const groupResources: Array<MonitorGroupResource> =
|
|
await MonitorGroupResourceService.findBy({
|
|
query: {
|
|
monitorGroupId: monitorGroupId,
|
|
},
|
|
select: {
|
|
monitorId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
});
|
|
|
|
const monitorsInGroupIds: Array<ObjectID> = groupResources
|
|
.map((resource: MonitorGroupResource) => {
|
|
return resource.monitorId!;
|
|
})
|
|
.filter((id: ObjectID) => {
|
|
return Boolean(id); // remove nulls
|
|
});
|
|
|
|
for (const monitorId of monitorsInGroupIds) {
|
|
if (
|
|
!monitorsOnStatusPage.find((item: ObjectID) => {
|
|
return item.toString() === monitorId.toString();
|
|
})
|
|
) {
|
|
monitorsOnStatusPage.push(monitorId);
|
|
}
|
|
}
|
|
|
|
monitorsInGroup[monitorGroupId.toString()] = monitorsInGroupIds;
|
|
}
|
|
|
|
// get scheduled event states.
|
|
const scheduledEventStates: Array<ScheduledMaintenanceState> =
|
|
await ScheduledMaintenanceStateService.findBy({
|
|
query: {
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
order: true,
|
|
isEndedState: true,
|
|
isOngoingState: true,
|
|
isScheduledState: true,
|
|
},
|
|
});
|
|
|
|
const response: JSONObject = {
|
|
scheduledMaintenanceEventsPublicNotes: BaseModel.toJSONArray(
|
|
scheduledMaintenanceEventsPublicNotes,
|
|
ScheduledMaintenancePublicNote,
|
|
),
|
|
scheduledMaintenanceStates: BaseModel.toJSONArray(
|
|
scheduledEventStates,
|
|
ScheduledMaintenanceState,
|
|
),
|
|
scheduledMaintenanceEvents: BaseModel.toJSONArray(
|
|
scheduledMaintenanceEvents,
|
|
ScheduledMaintenance,
|
|
),
|
|
statusPageResources: BaseModel.toJSONArray(
|
|
statusPageResources,
|
|
StatusPageResource,
|
|
),
|
|
scheduledMaintenanceStateTimelines: BaseModel.toJSONArray(
|
|
scheduledMaintenanceStateTimelines,
|
|
ScheduledMaintenanceStateTimeline,
|
|
),
|
|
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
|
};
|
|
|
|
return response;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async getAnnouncements(
|
|
statusPageId: ObjectID,
|
|
announcementId: ObjectID | null,
|
|
req: ExpressRequest,
|
|
): Promise<JSONObject> {
|
|
await this.checkHasReadAccess({
|
|
statusPageId: statusPageId,
|
|
req: req,
|
|
});
|
|
|
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: statusPageId.toString(),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
showAnnouncementHistoryInDays: true,
|
|
showAnnouncementsOnStatusPage: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage) {
|
|
throw new BadDataException("Status Page not found");
|
|
}
|
|
|
|
if (!statusPage.showAnnouncementsOnStatusPage) {
|
|
throw new BadDataException(
|
|
"Announcements are not enabled for this status page.",
|
|
);
|
|
}
|
|
|
|
// check if status page has active announcement.
|
|
|
|
const today: Date = OneUptimeDate.getCurrentDate();
|
|
const historyDays: Date = OneUptimeDate.getSomeDaysAgo(
|
|
statusPage.showAnnouncementHistoryInDays || 14,
|
|
);
|
|
|
|
let query: Query<StatusPageAnnouncement> = {
|
|
statusPages: [statusPageId] as any,
|
|
showAnnouncementAt: QueryHelper.inBetween(historyDays, today),
|
|
projectId: statusPage.projectId!,
|
|
};
|
|
|
|
if (announcementId) {
|
|
query = {
|
|
statusPages: [statusPageId] as any,
|
|
_id: announcementId.toString(),
|
|
projectId: statusPage.projectId!,
|
|
};
|
|
}
|
|
|
|
const announcements: Array<StatusPageAnnouncement> =
|
|
await StatusPageAnnouncementService.findBy({
|
|
query: query,
|
|
select: {
|
|
createdAt: true,
|
|
title: true,
|
|
description: true,
|
|
_id: true,
|
|
showAnnouncementAt: true,
|
|
endAnnouncementAt: true,
|
|
monitors: {
|
|
_id: true,
|
|
name: true,
|
|
},
|
|
attachments: {
|
|
_id: true,
|
|
name: true,
|
|
},
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// get monitors on status page.
|
|
const statusPageResources: Array<StatusPageResource> =
|
|
await StatusPageResourceService.findBy({
|
|
query: {
|
|
statusPageId: statusPageId,
|
|
},
|
|
select: {
|
|
statusPageGroupId: true,
|
|
monitorId: true,
|
|
displayTooltip: true,
|
|
displayDescription: true,
|
|
displayName: true,
|
|
monitorGroupId: true,
|
|
monitor: {
|
|
_id: true,
|
|
currentMonitorStatusId: true,
|
|
},
|
|
},
|
|
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const monitorGroupIds: Array<ObjectID> = statusPageResources
|
|
.map((resource: StatusPageResource) => {
|
|
return resource.monitorGroupId!;
|
|
})
|
|
.filter((id: ObjectID) => {
|
|
return Boolean(id); // remove nulls
|
|
});
|
|
|
|
// get monitors in the group.
|
|
const monitorsInGroup: Dictionary<Array<ObjectID>> = {};
|
|
|
|
// get monitor status charts.
|
|
const monitorsOnStatusPage: Array<ObjectID> = statusPageResources
|
|
.map((monitor: StatusPageResource) => {
|
|
return monitor.monitorId!;
|
|
})
|
|
.filter((id: ObjectID) => {
|
|
return Boolean(id); // remove nulls
|
|
});
|
|
|
|
for (const monitorGroupId of monitorGroupIds) {
|
|
// get monitors in the group.
|
|
|
|
const groupResources: Array<MonitorGroupResource> =
|
|
await MonitorGroupResourceService.findBy({
|
|
query: {
|
|
monitorGroupId: monitorGroupId,
|
|
},
|
|
select: {
|
|
monitorId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
});
|
|
|
|
const monitorsInGroupIds: Array<ObjectID> = groupResources
|
|
.map((resource: MonitorGroupResource) => {
|
|
return resource.monitorId!;
|
|
})
|
|
.filter((id: ObjectID) => {
|
|
return Boolean(id); // remove nulls
|
|
});
|
|
|
|
for (const monitorId of monitorsInGroupIds) {
|
|
if (
|
|
!monitorsOnStatusPage.find((item: ObjectID) => {
|
|
return item.toString() === monitorId.toString();
|
|
})
|
|
) {
|
|
monitorsOnStatusPage.push(monitorId);
|
|
}
|
|
}
|
|
|
|
monitorsInGroup[monitorGroupId.toString()] = monitorsInGroupIds;
|
|
}
|
|
|
|
const response: JSONObject = {
|
|
announcements: BaseModel.toJSONArray(
|
|
announcements,
|
|
StatusPageAnnouncement,
|
|
),
|
|
statusPageResources: BaseModel.toJSONArray(
|
|
statusPageResources,
|
|
StatusPageResource,
|
|
),
|
|
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
|
};
|
|
|
|
return response;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async manageExistingSubscription(req: ExpressRequest): Promise<void> {
|
|
const statusPageId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
logger.debug(
|
|
`Managing Existing Subscription for Status Page: ${statusPageId}`,
|
|
);
|
|
|
|
await this.checkHasReadAccess({
|
|
statusPageId: statusPageId,
|
|
req: req,
|
|
});
|
|
|
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: statusPageId.toString(),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
enableEmailSubscribers: true,
|
|
enableSlackSubscribers: true,
|
|
enableMicrosoftTeamsSubscribers: true,
|
|
enableSmsSubscribers: true,
|
|
allowSubscribersToChooseResources: true,
|
|
allowSubscribersToChooseEventTypes: true,
|
|
showSubscriberPageOnStatusPage: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage) {
|
|
logger.debug(`Status page not found with ID: ${statusPageId}`);
|
|
throw new BadDataException("Status Page not found");
|
|
}
|
|
|
|
if (!statusPage.showSubscriberPageOnStatusPage) {
|
|
logger.debug(
|
|
`Subscriber page not enabled for status page with ID: ${statusPageId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"Subscribes not enabled for this status page.",
|
|
);
|
|
}
|
|
|
|
logger.debug(`Status page found: ${JSON.stringify(statusPage)}`);
|
|
|
|
if (
|
|
req.body.data["subscriberEmail"] &&
|
|
!statusPage.enableEmailSubscribers
|
|
) {
|
|
logger.debug(
|
|
`Email subscribers not enabled for status page with ID: ${statusPageId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"Email subscribers not enabled for this status page.",
|
|
);
|
|
}
|
|
|
|
if (
|
|
req.body.data["slackIncomingWebhookUrl"] &&
|
|
!statusPage.enableSlackSubscribers
|
|
) {
|
|
logger.debug(
|
|
`Slack subscribers not enabled for status page with ID: ${statusPageId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"Slack subscribers not enabled for this status page.",
|
|
);
|
|
}
|
|
|
|
if (req.body.data["subscriberPhone"] && !statusPage.enableSmsSubscribers) {
|
|
logger.debug(
|
|
`SMS subscribers not enabled for status page with ID: ${statusPageId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"SMS subscribers not enabled for this status page.",
|
|
);
|
|
}
|
|
|
|
// if no email or phone, throw error.
|
|
|
|
if (
|
|
!req.body.data["subscriberEmail"] &&
|
|
!req.body.data["subscriberPhone"] &&
|
|
!req.body.data["slackWorkspaceName"]
|
|
) {
|
|
logger.debug(
|
|
`No email, slack workspace name or phone provided for subscription to status page with ID: ${statusPageId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"Email, phone or slack workspace name is required to subscribe to this status page.",
|
|
);
|
|
}
|
|
|
|
const email: Email | undefined = req.body.data["subscriberEmail"]
|
|
? new Email(req.body.data["subscriberEmail"] as string)
|
|
: undefined;
|
|
|
|
const phone: Phone | undefined = req.body.data["subscriberPhone"]
|
|
? new Phone(req.body.data["subscriberPhone"] as string)
|
|
: undefined;
|
|
|
|
const slackWorkspaceName: string | undefined = req.body.data[
|
|
"slackWorkspaceName"
|
|
]
|
|
? (req.body.data["slackWorkspaceName"] as string)
|
|
: undefined;
|
|
|
|
let statusPageSubscriber: StatusPageSubscriber | null = null;
|
|
|
|
if (email) {
|
|
logger.debug(`Setting subscriber email: ${email}`);
|
|
statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
|
|
query: {
|
|
subscriberEmail: email,
|
|
statusPageId: statusPageId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
subscriberEmail: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (phone) {
|
|
logger.debug(`Setting subscriber phone: ${phone}`);
|
|
statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
|
|
query: {
|
|
subscriberPhone: phone,
|
|
statusPageId: statusPageId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
subscriberPhone: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (slackWorkspaceName) {
|
|
logger.debug(`Setting subscriber slack workspace: ${slackWorkspaceName}`);
|
|
statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
|
|
query: {
|
|
slackWorkspaceName: slackWorkspaceName,
|
|
statusPageId: statusPageId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
slackWorkspaceName: true,
|
|
slackIncomingWebhookUrl: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (!statusPageSubscriber) {
|
|
// not found, return bad data
|
|
logger.debug(
|
|
`Subscriber not found for email: ${email}, phone: ${phone}, or slack workspace: ${slackWorkspaceName}`,
|
|
);
|
|
|
|
let identifierType: string = "email";
|
|
if (phone) {
|
|
identifierType = "phone";
|
|
} else if (slackWorkspaceName) {
|
|
identifierType = "slack workspace name";
|
|
}
|
|
|
|
throw new BadDataException(
|
|
`Subscription not found for this status page. Please make sure your ${identifierType} is correct.`,
|
|
);
|
|
}
|
|
|
|
const statusPageURL: string =
|
|
await StatusPageService.getStatusPageURL(statusPageId);
|
|
|
|
const manageUrlink: string = StatusPageSubscriberService.getUnsubscribeLink(
|
|
URL.fromString(statusPageURL),
|
|
statusPageSubscriber.id!,
|
|
).toString();
|
|
|
|
const statusPages: Array<StatusPage> =
|
|
await StatusPageSubscriberService.getStatusPagesToSendNotification([
|
|
statusPageId,
|
|
]);
|
|
|
|
for (const statusPage of statusPages) {
|
|
// send email to subscriber or sms if phone is provided.
|
|
|
|
if (email) {
|
|
const host: Hostname = await DatabaseConfig.getHost();
|
|
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
const statusPageIdString: string | null =
|
|
statusPage.id?.toString() || statusPage._id?.toString() || null;
|
|
|
|
MailService.sendMail(
|
|
{
|
|
toEmail: email,
|
|
templateType:
|
|
EmailTemplateType.ManageExistingStatusPageSubscriberSubscription,
|
|
vars: {
|
|
statusPageName: statusPage.name || "Status Page",
|
|
statusPageUrl: statusPageURL,
|
|
logoUrl:
|
|
statusPage.logoFileId && statusPageIdString
|
|
? new URL(httpProtocol, host)
|
|
.addRoute(StatusPageApiRoute)
|
|
.addRoute(`/logo/${statusPageIdString}`)
|
|
.toString()
|
|
: "",
|
|
isPublicStatusPage: statusPage.isPublicStatusPage
|
|
? "true"
|
|
: "false",
|
|
subscriberEmailNotificationFooterText:
|
|
StatusPageServiceType.getSubscriberEmailFooterText(statusPage),
|
|
|
|
manageSubscriptionUrl: manageUrlink,
|
|
},
|
|
subject:
|
|
"Manage your Subscription for " +
|
|
(statusPage.name || "Status Page"),
|
|
},
|
|
{
|
|
mailServer: ProjectSmtpConfigService.toEmailServer(
|
|
statusPage.smtpConfig,
|
|
),
|
|
projectId: statusPage.projectId!,
|
|
statusPageId: statusPage.id!,
|
|
},
|
|
);
|
|
}
|
|
|
|
if (phone) {
|
|
const sms: SMS = {
|
|
message: `You have selected to manage your subscription for the status page: ${statusPage.name}. You can manage your subscription here: ${manageUrlink}`,
|
|
to: phone,
|
|
};
|
|
// send sms here.
|
|
SmsService.sendSms(sms, {
|
|
projectId: statusPage.projectId,
|
|
customTwilioConfig: ProjectCallSMSConfigService.toTwilioConfig(
|
|
statusPage.callSmsConfig,
|
|
),
|
|
statusPageId: statusPage.id!,
|
|
}).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
}
|
|
|
|
if (statusPageSubscriber.slackIncomingWebhookUrl) {
|
|
const slackMessage: string = `You have selected to manage your subscription for the status page: ${statusPage.name}. You can manage your subscription here: ${manageUrlink}`;
|
|
|
|
SlackUtil.sendMessageToChannelViaIncomingWebhook({
|
|
url: statusPageSubscriber.slackIncomingWebhookUrl,
|
|
text: slackMessage,
|
|
}).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
}
|
|
|
|
logger.debug(
|
|
`Subscription management link sent to subscriber with ID: ${statusPageSubscriber.id}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async subscribeToStatusPage(req: ExpressRequest): Promise<void> {
|
|
const objectId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
logger.debug(`Subscribing to status page with ID: ${objectId}`);
|
|
|
|
await this.checkHasReadAccess({
|
|
statusPageId: objectId,
|
|
req: req,
|
|
});
|
|
|
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: objectId.toString(),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
enableEmailSubscribers: true,
|
|
enableSmsSubscribers: true,
|
|
enableSlackSubscribers: true,
|
|
enableMicrosoftTeamsSubscribers: true,
|
|
allowSubscribersToChooseResources: true,
|
|
allowSubscribersToChooseEventTypes: true,
|
|
showSubscriberPageOnStatusPage: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage) {
|
|
logger.debug(`Status page not found with ID: ${objectId}`);
|
|
throw new BadDataException("Status Page not found");
|
|
}
|
|
|
|
if (!statusPage.showSubscriberPageOnStatusPage) {
|
|
logger.debug(
|
|
`Subscriber page not enabled for status page with ID: ${objectId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"Subscribes not enabled for this status page.",
|
|
);
|
|
}
|
|
|
|
logger.debug(`Status page found: ${JSON.stringify(statusPage)}`);
|
|
|
|
if (
|
|
req.body.data["subscriberEmail"] &&
|
|
!statusPage.enableEmailSubscribers
|
|
) {
|
|
logger.debug(
|
|
`Email subscribers not enabled for status page with ID: ${objectId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"Email subscribers not enabled for this status page.",
|
|
);
|
|
}
|
|
|
|
if (req.body.data["subscriberPhone"] && !statusPage.enableSmsSubscribers) {
|
|
logger.debug(
|
|
`SMS subscribers not enabled for status page with ID: ${objectId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"SMS subscribers not enabled for this status page.",
|
|
);
|
|
}
|
|
|
|
// if no email or phone, throw error.
|
|
|
|
if (
|
|
req.body.data["slackWorkspaceName"] &&
|
|
!statusPage.enableSlackSubscribers
|
|
) {
|
|
logger.debug(
|
|
`Slack subscribers not enabled for status page with ID: ${objectId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"Slack subscribers not enabled for this status page.",
|
|
);
|
|
}
|
|
|
|
if (
|
|
req.body.data["microsoftTeamsWorkspaceName"] &&
|
|
!statusPage.enableMicrosoftTeamsSubscribers
|
|
) {
|
|
logger.debug(
|
|
`Microsoft Teams subscribers not enabled for status page with ID: ${objectId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"Microsoft Teams subscribers not enabled for this status page.",
|
|
);
|
|
}
|
|
|
|
if (
|
|
!req.body.data["subscriberEmail"] &&
|
|
!req.body.data["subscriberPhone"] &&
|
|
!req.body.data["slackWorkspaceName"] &&
|
|
!req.body.data["microsoftTeamsWorkspaceName"]
|
|
) {
|
|
logger.debug(
|
|
`No email, phone, slack workspace name, or Microsoft Teams workspace name provided for subscription to status page with ID: ${objectId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"Email, phone, slack workspace name, or Microsoft Teams workspace name is required to subscribe to this status page.",
|
|
);
|
|
}
|
|
|
|
const email: Email | undefined = req.body.data["subscriberEmail"]
|
|
? new Email(req.body.data["subscriberEmail"] as string)
|
|
: undefined;
|
|
|
|
const phone: Phone | undefined = req.body.data["subscriberPhone"]
|
|
? new Phone(req.body.data["subscriberPhone"] as string)
|
|
: undefined;
|
|
|
|
const slackIncomingWebhookUrl: string | undefined = req.body.data[
|
|
"slackIncomingWebhookUrl"
|
|
]
|
|
? (req.body.data["slackIncomingWebhookUrl"] as string)
|
|
: undefined;
|
|
|
|
const slackWorkspaceName: string | undefined = req.body.data[
|
|
"slackWorkspaceName"
|
|
]
|
|
? (req.body.data["slackWorkspaceName"] as string)
|
|
: undefined;
|
|
|
|
const microsoftTeamsIncomingWebhookUrl: string | undefined = req.body.data[
|
|
"microsoftTeamsIncomingWebhookUrl"
|
|
]
|
|
? (req.body.data["microsoftTeamsIncomingWebhookUrl"] as string)
|
|
: undefined;
|
|
|
|
const microsoftTeamsWorkspaceName: string | undefined = req.body.data[
|
|
"microsoftTeamsWorkspaceName"
|
|
]
|
|
? (req.body.data["microsoftTeamsWorkspaceName"] as string)
|
|
: undefined;
|
|
|
|
let statusPageSubscriber: StatusPageSubscriber | null = null;
|
|
|
|
let isUpdate: boolean = false;
|
|
|
|
if (!req.params["subscriberId"]) {
|
|
logger.debug(
|
|
`Creating new subscriber for status page with ID: ${objectId}`,
|
|
);
|
|
statusPageSubscriber = new StatusPageSubscriber();
|
|
} else {
|
|
const subscriberId: ObjectID = new ObjectID(
|
|
req.params["subscriberId"] as string,
|
|
);
|
|
|
|
logger.debug(
|
|
`Updating existing subscriber with ID: ${subscriberId} for status page with ID: ${objectId}`,
|
|
);
|
|
statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
|
|
query: {
|
|
_id: subscriberId.toString(),
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPageSubscriber) {
|
|
logger.debug(`Subscriber not found with ID: ${subscriberId}`);
|
|
throw new BadDataException("Subscriber not found");
|
|
}
|
|
|
|
isUpdate = true;
|
|
}
|
|
|
|
if (email) {
|
|
logger.debug(`Setting subscriber email: ${email}`);
|
|
statusPageSubscriber.subscriberEmail = email;
|
|
}
|
|
|
|
if (phone) {
|
|
logger.debug(`Setting subscriber phone: ${phone}`);
|
|
statusPageSubscriber.subscriberPhone = phone;
|
|
}
|
|
|
|
if (slackIncomingWebhookUrl) {
|
|
logger.debug(`Setting subscriber slack: ${slackIncomingWebhookUrl}`);
|
|
statusPageSubscriber.slackIncomingWebhookUrl = URL.fromString(
|
|
slackIncomingWebhookUrl,
|
|
);
|
|
}
|
|
|
|
if (slackWorkspaceName) {
|
|
logger.debug(
|
|
`Setting subscriber slack workspace name: ${slackWorkspaceName}`,
|
|
);
|
|
statusPageSubscriber.slackWorkspaceName = slackWorkspaceName;
|
|
}
|
|
|
|
if (microsoftTeamsIncomingWebhookUrl) {
|
|
logger.debug(
|
|
`Setting subscriber Microsoft Teams webhook: ${microsoftTeamsIncomingWebhookUrl}`,
|
|
);
|
|
statusPageSubscriber.microsoftTeamsIncomingWebhookUrl = URL.fromString(
|
|
microsoftTeamsIncomingWebhookUrl,
|
|
);
|
|
}
|
|
|
|
if (microsoftTeamsWorkspaceName) {
|
|
logger.debug(
|
|
`Setting subscriber Microsoft Teams workspace name: ${microsoftTeamsWorkspaceName}`,
|
|
);
|
|
statusPageSubscriber.microsoftTeamsWorkspaceName =
|
|
microsoftTeamsWorkspaceName;
|
|
}
|
|
|
|
if (
|
|
req.body.data["statusPageResources"] &&
|
|
!statusPage.allowSubscribersToChooseResources
|
|
) {
|
|
logger.debug(
|
|
`Subscribers not allowed to choose resources for status page with ID: ${objectId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"Subscribers are not allowed to choose resources for this status page.",
|
|
);
|
|
}
|
|
|
|
if (
|
|
req.body.data["statusPageEventTypes"] &&
|
|
!statusPage.allowSubscribersToChooseEventTypes
|
|
) {
|
|
logger.debug(
|
|
`Subscribers not allowed to choose event types for status page with ID: ${objectId}`,
|
|
);
|
|
throw new BadDataException(
|
|
"Subscribers are not allowed to choose event types for this status page.",
|
|
);
|
|
}
|
|
|
|
statusPageSubscriber.statusPageId = objectId;
|
|
statusPageSubscriber.sendYouHaveSubscribedMessage = true;
|
|
statusPageSubscriber.projectId = statusPage.projectId!;
|
|
statusPageSubscriber.isSubscribedToAllResources = Boolean(
|
|
req.body.data["isSubscribedToAllResources"],
|
|
);
|
|
|
|
statusPageSubscriber.isSubscribedToAllEventTypes = Boolean(
|
|
req.body.data["isSubscribedToAllEventTypes"],
|
|
);
|
|
|
|
if (
|
|
req.body.data["statusPageResources"] &&
|
|
req.body.data["statusPageResources"].length > 0
|
|
) {
|
|
logger.debug(
|
|
`Setting subscriber resources: ${JSON.stringify(req.body.data["statusPageResources"])}`,
|
|
);
|
|
statusPageSubscriber.statusPageResources = req.body.data[
|
|
"statusPageResources"
|
|
] as Array<StatusPageResource>;
|
|
}
|
|
|
|
if (
|
|
req.body.data["statusPageEventTypes"] &&
|
|
req.body.data["statusPageEventTypes"].length > 0
|
|
) {
|
|
logger.debug(
|
|
`Setting subscriber event types: ${JSON.stringify(req.body.data["statusPageEventTypes"])}`,
|
|
);
|
|
statusPageSubscriber.statusPageEventTypes = req.body.data[
|
|
"statusPageEventTypes"
|
|
] as Array<StatusPageEventType>;
|
|
}
|
|
|
|
if (isUpdate) {
|
|
// check isUnsubscribed is set to false.
|
|
logger.debug(`Updating subscriber with ID: ${statusPageSubscriber.id}`);
|
|
statusPageSubscriber.isUnsubscribed = Boolean(
|
|
req.body.data["isUnsubscribed"],
|
|
);
|
|
|
|
await StatusPageSubscriberService.updateOneById({
|
|
id: statusPageSubscriber.id!,
|
|
data: {
|
|
statusPageResources: statusPageSubscriber.statusPageResources!,
|
|
isSubscribedToAllResources:
|
|
statusPageSubscriber.isSubscribedToAllResources!,
|
|
isUnsubscribed: statusPageSubscriber.isUnsubscribed,
|
|
} as any,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
} else {
|
|
logger.debug(
|
|
`Creating new subscriber: ${JSON.stringify(statusPageSubscriber)}`,
|
|
);
|
|
await StatusPageSubscriberService.create({
|
|
data: statusPageSubscriber,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
logger.debug(
|
|
`Subscription process completed for status page with ID: ${objectId}`,
|
|
);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async getSubscriber(
|
|
req: ExpressRequest,
|
|
): Promise<StatusPageSubscriber> {
|
|
const objectId: ObjectID = new ObjectID(
|
|
req.params["statusPageId"] as string,
|
|
);
|
|
|
|
await this.checkHasReadAccess({
|
|
statusPageId: objectId,
|
|
req: req,
|
|
});
|
|
|
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: objectId.toString(),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage) {
|
|
throw new BadDataException("Status Page not found");
|
|
}
|
|
|
|
const subscriberId: ObjectID = new ObjectID(
|
|
req.params["subscriberId"] as string,
|
|
);
|
|
|
|
const statusPageSubscriber: StatusPageSubscriber | null =
|
|
await StatusPageSubscriberService.findOneBy({
|
|
query: {
|
|
_id: subscriberId.toString(),
|
|
statusPageId: statusPage.id!,
|
|
},
|
|
select: {
|
|
isUnsubscribed: true,
|
|
subscriberEmail: true,
|
|
subscriberPhone: true,
|
|
slackWorkspaceName: true,
|
|
statusPageId: true,
|
|
statusPageResources: true,
|
|
isSubscribedToAllResources: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPageSubscriber) {
|
|
throw new BadDataException("Subscriber not found");
|
|
}
|
|
|
|
return statusPageSubscriber;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async getIncidents(
|
|
statusPageId: ObjectID,
|
|
incidentId: ObjectID | null,
|
|
req: ExpressRequest,
|
|
): Promise<JSONObject> {
|
|
await this.checkHasReadAccess({
|
|
statusPageId: statusPageId,
|
|
req: req,
|
|
});
|
|
|
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: statusPageId.toString(),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
showIncidentHistoryInDays: true,
|
|
showIncidentLabelsOnStatusPage: true,
|
|
showIncidentsOnStatusPage: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage) {
|
|
throw new BadDataException("Status Page not found");
|
|
}
|
|
|
|
if (!statusPage.showIncidentsOnStatusPage) {
|
|
throw new BadDataException(
|
|
"Incidents are not enabled on this status page.",
|
|
);
|
|
}
|
|
|
|
// get monitors on status page.
|
|
const statusPageResources: Array<StatusPageResource> =
|
|
await StatusPageService.getStatusPageResources({
|
|
statusPageId: statusPageId,
|
|
});
|
|
|
|
const { monitorsOnStatusPage, monitorsInGroup } =
|
|
await StatusPageService.getMonitorIdsOnStatusPage({
|
|
statusPageId: statusPageId,
|
|
});
|
|
|
|
const today: Date = OneUptimeDate.getCurrentDate();
|
|
|
|
const historyDays: Date = OneUptimeDate.getSomeDaysAgo(
|
|
statusPage.showIncidentHistoryInDays || 14,
|
|
);
|
|
|
|
let incidentQuery: Query<Incident> = {
|
|
monitors: monitorsOnStatusPage as any,
|
|
projectId: statusPage.projectId!,
|
|
createdAt: QueryHelper.inBetween(historyDays, today),
|
|
isVisibleOnStatusPage: true,
|
|
};
|
|
|
|
if (incidentId) {
|
|
incidentQuery = {
|
|
monitors: monitorsOnStatusPage as any,
|
|
projectId: statusPage.projectId!,
|
|
_id: incidentId.toString(),
|
|
};
|
|
}
|
|
|
|
// check if status page has active incident.
|
|
let incidents: Array<Incident> = [];
|
|
|
|
let selectIncidents: Select<Incident> = {
|
|
createdAt: true,
|
|
declaredAt: true,
|
|
updatedAt: true,
|
|
title: true,
|
|
description: true,
|
|
_id: true,
|
|
postmortemNote: true,
|
|
postmortemPostedAt: true,
|
|
showPostmortemOnStatusPage: true,
|
|
postmortemAttachments: {
|
|
_id: true,
|
|
name: true,
|
|
},
|
|
incidentSeverity: {
|
|
name: true,
|
|
color: true,
|
|
},
|
|
currentIncidentState: {
|
|
name: true,
|
|
color: true,
|
|
_id: true,
|
|
order: true,
|
|
},
|
|
monitors: {
|
|
_id: true,
|
|
},
|
|
};
|
|
|
|
if (statusPage.showIncidentLabelsOnStatusPage) {
|
|
selectIncidents = {
|
|
...selectIncidents,
|
|
labels: {
|
|
name: true,
|
|
color: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (monitorsOnStatusPage.length > 0) {
|
|
incidents = await IncidentService.findBy({
|
|
query: incidentQuery,
|
|
select: selectIncidents,
|
|
sort: {
|
|
declaredAt: SortOrder.Descending,
|
|
createdAt: SortOrder.Descending,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let activeIncidents: Array<Incident> = [];
|
|
|
|
const unresolvedIncidentStates: Array<IncidentState> =
|
|
await IncidentStateService.getUnresolvedIncidentStates(
|
|
statusPage.projectId!,
|
|
{
|
|
isRoot: true,
|
|
},
|
|
);
|
|
|
|
const unresolvbedIncidentStateIds: Array<ObjectID> =
|
|
unresolvedIncidentStates.map((state: IncidentState) => {
|
|
return state.id!;
|
|
});
|
|
|
|
// If there is no particular incident id to fetch then fetch active incidents.
|
|
if (!incidentId) {
|
|
activeIncidents = await IncidentService.findBy({
|
|
query: {
|
|
monitors: monitorsOnStatusPage as any,
|
|
isVisibleOnStatusPage: true,
|
|
currentIncidentStateId: QueryHelper.any(
|
|
unresolvbedIncidentStateIds,
|
|
),
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: selectIncidents,
|
|
sort: {
|
|
declaredAt: SortOrder.Descending,
|
|
createdAt: SortOrder.Descending,
|
|
},
|
|
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
incidents = [...activeIncidents, ...incidents];
|
|
|
|
// get distinct by id.
|
|
|
|
incidents = ArrayUtil.distinctByFieldName(incidents, "_id");
|
|
}
|
|
|
|
const incidentsOnStatusPage: Array<ObjectID> = incidents.map(
|
|
(incident: Incident) => {
|
|
return incident.id!;
|
|
},
|
|
);
|
|
|
|
let incidentPublicNotes: Array<IncidentPublicNote> = [];
|
|
|
|
if (incidentsOnStatusPage.length > 0) {
|
|
incidentPublicNotes = await IncidentPublicNoteService.findBy({
|
|
query: {
|
|
incidentId: QueryHelper.any(incidentsOnStatusPage),
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
postedAt: true,
|
|
note: true,
|
|
incidentId: true,
|
|
attachments: {
|
|
_id: true,
|
|
name: true,
|
|
},
|
|
},
|
|
sort: {
|
|
postedAt: SortOrder.Descending, // new note first
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
let incidentStateTimelines: Array<IncidentStateTimeline> = [];
|
|
|
|
if (incidentsOnStatusPage.length > 0) {
|
|
incidentStateTimelines = await IncidentStateTimelineService.findBy({
|
|
query: {
|
|
incidentId: QueryHelper.any(incidentsOnStatusPage),
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
createdAt: true,
|
|
startsAt: true,
|
|
incidentId: true,
|
|
incidentState: {
|
|
name: true,
|
|
color: true,
|
|
},
|
|
},
|
|
sort: {
|
|
startsAt: SortOrder.Descending, // newer state changes first
|
|
},
|
|
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
// get all the incident states for this project.
|
|
const incidentStates: Array<IncidentState> =
|
|
await IncidentStateService.findBy({
|
|
query: {
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
isResolvedState: true,
|
|
order: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const response: JSONObject = {
|
|
incidentPublicNotes: BaseModel.toJSONArray(
|
|
incidentPublicNotes,
|
|
IncidentPublicNote,
|
|
),
|
|
incidentStates: BaseModel.toJSONArray(incidentStates, IncidentState),
|
|
incidents: BaseModel.toJSONArray(incidents, Incident),
|
|
statusPageResources: BaseModel.toJSONArray(
|
|
statusPageResources,
|
|
StatusPageResource,
|
|
),
|
|
incidentStateTimelines: BaseModel.toJSONArray(
|
|
incidentStateTimelines,
|
|
IncidentStateTimeline,
|
|
),
|
|
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
|
};
|
|
|
|
return response;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async getStatusPageResourcesAndTimelines(data: {
|
|
statusPageId: ObjectID;
|
|
startDateForMonitorTimeline: Date;
|
|
endDateForMonitorTimeline: Date;
|
|
}): Promise<{
|
|
statusPageResources: StatusPageResource[];
|
|
monitorStatuses: MonitorStatus[];
|
|
monitorStatusTimelines: MonitorStatusTimeline[];
|
|
monitorGroupCurrentStatuses: Dictionary<ObjectID>;
|
|
statusPageGroups: StatusPageGroup[];
|
|
statusPage: StatusPage;
|
|
monitorsOnStatusPage: ObjectID[];
|
|
monitorsInGroup: Dictionary<ObjectID[]>;
|
|
}> {
|
|
const objectId: ObjectID = data.statusPageId;
|
|
|
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: objectId.toString(),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
isPublicStatusPage: true,
|
|
overviewPageDescription: true,
|
|
showIncidentLabelsOnStatusPage: true,
|
|
showScheduledEventLabelsOnStatusPage: true,
|
|
downtimeMonitorStatuses: {
|
|
_id: true,
|
|
},
|
|
defaultBarColor: true,
|
|
showOverallUptimePercentOnStatusPage: true,
|
|
overallUptimePercentPrecision: true,
|
|
showAnnouncementsOnStatusPage: true,
|
|
showIncidentsOnStatusPage: true,
|
|
showScheduledMaintenanceEventsOnStatusPage: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage) {
|
|
throw new BadDataException("Status Page not found");
|
|
}
|
|
|
|
//get monitor statuses
|
|
|
|
const monitorStatuses: Array<MonitorStatus> =
|
|
await MonitorStatusService.findBy({
|
|
query: {
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
name: true,
|
|
color: true,
|
|
priority: true,
|
|
isOperationalState: true,
|
|
},
|
|
sort: {
|
|
priority: SortOrder.Ascending,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// get resource groups.
|
|
|
|
const groups: Array<StatusPageGroup> = await StatusPageGroupService.findBy({
|
|
query: {
|
|
statusPageId: objectId,
|
|
},
|
|
select: {
|
|
name: true,
|
|
order: true,
|
|
description: true,
|
|
isExpandedByDefault: true,
|
|
showCurrentStatus: true,
|
|
showUptimePercent: true,
|
|
uptimePercentPrecision: true,
|
|
},
|
|
sort: {
|
|
order: SortOrder.Ascending,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// get monitors on status page.
|
|
const statusPageResources: Array<StatusPageResource> =
|
|
await StatusPageResourceService.findBy({
|
|
query: {
|
|
statusPageId: objectId,
|
|
},
|
|
select: {
|
|
statusPageGroupId: true,
|
|
monitorId: true,
|
|
displayTooltip: true,
|
|
displayDescription: true,
|
|
displayName: true,
|
|
showStatusHistoryChart: true,
|
|
showCurrentStatus: true,
|
|
order: true,
|
|
monitor: {
|
|
_id: true,
|
|
currentMonitorStatusId: true,
|
|
},
|
|
monitorGroupId: true,
|
|
showUptimePercent: true,
|
|
uptimePercentPrecision: true,
|
|
},
|
|
sort: {
|
|
order: SortOrder.Ascending,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const monitorGroupIds: Array<ObjectID> = statusPageResources
|
|
.map((resource: StatusPageResource) => {
|
|
return resource.monitorGroupId!;
|
|
})
|
|
.filter((id: ObjectID) => {
|
|
return Boolean(id); // remove nulls
|
|
});
|
|
|
|
// get monitors in the group.
|
|
const monitorGroupCurrentStatuses: Dictionary<ObjectID> = {};
|
|
const monitorsInGroup: Dictionary<Array<ObjectID>> = {};
|
|
|
|
// get monitor status charts.
|
|
const monitorsOnStatusPage: Array<ObjectID> = statusPageResources
|
|
.map((monitor: StatusPageResource) => {
|
|
return monitor.monitorId!;
|
|
})
|
|
.filter((id: ObjectID) => {
|
|
return Boolean(id); // remove nulls
|
|
});
|
|
|
|
const monitorsOnStatusPageForTimeline: Array<ObjectID> = statusPageResources
|
|
.filter((monitor: StatusPageResource) => {
|
|
return monitor.showStatusHistoryChart || monitor.showUptimePercent;
|
|
})
|
|
.map((monitor: StatusPageResource) => {
|
|
return monitor.monitorId!;
|
|
})
|
|
.filter((id: ObjectID) => {
|
|
return Boolean(id); // remove nulls
|
|
});
|
|
|
|
for (const monitorGroupId of monitorGroupIds) {
|
|
// get current status of monitors in the group.
|
|
|
|
const currentStatus: MonitorStatus =
|
|
await MonitorGroupService.getCurrentStatus(monitorGroupId, {
|
|
isRoot: true,
|
|
});
|
|
|
|
monitorGroupCurrentStatuses[monitorGroupId.toString()] =
|
|
currentStatus.id!;
|
|
|
|
// get monitors in the group.
|
|
|
|
const groupResources: Array<MonitorGroupResource> =
|
|
await MonitorGroupResourceService.findBy({
|
|
query: {
|
|
monitorGroupId: monitorGroupId,
|
|
},
|
|
select: {
|
|
monitorId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
});
|
|
|
|
const monitorsInGroupIds: Array<ObjectID> = groupResources
|
|
.map((resource: MonitorGroupResource) => {
|
|
return resource.monitorId!;
|
|
})
|
|
.filter((id: ObjectID) => {
|
|
return Boolean(id); // remove nulls
|
|
});
|
|
|
|
const shouldShowTimelineForThisGroup: boolean = Boolean(
|
|
statusPageResources.find((resource: StatusPageResource) => {
|
|
return (
|
|
resource.monitorGroupId?.toString() === monitorGroupId.toString() &&
|
|
(resource.showStatusHistoryChart || resource.showUptimePercent)
|
|
);
|
|
}),
|
|
);
|
|
|
|
for (const monitorId of monitorsInGroupIds) {
|
|
if (!monitorId) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
!monitorsOnStatusPage.find((item: ObjectID) => {
|
|
return item.toString() === monitorId.toString();
|
|
})
|
|
) {
|
|
monitorsOnStatusPage.push(monitorId);
|
|
}
|
|
|
|
// add this to the timeline event for this group.
|
|
|
|
if (
|
|
shouldShowTimelineForThisGroup &&
|
|
!monitorsOnStatusPageForTimeline.find((item: ObjectID) => {
|
|
return item.toString() === monitorId.toString();
|
|
})
|
|
) {
|
|
monitorsOnStatusPageForTimeline.push(monitorId);
|
|
}
|
|
}
|
|
|
|
monitorsInGroup[monitorGroupId.toString()] = monitorsInGroupIds;
|
|
}
|
|
|
|
const monitorStatusTimelines: Array<MonitorStatusTimeline> =
|
|
await StatusPageService.getMonitorStatusTimelineForStatusPage({
|
|
monitorIds: monitorsOnStatusPageForTimeline,
|
|
startDate: data.startDateForMonitorTimeline,
|
|
endDate: data.endDateForMonitorTimeline,
|
|
});
|
|
|
|
// return everything.
|
|
|
|
return {
|
|
statusPageResources,
|
|
monitorStatuses,
|
|
monitorGroupCurrentStatuses,
|
|
statusPageGroups: groups,
|
|
monitorStatusTimelines,
|
|
statusPage,
|
|
monitorsOnStatusPage,
|
|
monitorsInGroup,
|
|
};
|
|
}
|
|
|
|
private async getStatusPageAnnouncementAttachment(
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
): Promise<void> {
|
|
const statusPageIdParam: string | undefined = req.params["statusPageId"];
|
|
const announcementIdParam: string | undefined =
|
|
req.params["announcementId"];
|
|
const fileIdParam: string | undefined = req.params["fileId"];
|
|
|
|
if (!statusPageIdParam || !announcementIdParam || !fileIdParam) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
let statusPageId: ObjectID;
|
|
let announcementId: ObjectID;
|
|
let fileId: ObjectID;
|
|
|
|
try {
|
|
statusPageId = new ObjectID(statusPageIdParam);
|
|
announcementId = new ObjectID(announcementIdParam);
|
|
fileId = new ObjectID(fileIdParam);
|
|
} catch {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
await this.checkHasReadAccess({
|
|
statusPageId: statusPageId,
|
|
req: req,
|
|
});
|
|
|
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: statusPageId.toString(),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
showAnnouncementsOnStatusPage: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (
|
|
!statusPage ||
|
|
!statusPage.projectId ||
|
|
!statusPage.showAnnouncementsOnStatusPage
|
|
) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
const announcement: StatusPageAnnouncement | null =
|
|
await StatusPageAnnouncementService.findOneBy({
|
|
query: {
|
|
_id: announcementId.toString(),
|
|
projectId: statusPage.projectId!,
|
|
statusPages: [statusPageId] as any,
|
|
},
|
|
select: {
|
|
attachments: {
|
|
_id: true,
|
|
file: true,
|
|
fileType: true,
|
|
name: true,
|
|
},
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!announcement) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
const attachment: File | undefined = announcement.attachments?.find(
|
|
(file: File) => {
|
|
const attachmentId: string | null = file._id
|
|
? file._id.toString()
|
|
: file.id
|
|
? file.id.toString()
|
|
: null;
|
|
return attachmentId === fileId.toString();
|
|
},
|
|
);
|
|
|
|
if (!attachment || !attachment.file) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
Response.setNoCacheHeaders(res);
|
|
return Response.sendFileResponse(req, res, attachment);
|
|
}
|
|
|
|
private async getScheduledMaintenancePublicNoteAttachment(
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
): Promise<void> {
|
|
const statusPageIdParam: string | undefined = req.params["statusPageId"];
|
|
const scheduledMaintenanceIdParam: string | undefined =
|
|
req.params["scheduledMaintenanceId"];
|
|
const noteIdParam: string | undefined = req.params["noteId"];
|
|
const fileIdParam: string | undefined = req.params["fileId"];
|
|
|
|
if (
|
|
!statusPageIdParam ||
|
|
!scheduledMaintenanceIdParam ||
|
|
!noteIdParam ||
|
|
!fileIdParam
|
|
) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
let statusPageId: ObjectID;
|
|
let scheduledMaintenanceId: ObjectID;
|
|
let noteId: ObjectID;
|
|
let fileId: ObjectID;
|
|
|
|
try {
|
|
statusPageId = new ObjectID(statusPageIdParam);
|
|
scheduledMaintenanceId = new ObjectID(scheduledMaintenanceIdParam);
|
|
noteId = new ObjectID(noteIdParam);
|
|
fileId = new ObjectID(fileIdParam);
|
|
} catch {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
await this.checkHasReadAccess({
|
|
statusPageId: statusPageId,
|
|
req: req,
|
|
});
|
|
|
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: statusPageId.toString(),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
showScheduledMaintenanceEventsOnStatusPage: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (
|
|
!statusPage ||
|
|
!statusPage.projectId ||
|
|
!statusPage.showScheduledMaintenanceEventsOnStatusPage
|
|
) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
const scheduledMaintenance: ScheduledMaintenance | null =
|
|
await ScheduledMaintenanceService.findOneBy({
|
|
query: {
|
|
_id: scheduledMaintenanceId.toString(),
|
|
projectId: statusPage.projectId!,
|
|
isVisibleOnStatusPage: true,
|
|
statusPages: statusPageId as any,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!scheduledMaintenance) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
const scheduledMaintenancePublicNote: ScheduledMaintenancePublicNote | null =
|
|
await ScheduledMaintenancePublicNoteService.findOneBy({
|
|
query: {
|
|
_id: noteId.toString(),
|
|
scheduledMaintenanceId: scheduledMaintenanceId.toString(),
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
attachments: {
|
|
_id: true,
|
|
file: true,
|
|
fileType: true,
|
|
name: true,
|
|
},
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!scheduledMaintenancePublicNote) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
const attachment: File | undefined =
|
|
scheduledMaintenancePublicNote.attachments?.find((file: File) => {
|
|
const attachmentId: string | null = file._id
|
|
? file._id.toString()
|
|
: file.id
|
|
? file.id.toString()
|
|
: null;
|
|
return attachmentId === fileId.toString();
|
|
});
|
|
|
|
if (!attachment || !attachment.file) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
Response.setNoCacheHeaders(res);
|
|
return Response.sendFileResponse(req, res, attachment);
|
|
}
|
|
|
|
private async getIncidentPostmortemAttachment(
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
): Promise<void> {
|
|
const statusPageIdParam: string | undefined = req.params["statusPageId"];
|
|
const incidentIdParam: string | undefined = req.params["incidentId"];
|
|
const fileIdParam: string | undefined = req.params["fileId"];
|
|
|
|
if (!statusPageIdParam || !incidentIdParam || !fileIdParam) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
let statusPageId: ObjectID;
|
|
let incidentId: ObjectID;
|
|
let fileId: ObjectID;
|
|
|
|
try {
|
|
statusPageId = new ObjectID(statusPageIdParam);
|
|
incidentId = new ObjectID(incidentIdParam);
|
|
fileId = new ObjectID(fileIdParam);
|
|
} catch {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
await this.checkHasReadAccess({
|
|
statusPageId,
|
|
req,
|
|
});
|
|
|
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: statusPageId.toString(),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
showIncidentsOnStatusPage: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (
|
|
!statusPage ||
|
|
!statusPage.projectId ||
|
|
!statusPage.showIncidentsOnStatusPage
|
|
) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
const { monitorsOnStatusPage } =
|
|
await StatusPageService.getMonitorIdsOnStatusPage({
|
|
statusPageId,
|
|
});
|
|
|
|
if (!monitorsOnStatusPage || monitorsOnStatusPage.length === 0) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
const incident: Incident | null = await IncidentService.findOneBy({
|
|
query: {
|
|
_id: incidentId.toString(),
|
|
projectId: statusPage.projectId!,
|
|
isVisibleOnStatusPage: true,
|
|
showPostmortemOnStatusPage: true,
|
|
monitors: monitorsOnStatusPage as any,
|
|
},
|
|
select: {
|
|
postmortemAttachments: {
|
|
_id: true,
|
|
file: true,
|
|
fileType: true,
|
|
name: true,
|
|
},
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!incident) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
const attachment: File | undefined = incident.postmortemAttachments?.find(
|
|
(file: File) => {
|
|
const attachmentId: string | null = file._id
|
|
? file._id.toString()
|
|
: file.id
|
|
? file.id.toString()
|
|
: null;
|
|
return attachmentId === fileId.toString();
|
|
},
|
|
);
|
|
|
|
if (!attachment || !attachment.file) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
Response.setNoCacheHeaders(res);
|
|
return Response.sendFileResponse(req, res, attachment);
|
|
}
|
|
|
|
private async getIncidentPublicNoteAttachment(
|
|
req: ExpressRequest,
|
|
res: ExpressResponse,
|
|
): Promise<void> {
|
|
const statusPageIdParam: string | undefined = req.params["statusPageId"];
|
|
const incidentIdParam: string | undefined = req.params["incidentId"];
|
|
const noteIdParam: string | undefined = req.params["noteId"];
|
|
const fileIdParam: string | undefined = req.params["fileId"];
|
|
|
|
if (
|
|
!statusPageIdParam ||
|
|
!incidentIdParam ||
|
|
!noteIdParam ||
|
|
!fileIdParam
|
|
) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
let statusPageId: ObjectID;
|
|
let incidentId: ObjectID;
|
|
let noteId: ObjectID;
|
|
let fileId: ObjectID;
|
|
|
|
try {
|
|
statusPageId = new ObjectID(statusPageIdParam);
|
|
incidentId = new ObjectID(incidentIdParam);
|
|
noteId = new ObjectID(noteIdParam);
|
|
fileId = new ObjectID(fileIdParam);
|
|
} catch {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
await this.checkHasReadAccess({
|
|
statusPageId: statusPageId,
|
|
req: req,
|
|
});
|
|
|
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
query: {
|
|
_id: statusPageId.toString(),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
showIncidentsOnStatusPage: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!statusPage || !statusPage.projectId) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
if (!statusPage.showIncidentsOnStatusPage) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
const { monitorsOnStatusPage } =
|
|
await StatusPageService.getMonitorIdsOnStatusPage({
|
|
statusPageId: statusPageId,
|
|
});
|
|
|
|
if (!monitorsOnStatusPage || monitorsOnStatusPage.length === 0) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
const incident: Incident | null = await IncidentService.findOneBy({
|
|
query: {
|
|
_id: incidentId.toString(),
|
|
projectId: statusPage.projectId!,
|
|
isVisibleOnStatusPage: true,
|
|
monitors: monitorsOnStatusPage as any,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!incident) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
const incidentPublicNote: IncidentPublicNote | null =
|
|
await IncidentPublicNoteService.findOneBy({
|
|
query: {
|
|
_id: noteId.toString(),
|
|
incidentId: incidentId.toString(),
|
|
projectId: statusPage.projectId!,
|
|
},
|
|
select: {
|
|
attachments: {
|
|
_id: true,
|
|
file: true,
|
|
fileType: true,
|
|
name: true,
|
|
},
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!incidentPublicNote) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
const attachment: File | undefined = incidentPublicNote.attachments?.find(
|
|
(file: File) => {
|
|
const attachmentId: string | null = file._id
|
|
? file._id.toString()
|
|
: file.id
|
|
? file.id.toString()
|
|
: null;
|
|
return attachmentId === fileId.toString();
|
|
},
|
|
);
|
|
|
|
if (!attachment || !attachment.file) {
|
|
throw new NotFoundException("Attachment not found");
|
|
}
|
|
|
|
Response.setNoCacheHeaders(res);
|
|
return Response.sendFileResponse(req, res, attachment);
|
|
}
|
|
|
|
public async checkHasReadAccess(data: {
|
|
statusPageId: ObjectID;
|
|
req: ExpressRequest;
|
|
}): Promise<void> {
|
|
const accessResult: {
|
|
hasReadAccess: boolean;
|
|
error?: NotAuthenticatedException | ForbiddenException;
|
|
} = await this.service.hasReadAccess({
|
|
statusPageId: data.statusPageId,
|
|
req: data.req,
|
|
});
|
|
|
|
if (!accessResult.hasReadAccess) {
|
|
throw (
|
|
accessResult.error ||
|
|
new NotAuthenticatedException(
|
|
"You are not authenticated to access this status page",
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|