feat: implement IP whitelist functionality for status pages and enhance access control

This commit is contained in:
Simon Larsen
2025-05-28 20:41:03 +01:00
parent 65fc159560
commit 474998a3bf
7 changed files with 201 additions and 195 deletions

View File

@@ -34,10 +34,8 @@ import {
import logger from "../Utils/Logger";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import CommonAPI from "./CommonAPI";
import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
import ArrayUtil from "../../Utils/Array";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
import SortOrder from "../../Types/BaseDatabase/SortOrder";
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
import OneUptimeDate from "../../Types/Date";
@@ -90,6 +88,7 @@ import Protocol from "../../Types/API/Protocol";
import DatabaseConfig from "../DatabaseConfig";
import { FileRoute } from "../../ServiceRoute";
import ProjectSmtpConfigService from "../Services/ProjectSmtpConfigService";
import ForbiddenException from "../../Types/Exception/ForbiddenException";
export default class StatusPageAPI extends BaseAPI<
StatusPage,
@@ -658,26 +657,19 @@ export default class StatusPageAPI extends BaseAPI<
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const objectId: ObjectID = new ObjectID(
const statusPageId: ObjectID = new ObjectID(
req.params["statusPageId"] as string,
);
if (
!(await this.service.hasReadAccess(
objectId,
await CommonAPI.getDatabaseCommonInteractionProps(req),
req,
))
) {
throw new NotAuthenticatedException(
"You are not authenticated to access this status page",
);
}
await this.checkHasReadAccess({
statusPageId: statusPageId,
req: req,
});
const resources: Array<StatusPageResource> =
await StatusPageResourceService.findBy({
query: {
statusPageId: objectId,
statusPageId: statusPageId,
},
select: {
_id: true,
@@ -729,17 +721,10 @@ export default class StatusPageAPI extends BaseAPI<
throw new BadDataException("Status Page or Resource not found");
}
if (
!(await this.service.hasReadAccess(
statusPageId,
await CommonAPI.getDatabaseCommonInteractionProps(req),
req,
))
) {
throw new NotAuthenticatedException(
"You are not authenticated to access this status page",
);
}
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.
@@ -1022,21 +1007,14 @@ export default class StatusPageAPI extends BaseAPI<
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const objectId: ObjectID = new ObjectID(
const statusPageId: ObjectID = new ObjectID(
req.params["statusPageId"] as string,
);
if (
!(await this.service.hasReadAccess(
objectId,
await CommonAPI.getDatabaseCommonInteractionProps(req),
req,
))
) {
throw new NotAuthenticatedException(
"You are not authenticated to access this status page",
);
}
await this.checkHasReadAccess({
statusPageId: statusPageId,
req: req,
});
const startDate: Date = OneUptimeDate.getSomeDaysAgo(90);
const endDate: Date = OneUptimeDate.getCurrentDate();
@@ -1051,7 +1029,7 @@ export default class StatusPageAPI extends BaseAPI<
statusPageGroups,
monitorsInGroup,
} = await this.getStatusPageResourcesAndTimelines({
statusPageId: objectId,
statusPageId: statusPageId,
startDateForMonitorTimeline: startDate,
endDateForMonitorTimeline: endDate,
});
@@ -1198,7 +1176,7 @@ export default class StatusPageAPI extends BaseAPI<
if (statusPage.showAnnouncementsOnStatusPage) {
activeAnnouncements = await StatusPageAnnouncementService.findBy({
query: {
statusPages: objectId as any,
statusPages: statusPageId as any,
showAnnouncementAt: QueryHelper.lessThan(today),
endAnnouncementAt: QueryHelper.greaterThanOrNull(today),
projectId: statusPage.projectId!,
@@ -1259,7 +1237,7 @@ export default class StatusPageAPI extends BaseAPI<
currentScheduledMaintenanceState: {
isOngoingState: true,
} as any,
statusPages: objectId as any,
statusPages: statusPageId as any,
projectId: statusPage.projectId!,
isVisibleOnStatusPage: true,
},
@@ -1285,7 +1263,7 @@ export default class StatusPageAPI extends BaseAPI<
currentScheduledMaintenanceState: {
isScheduledState: true,
} as any,
statusPages: objectId as any,
statusPages: statusPageId as any,
projectId: statusPage.projectId!,
isVisibleOnStatusPage: true,
},
@@ -1381,7 +1359,7 @@ export default class StatusPageAPI extends BaseAPI<
const statusPageHistoryChartBarColorRules: Array<StatusPageHistoryChartBarColorRule> =
await StatusPageHistoryChartBarColorRuleService.findBy({
query: {
statusPageId: objectId,
statusPageId: statusPageId,
},
select: {
_id: true,
@@ -1557,7 +1535,6 @@ export default class StatusPageAPI extends BaseAPI<
const response: JSONObject = await this.getIncidents(
objectId,
null,
await CommonAPI.getDatabaseCommonInteractionProps(req),
req,
);
@@ -1582,7 +1559,7 @@ export default class StatusPageAPI extends BaseAPI<
const response: JSONObject = await this.getScheduledMaintenanceEvents(
objectId,
null,
await CommonAPI.getDatabaseCommonInteractionProps(req),
req,
);
@@ -1607,7 +1584,7 @@ export default class StatusPageAPI extends BaseAPI<
const response: JSONObject = await this.getAnnouncements(
objectId,
null,
await CommonAPI.getDatabaseCommonInteractionProps(req),
req,
);
@@ -1636,7 +1613,6 @@ export default class StatusPageAPI extends BaseAPI<
const response: JSONObject = await this.getIncidents(
objectId,
incidentId,
await CommonAPI.getDatabaseCommonInteractionProps(req),
req,
);
@@ -1665,7 +1641,7 @@ export default class StatusPageAPI extends BaseAPI<
const response: JSONObject = await this.getScheduledMaintenanceEvents(
objectId,
scheduledMaintenanceId,
await CommonAPI.getDatabaseCommonInteractionProps(req),
req,
);
@@ -1694,7 +1670,7 @@ export default class StatusPageAPI extends BaseAPI<
const response: JSONObject = await this.getAnnouncements(
objectId,
announcementId,
await CommonAPI.getDatabaseCommonInteractionProps(req),
req,
);
@@ -1710,14 +1686,12 @@ export default class StatusPageAPI extends BaseAPI<
public async getScheduledMaintenanceEvents(
statusPageId: ObjectID,
scheduledMaintenanceId: ObjectID | null,
props: DatabaseCommonInteractionProps,
req: ExpressRequest,
): Promise<JSONObject> {
if (!(await this.service.hasReadAccess(statusPageId, props, req))) {
throw new NotAuthenticatedException(
"You are not authenticated to access this status page",
);
}
await this.checkHasReadAccess({
statusPageId: statusPageId,
req: req,
});
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
query: {
@@ -2028,14 +2002,12 @@ export default class StatusPageAPI extends BaseAPI<
public async getAnnouncements(
statusPageId: ObjectID,
announcementId: ObjectID | null,
props: DatabaseCommonInteractionProps,
req: ExpressRequest,
): Promise<JSONObject> {
if (!(await this.service.hasReadAccess(statusPageId, props, req))) {
throw new NotAuthenticatedException(
"You are not authenticated to access this status page",
);
}
await this.checkHasReadAccess({
statusPageId: statusPageId,
req: req,
});
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
query: {
@@ -2142,28 +2114,22 @@ export default class StatusPageAPI extends BaseAPI<
@CaptureSpan()
public async manageExistingSubscription(req: ExpressRequest): Promise<void> {
const objectId: ObjectID = new ObjectID(
const statusPageId: ObjectID = new ObjectID(
req.params["statusPageId"] as string,
);
logger.debug(`Managing Existing Subscription for Status Page: ${objectId}`);
logger.debug(
`Managing Existing Subscription for Status Page: ${statusPageId}`,
);
if (
!(await this.service.hasReadAccess(
objectId,
await CommonAPI.getDatabaseCommonInteractionProps(req),
req,
))
) {
logger.debug(`No read access to status page with ID: ${objectId}`);
throw new NotAuthenticatedException(
"You are not authenticated to access this status page",
);
}
await this.checkHasReadAccess({
statusPageId: statusPageId,
req: req,
});
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
query: {
_id: objectId.toString(),
_id: statusPageId.toString(),
},
select: {
_id: true,
@@ -2180,13 +2146,13 @@ export default class StatusPageAPI extends BaseAPI<
});
if (!statusPage) {
logger.debug(`Status page not found with ID: ${objectId}`);
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: ${objectId}`,
`Subscriber page not enabled for status page with ID: ${statusPageId}`,
);
throw new BadDataException(
"Subscribes not enabled for this status page.",
@@ -2200,7 +2166,7 @@ export default class StatusPageAPI extends BaseAPI<
!statusPage.enableEmailSubscribers
) {
logger.debug(
`Email subscribers not enabled for status page with ID: ${objectId}`,
`Email subscribers not enabled for status page with ID: ${statusPageId}`,
);
throw new BadDataException(
"Email subscribers not enabled for this status page.",
@@ -2209,7 +2175,7 @@ export default class StatusPageAPI extends BaseAPI<
if (req.body.data["subscriberPhone"] && !statusPage.enableSmsSubscribers) {
logger.debug(
`SMS subscribers not enabled for status page with ID: ${objectId}`,
`SMS subscribers not enabled for status page with ID: ${statusPageId}`,
);
throw new BadDataException(
"SMS subscribers not enabled for this status page.",
@@ -2223,7 +2189,7 @@ export default class StatusPageAPI extends BaseAPI<
!req.body.data["subscriberPhone"]
) {
logger.debug(
`No email or phone provided for subscription to status page with ID: ${objectId}`,
`No email or phone provided for subscription to status page with ID: ${statusPageId}`,
);
throw new BadDataException(
"Email or phone is required to subscribe to this status page.",
@@ -2245,7 +2211,7 @@ export default class StatusPageAPI extends BaseAPI<
statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
query: {
subscriberEmail: email,
statusPageId: objectId,
statusPageId: statusPageId,
},
select: {
_id: true,
@@ -2262,7 +2228,7 @@ export default class StatusPageAPI extends BaseAPI<
statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
query: {
subscriberPhone: phone,
statusPageId: objectId,
statusPageId: statusPageId,
},
select: {
_id: true,
@@ -2291,7 +2257,7 @@ export default class StatusPageAPI extends BaseAPI<
}
const statusPageURL: string =
await StatusPageService.getStatusPageURL(objectId);
await StatusPageService.getStatusPageURL(statusPageId);
const manageUrlink: string = StatusPageSubscriberService.getUnsubscribeLink(
URL.fromString(statusPageURL),
@@ -2300,7 +2266,7 @@ export default class StatusPageAPI extends BaseAPI<
const statusPages: Array<StatusPage> =
await StatusPageSubscriberService.getStatusPagesToSendNotification([
objectId,
statusPageId,
]);
for (const statusPage of statusPages) {
@@ -2375,18 +2341,10 @@ export default class StatusPageAPI extends BaseAPI<
logger.debug(`Subscribing to status page with ID: ${objectId}`);
if (
!(await this.service.hasReadAccess(
objectId,
await CommonAPI.getDatabaseCommonInteractionProps(req),
req,
))
) {
logger.debug(`No read access to status page with ID: ${objectId}`);
throw new NotAuthenticatedException(
"You are not authenticated to access this status page",
);
}
await this.checkHasReadAccess({
statusPageId: objectId,
req: req,
});
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
query: {
@@ -2612,17 +2570,10 @@ export default class StatusPageAPI extends BaseAPI<
req.params["statusPageId"] as string,
);
if (
!(await this.service.hasReadAccess(
objectId,
await CommonAPI.getDatabaseCommonInteractionProps(req),
req,
))
) {
throw new NotAuthenticatedException(
"You are not authenticated to access this status page",
);
}
await this.checkHasReadAccess({
statusPageId: objectId,
req: req,
});
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
query: {
@@ -2675,14 +2626,12 @@ export default class StatusPageAPI extends BaseAPI<
public async getIncidents(
statusPageId: ObjectID,
incidentId: ObjectID | null,
props: DatabaseCommonInteractionProps,
req: ExpressRequest,
): Promise<JSONObject> {
if (!(await this.service.hasReadAccess(statusPageId, props, req))) {
throw new NotAuthenticatedException(
"You are not authenticated to access this status page",
);
}
await this.checkHasReadAccess({
statusPageId: statusPageId,
req: req,
});
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
query: {
@@ -3235,4 +3184,26 @@ export default class StatusPageAPI extends BaseAPI<
monitorsInGroup,
};
}
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",
)
);
}
}
}

View File

@@ -1,16 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1748456937826 implements MigrationInterface {
public name = 'MigrationName1748456937826'
public name = "MigrationName1748456937826";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "StatusPage" ADD "ipWhitelist" text`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "StatusPage" DROP COLUMN "ipWhitelist"`);
}
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "StatusPage" ADD "ipWhitelist" text`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPage" DROP COLUMN "ipWhitelist"`,
);
}
}

View File

@@ -271,5 +271,5 @@ export default [
MigrationName1744809770336,
MigrationName1747305098533,
MigrationName1747674762672,
MigrationName1748456937826
MigrationName1748456937826,
];

View File

@@ -58,6 +58,9 @@ import SortOrder from "../../Types/BaseDatabase/SortOrder";
import UptimeUtil from "../../Utils/Uptime/UptimeUtil";
import UptimePrecision from "../../Types/StatusPage/UptimePrecision";
import IP from "../../Types/IP/IP";
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
import ForbiddenException from "../../Types/Exception/ForbiddenException";
import CommonAPI from "../API/CommonAPI";
export interface StatusPageReportItem {
resourceName: string;
@@ -83,7 +86,7 @@ export class Service extends DatabaseService<StatusPage> {
@CaptureSpan()
protected override async onBeforeCreate(
createBy: CreateBy<StatusPage>,
createBy: CreateBy<StatusPage>
): Promise<OnCreate<StatusPage>> {
if (!createBy.data.projectId) {
throw new BadDataException("projectId is required");
@@ -92,12 +95,12 @@ export class Service extends DatabaseService<StatusPage> {
// if the project is on the free plan, then only allow 1 status page.
if (IsBillingEnabled) {
const currentPlan: CurrentPlan = await ProjectService.getCurrentPlan(
createBy.data.projectId,
createBy.data.projectId
);
if (currentPlan.isSubscriptionUnpaid) {
throw new BadDataException(
"Your subscription is unpaid. Please update your payment method and pay all the outstanding invoices to add more status pages.",
"Your subscription is unpaid. Please update your payment method and pay all the outstanding invoices to add more status pages."
);
}
@@ -113,7 +116,7 @@ export class Service extends DatabaseService<StatusPage> {
if (statusPageCount.toNumber() >= AllowedStatusPageCountInFreePlan) {
throw new BadDataException(
`You have reached the maximum allowed status page limit for the free plan. Please upgrade your plan to add more status pages.`,
`You have reached the maximum allowed status page limit for the free plan. Please upgrade your plan to add more status pages.`
);
}
}
@@ -166,7 +169,7 @@ export class Service extends DatabaseService<StatusPage> {
@CaptureSpan()
protected override async onCreateSuccess(
onCreate: OnCreate<StatusPage>,
createdItem: StatusPage,
createdItem: StatusPage
): Promise<StatusPage> {
// add owners.
@@ -185,7 +188,7 @@ export class Service extends DatabaseService<StatusPage> {
(onCreate.createBy.miscDataProps["ownerTeams"] as Array<ObjectID>) ||
[],
false,
onCreate.createBy.props,
onCreate.createBy.props
);
}
@@ -253,7 +256,7 @@ export class Service extends DatabaseService<StatusPage> {
const isUserAlreadyAdded: User | undefined = users.find(
(user: User) => {
return user.id!.toString() === teamUser.id!.toString();
},
}
);
if (!isUserAlreadyAdded) {
@@ -272,7 +275,7 @@ export class Service extends DatabaseService<StatusPage> {
userIds: Array<ObjectID>,
teamIds: Array<ObjectID>,
notifyOwners: boolean,
props: DatabaseCommonInteractionProps,
props: DatabaseCommonInteractionProps
): Promise<void> {
for (let teamId of teamIds) {
if (typeof teamId === Typeof.String) {
@@ -310,42 +313,29 @@ export class Service extends DatabaseService<StatusPage> {
@CaptureSpan()
public async getStatusPageLinkInDashboard(
projectId: ObjectID,
statusPageId: ObjectID,
statusPageId: ObjectID
): Promise<URL> {
const dahboardUrl: URL = await DatabaseConfig.getDashboardUrl();
return URL.fromString(dahboardUrl.toString()).addRoute(
`/${projectId.toString()}/status-pages/${statusPageId.toString()}`,
`/${projectId.toString()}/status-pages/${statusPageId.toString()}`
);
}
@CaptureSpan()
public async hasReadAccess(
statusPageId: ObjectID,
props: DatabaseCommonInteractionProps,
req: ExpressRequest,
): Promise<boolean> {
public async hasReadAccess(data: {
statusPageId: ObjectID;
req: ExpressRequest;
}): Promise<{
hasReadAccess: boolean;
error?: NotAuthenticatedException | ForbiddenException;
}> {
const statusPageId: ObjectID = data.statusPageId;
const req: ExpressRequest = data.req;
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
try {
// token decode.
const token: string | undefined = CookieUtil.getCookieFromExpressRequest(
req,
CookieUtil.getUserTokenKey(statusPageId),
);
if (token) {
try {
const decoded: JSONWebTokenData = JSONWebToken.decode(
token as string,
);
if (decoded.statusPageId?.toString() === statusPageId.toString()) {
return true;
}
} catch (err) {
logger.error(err);
}
}
// get status page by id.
const statusPage: StatusPage | null = await this.findOneById({
id: statusPageId,
@@ -359,10 +349,6 @@ export class Service extends DatabaseService<StatusPage> {
},
});
if (statusPage && statusPage.isPublicStatusPage) {
return true;
}
if (statusPage?.ipWhitelist && statusPage.ipWhitelist.length > 0) {
const ipWhitelist: Array<string> = statusPage.ipWhitelist?.split("\n");
@@ -375,7 +361,12 @@ export class Service extends DatabaseService<StatusPage> {
if (!ipAccessedFrom) {
logger.error("IP address not found in request.");
return false;
return {
hasReadAccess: false,
error: new ForbiddenException(
"Unable to verify IP address for status page access."
),
};
}
const isIPWhitelisted: boolean = IP.isInWhitelist({
@@ -383,13 +374,42 @@ export class Service extends DatabaseService<StatusPage> {
whitelist: ipWhitelist,
});
if (isIPWhitelisted) {
return true;
if (!isIPWhitelisted) {
return {
hasReadAccess: false,
error: new ForbiddenException(
`Your IP address ${ipAccessedFrom} is blocked from accessing this status page.`
),
};
}
}
if (statusPage && statusPage.isPublicStatusPage) {
return {
hasReadAccess: true,
};
}
// token decode.
const token: string | undefined = CookieUtil.getCookieFromExpressRequest(
req,
CookieUtil.getUserTokenKey(statusPageId)
);
if (token) {
try {
const decoded: JSONWebTokenData = JSONWebToken.decode(
token as string
);
if (decoded.statusPageId?.toString() === statusPageId.toString()) {
return {
hasReadAccess: true,
};
}
} catch (err) {
logger.error(err);
}
logger.error(
`IP ${ipAccessedFrom} is not whitelisted for status page ${statusPageId.toString()}.`,
);
return false;
}
// if it does not have public access, check if this user has access.
@@ -407,13 +427,20 @@ export class Service extends DatabaseService<StatusPage> {
});
if (items.length > 0) {
return true;
return {
hasReadAccess: true,
};
}
} catch (err) {
logger.error(err);
}
return false;
return {
hasReadAccess: false,
error: new NotAuthenticatedException(
"You do not have access to this status page. Please login to view the status page."
),
};
}
@CaptureSpan()
@@ -479,7 +506,7 @@ export class Service extends DatabaseService<StatusPage> {
props: {
isRoot: true,
},
}),
})
);
// sort monitorStatusTimelines by createdAt.
@@ -490,7 +517,7 @@ export class Service extends DatabaseService<StatusPage> {
}
return b.createdAt!.getTime() - a.createdAt!.getTime();
},
}
);
}
@@ -571,7 +598,7 @@ export class Service extends DatabaseService<StatusPage> {
@CaptureSpan()
protected override async onBeforeUpdate(
updateBy: UpdateBy<StatusPage>,
updateBy: UpdateBy<StatusPage>
): Promise<OnUpdate<StatusPage>> {
// is enabling SMS subscribers.
@@ -595,7 +622,7 @@ export class Service extends DatabaseService<StatusPage> {
if (!isSMSEnabled) {
throw new BadDataException(
"SMS notifications are not enabled for this project. Please enable SMS notifications in the Project Settings > Notifications Settings.",
"SMS notifications are not enabled for this project. Please enable SMS notifications in the Project Settings > Notifications Settings."
);
}
}
@@ -627,13 +654,13 @@ export class Service extends DatabaseService<StatusPage> {
const reportRecurringInterval: Recurring | undefined =
Recurring.fromJSON(
(updateBy.data.reportRecurringInterval as Recurring) ||
statusPage.reportRecurringInterval,
statusPage.reportRecurringInterval
);
if (rerportStartDate && reportRecurringInterval) {
const nextReportDate: Date = Recurring.getNextDate(
rerportStartDate,
reportRecurringInterval,
reportRecurringInterval
);
updateBy.data.sendNextReportBy = nextReportDate;
}
@@ -680,12 +707,12 @@ export class Service extends DatabaseService<StatusPage> {
type SendEmailFunction = (
email: Email,
unsubscribeUrl: URL | null,
unsubscribeUrl: URL | null
) => Promise<void>;
const sendEmail: SendEmailFunction = async (
email: Email,
unsubscribeUrl: URL | null,
unsubscribeUrl: URL | null
): Promise<void> => {
// send email here.
@@ -716,10 +743,10 @@ export class Service extends DatabaseService<StatusPage> {
},
{
mailServer: ProjectSMTPConfigService.toEmailServer(
statuspage.smtpConfig,
statuspage.smtpConfig
),
projectId: statuspage.projectId,
},
}
).catch((err: Error) => {
logger.error(err);
});
@@ -736,7 +763,7 @@ export class Service extends DatabaseService<StatusPage> {
{
isRoot: true,
ignoreHooks: true,
},
}
);
for (const subscriber of subscribers) {
@@ -754,13 +781,13 @@ export class Service extends DatabaseService<StatusPage> {
const unsubscribeUrl: string =
StatusPageSubscriberService.getUnsubscribeLink(
URL.fromString(statusPageURL),
subscriber.id!,
subscriber.id!
).toString();
if (subscriber.subscriberEmail) {
await sendEmail(
subscriber.subscriberEmail,
URL.fromString(unsubscribeUrl),
URL.fromString(unsubscribeUrl)
);
}
@@ -847,7 +874,7 @@ export class Service extends DatabaseService<StatusPage> {
if (resource.monitorGroupId) {
const groupId: string = resource.monitorGroupId.toString();
monitorIdsForThisResource = monitorIdsForThisResource.concat(
monitors.monitorsInGroup[groupId] || [],
monitors.monitorsInGroup[groupId] || []
);
}
@@ -861,14 +888,14 @@ export class Service extends DatabaseService<StatusPage> {
const uptimePercent: number = UptimeUtil.calculateUptimePercentage(
timelineForThisResource,
resource.uptimePercentPrecision || UptimePrecision.TWO_DECIMAL,
statusPage.downtimeMonitorStatuses!,
statusPage.downtimeMonitorStatuses!
);
const downtime: {
totalDowntimeInSeconds: number;
totalSecondsInTimePeriod: number;
} = UptimeUtil.getTotalDowntimeInSeconds(
timelineForThisResource,
statusPage.downtimeMonitorStatuses!,
statusPage.downtimeMonitorStatuses!
);
const reportItem: StatusPageReportItem = {
@@ -881,7 +908,7 @@ export class Service extends DatabaseService<StatusPage> {
uptimePercentAsString: `${uptimePercent}%`,
downtimeInHoursAndMinutes:
OneUptimeDate.convertMinutesToDaysHoursAndMinutes(
Math.ceil(downtime.totalDowntimeInSeconds / 60),
Math.ceil(downtime.totalDowntimeInSeconds / 60)
),
};
@@ -900,7 +927,7 @@ export class Service extends DatabaseService<StatusPage> {
totalSecondsInTimePeriod: number;
} = UptimeUtil.getTotalDowntimeInSeconds(
timeline,
statusPage.downtimeMonitorStatuses!,
statusPage.downtimeMonitorStatuses!
);
return {
@@ -911,7 +938,7 @@ export class Service extends DatabaseService<StatusPage> {
resources: reportItems,
totalDowntimeInHoursAndMinutes:
OneUptimeDate.convertMinutesToDaysHoursAndMinutes(
Math.ceil(totalDowntimeInSeconds.totalDowntimeInSeconds / 60),
Math.ceil(totalDowntimeInSeconds.totalDowntimeInSeconds / 60)
),
};
}
@@ -924,7 +951,7 @@ export class Service extends DatabaseService<StatusPage> {
const today: Date = OneUptimeDate.getCurrentDate();
const historyDays: Date = OneUptimeDate.getSomeDaysAgo(
data.historyDays || 14,
data.historyDays || 14
);
const incidentCount: PositiveNumber = await IncidentService.countBy({
@@ -1069,7 +1096,7 @@ export class Service extends DatabaseService<StatusPage> {
return statusPageResources.sort(
(a: StatusPageResource, b: StatusPageResource) => {
return a.order! - b.order!;
},
}
);
}
}

View File

@@ -7,6 +7,7 @@ enum ExceptionCode {
WebRequestException = 6,
BadDataException = 400,
BadRequestException = 400,
ForbiddenException = 403,
UnabletoReachServerException = 415,
ServerException = 500,
NotAuthorizedException = 403,

View File

@@ -0,0 +1,8 @@
import Exception from "./Exception";
import ExceptionCode from "./ExceptionCode";
export default class ForbiddenException extends Exception {
public constructor(message: string) {
super(ExceptionCode.ForbiddenException, message);
}
}

View File

@@ -55,7 +55,7 @@ const StatusPageDelete: FunctionComponent<
cardProps={{
title: "IP Whitelist",
description:
"IP Whitelist for this status page. This will only apply if the status page is not visible to public. ",
"IP Whitelist for this status page. If the status page is public then only IP addresses in this whitelist will be able to access the status page. If the status page is not public then only users who are registered as Private Users and who have access from the IP addresses in this whitelist will be able to access the status page.",
}}
editButtonText="Edit IP Whitelist"
isEditable={true}