mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Merge branch 'master' of github.com:OneUptime/oneuptime
This commit is contained in:
@@ -19,6 +19,7 @@ import StatusPagePrivateUserSessionService, {
|
||||
SessionMetadata as StatusPageSessionMetadata,
|
||||
} from "Common/Server/Services/StatusPagePrivateUserSessionService";
|
||||
import CookieUtil from "Common/Server/Utils/Cookie";
|
||||
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
@@ -33,11 +34,57 @@ import Response from "Common/Server/Utils/Response";
|
||||
import StatusPage from "Common/Models/DatabaseModels/StatusPage";
|
||||
import StatusPagePrivateUser from "Common/Models/DatabaseModels/StatusPagePrivateUser";
|
||||
import StatusPagePrivateUserSession from "Common/Models/DatabaseModels/StatusPagePrivateUserSession";
|
||||
import { MASTER_PASSWORD_COOKIE_IDENTIFIER } from "Common/Types/StatusPage/MasterPassword";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
|
||||
|
||||
type MasterPasswordAuthInput = {
|
||||
req: ExpressRequest;
|
||||
res?: ExpressResponse;
|
||||
statusPageId: ObjectID;
|
||||
};
|
||||
|
||||
const hasValidMasterPasswordSession: (
|
||||
data: MasterPasswordAuthInput,
|
||||
) => boolean = (data: MasterPasswordAuthInput): boolean => {
|
||||
const token: string | undefined = CookieUtil.getCookieFromExpressRequest(
|
||||
data.req,
|
||||
CookieUtil.getStatusPageMasterPasswordKey(data.statusPageId),
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
|
||||
|
||||
return (
|
||||
payload["statusPageId"] === data.statusPageId.toString() &&
|
||||
payload["type"] === MASTER_PASSWORD_COOKIE_IDENTIFIER
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const respondWithMasterPasswordAccess: (
|
||||
data: MasterPasswordAuthInput & { res: ExpressResponse },
|
||||
) => boolean = (
|
||||
data: MasterPasswordAuthInput & { res: ExpressResponse },
|
||||
): boolean => {
|
||||
if (!hasValidMasterPasswordSession(data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Response.sendEmptySuccessResponse(data.req, data.res);
|
||||
return true;
|
||||
};
|
||||
|
||||
type FinalizeStatusPageLoginInput = {
|
||||
req: ExpressRequest;
|
||||
res: ExpressResponse;
|
||||
@@ -123,6 +170,7 @@ router.post(
|
||||
|
||||
CookieUtil.removeCookie(res, CookieUtil.getUserTokenKey(statusPageId));
|
||||
CookieUtil.removeCookie(res, CookieUtil.getRefreshTokenKey(statusPageId));
|
||||
CookieUtil.removeStatusPageMasterPasswordCookie(res, statusPageId);
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
@@ -157,6 +205,16 @@ router.post(
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
if (
|
||||
respondWithMasterPasswordAccess({
|
||||
req,
|
||||
res,
|
||||
statusPageId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
@@ -178,6 +236,16 @@ router.post(
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
if (
|
||||
respondWithMasterPasswordAccess({
|
||||
req,
|
||||
res,
|
||||
statusPageId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
@@ -199,6 +267,16 @@ router.post(
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
if (
|
||||
respondWithMasterPasswordAccess({
|
||||
req,
|
||||
res,
|
||||
statusPageId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
@@ -223,6 +301,16 @@ router.post(
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
if (
|
||||
respondWithMasterPasswordAccess({
|
||||
req,
|
||||
res,
|
||||
statusPageId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
@@ -244,6 +332,16 @@ router.post(
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
if (
|
||||
respondWithMasterPasswordAccess({
|
||||
req,
|
||||
res,
|
||||
statusPageId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
@@ -279,6 +377,16 @@ router.post(
|
||||
CookieUtil.getRefreshTokenKey(statusPageId),
|
||||
);
|
||||
|
||||
if (
|
||||
respondWithMasterPasswordAccess({
|
||||
req,
|
||||
res,
|
||||
statusPageId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
|
||||
@@ -30,6 +30,7 @@ import { JSONObject } from "../../Types/JSON";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import Timezone from "../../Types/Timezone";
|
||||
import HashedString from "../../Types/HashedString";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
@@ -883,6 +884,78 @@ export default class StatusPage extends BaseModel {
|
||||
})
|
||||
public isPublicStatusPage?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectStatusPage,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectStatusPage,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditProjectStatusPage,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Enable Master Password",
|
||||
description:
|
||||
"Require visitors to enter a master password before viewing a private status page.",
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public enableMasterPassword?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectStatusPage,
|
||||
],
|
||||
|
||||
// This is a hashed column. So, reading the value is does not affect anything.
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectStatusPage,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditProjectStatusPage,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
title: "Master Password",
|
||||
description:
|
||||
"Password required to unlock a private status page. This value is stored as a secure hash.",
|
||||
hashed: true,
|
||||
type: TableColumnType.HashedString,
|
||||
placeholder: "Enter a new master password",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.HashedString,
|
||||
length: ColumnLength.HashedString,
|
||||
nullable: true,
|
||||
transformer: HashedString.getDatabaseTransformer(),
|
||||
})
|
||||
public masterPassword?: HashedString = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -49,6 +49,7 @@ 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";
|
||||
@@ -87,10 +88,13 @@ 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,
|
||||
@@ -798,6 +802,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
enableMicrosoftTeamsSubscribers: true,
|
||||
enableSmsSubscribers: true,
|
||||
isPublicStatusPage: true,
|
||||
enableMasterPassword: true,
|
||||
allowSubscribersToChooseResources: true,
|
||||
allowSubscribersToChooseEventTypes: true,
|
||||
requireSsoForLogin: true,
|
||||
@@ -910,6 +915,80 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
},
|
||||
);
|
||||
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1763643080445 implements MigrationInterface {
|
||||
public name = "MigrationName1763643080445";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" ADD "enableMasterPassword" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" ADD "masterPassword" character varying(64)`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" DROP COLUMN "masterPassword"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" DROP COLUMN "enableMasterPassword"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,7 @@ import { MigrationName1762890441920 } from "./1762890441920-MigrationName";
|
||||
import { MigrationName1763471659817 } from "./1763471659817-MigrationName";
|
||||
import { MigrationName1763477560906 } from "./1763477560906-MigrationName";
|
||||
import { MigrationName1763480947474 } from "./1763480947474-MigrationName";
|
||||
import { MigrationName1763643080445 } from "./1763643080445-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -374,4 +375,5 @@ export default [
|
||||
MigrationName1763471659817,
|
||||
MigrationName1763477560906,
|
||||
MigrationName1763480947474,
|
||||
MigrationName1763643080445,
|
||||
];
|
||||
|
||||
@@ -47,6 +47,7 @@ import ProjectSMTPConfigService from "./ProjectSmtpConfigService";
|
||||
import StatusPageResource from "../../Models/DatabaseModels/StatusPageResource";
|
||||
import StatusPageResourceService from "./StatusPageResourceService";
|
||||
import Dictionary from "../../Types/Dictionary";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import MonitorGroupResource from "../../Models/DatabaseModels/MonitorGroupResource";
|
||||
import MonitorGroupResourceService from "./MonitorGroupResourceService";
|
||||
import QueryHelper from "../Types/Database/QueryHelper";
|
||||
@@ -61,6 +62,11 @@ import IP from "../../Types/IP/IP";
|
||||
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
|
||||
import ForbiddenException from "../../Types/Exception/ForbiddenException";
|
||||
import CommonAPI from "../API/CommonAPI";
|
||||
import MasterPasswordRequiredException from "../../Types/Exception/MasterPasswordRequiredException";
|
||||
import {
|
||||
MASTER_PASSWORD_COOKIE_IDENTIFIER,
|
||||
MASTER_PASSWORD_REQUIRED_MESSAGE,
|
||||
} from "../../Types/StatusPage/MasterPassword";
|
||||
|
||||
export interface StatusPageReportItem {
|
||||
resourceName: string;
|
||||
@@ -389,6 +395,8 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
_id: true,
|
||||
isPublicStatusPage: true,
|
||||
ipWhitelist: true,
|
||||
enableMasterPassword: true,
|
||||
masterPassword: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -462,6 +470,34 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
}
|
||||
}
|
||||
|
||||
const shouldEnforceMasterPassword: boolean = Boolean(
|
||||
statusPage &&
|
||||
statusPage.enableMasterPassword &&
|
||||
statusPage.masterPassword &&
|
||||
!statusPage.isPublicStatusPage,
|
||||
);
|
||||
|
||||
if (shouldEnforceMasterPassword) {
|
||||
const hasValidMasterPassword: boolean =
|
||||
this.hasValidMasterPasswordCookie({
|
||||
req,
|
||||
statusPageId,
|
||||
});
|
||||
|
||||
if (hasValidMasterPassword) {
|
||||
return {
|
||||
hasReadAccess: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasReadAccess: false,
|
||||
error: new MasterPasswordRequiredException(
|
||||
MASTER_PASSWORD_REQUIRED_MESSAGE,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// if it does not have public access, check if this user has access.
|
||||
|
||||
const items: Array<StatusPage> = await this.findBy({
|
||||
@@ -493,6 +529,33 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
};
|
||||
}
|
||||
|
||||
private hasValidMasterPasswordCookie(data: {
|
||||
req: ExpressRequest;
|
||||
statusPageId: ObjectID;
|
||||
}): boolean {
|
||||
const token: string | undefined = CookieUtil.getCookieFromExpressRequest(
|
||||
data.req,
|
||||
CookieUtil.getStatusPageMasterPasswordKey(data.statusPageId),
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
|
||||
|
||||
return (
|
||||
payload["statusPageId"] === data.statusPageId.toString() &&
|
||||
payload["type"] === MASTER_PASSWORD_COOKIE_IDENTIFIER
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async getMonitorStatusTimelineForStatusPage(data: {
|
||||
monitorIds: Array<ObjectID>;
|
||||
|
||||
@@ -8,6 +8,10 @@ import StatusPagePrivateUser from "../../Models/DatabaseModels/StatusPagePrivate
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
import PositiveNumber from "../../Types/PositiveNumber";
|
||||
import CookieName from "../../Types/CookieName";
|
||||
import {
|
||||
MASTER_PASSWORD_COOKIE_IDENTIFIER,
|
||||
MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
|
||||
} from "../../Types/StatusPage/MasterPassword";
|
||||
import CaptureSpan from "./Telemetry/CaptureSpan";
|
||||
|
||||
export default class CookieUtil {
|
||||
@@ -233,6 +237,34 @@ export default class CookieUtil {
|
||||
return token;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static setStatusPageMasterPasswordCookie(data: {
|
||||
expressResponse: ExpressResponse;
|
||||
statusPageId: ObjectID;
|
||||
}): void {
|
||||
const expiresInDays: PositiveNumber = new PositiveNumber(
|
||||
MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
|
||||
);
|
||||
|
||||
const token: string = JSONWebToken.signJsonPayload(
|
||||
{
|
||||
statusPageId: data.statusPageId.toString(),
|
||||
type: MASTER_PASSWORD_COOKIE_IDENTIFIER,
|
||||
},
|
||||
OneUptimeDate.getSecondsInDays(expiresInDays),
|
||||
);
|
||||
|
||||
CookieUtil.setCookie(
|
||||
data.expressResponse,
|
||||
CookieUtil.getStatusPageMasterPasswordKey(data.statusPageId),
|
||||
token,
|
||||
{
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(expiresInDays),
|
||||
httpOnly: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static setCookie(
|
||||
res: ExpressResponse,
|
||||
@@ -280,6 +312,17 @@ export default class CookieUtil {
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static removeStatusPageMasterPasswordCookie(
|
||||
res: ExpressResponse,
|
||||
statusPageId: ObjectID,
|
||||
): void {
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getStatusPageMasterPasswordKey(statusPageId),
|
||||
);
|
||||
}
|
||||
|
||||
// get all cookies with express request
|
||||
@CaptureSpan()
|
||||
public static getAllCookies(req: ExpressRequest): Dictionary<string> {
|
||||
@@ -304,6 +347,11 @@ export default class CookieUtil {
|
||||
return `${CookieName.RefreshToken}-${id.toString()}`;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static getStatusPageMasterPasswordKey(id: ObjectID): string {
|
||||
return `${CookieName.StatusPageMasterPassword}-${id.toString()}`;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static getUserSSOKey(id: ObjectID): string {
|
||||
return `${this.getSSOKey()}${id.toString()}`;
|
||||
|
||||
@@ -7,6 +7,7 @@ enum CookieName {
|
||||
Timezone = "user-timezone",
|
||||
IsMasterAdmin = "user-is-master-admin",
|
||||
ProfilePicID = "user-profile-pic-id",
|
||||
StatusPageMasterPassword = "status-page-master-password",
|
||||
}
|
||||
|
||||
export default CookieName;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import NotAuthenticatedException from "./NotAuthenticatedException";
|
||||
|
||||
export default class MasterPasswordRequiredException extends NotAuthenticatedException {
|
||||
public constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
10
Common/Types/StatusPage/MasterPassword.ts
Normal file
10
Common/Types/StatusPage/MasterPassword.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const MASTER_PASSWORD_REQUIRED_MESSAGE: string =
|
||||
"Master password required";
|
||||
|
||||
export const MASTER_PASSWORD_INVALID_MESSAGE: string =
|
||||
"Invalid master password. Please try again.";
|
||||
|
||||
export const MASTER_PASSWORD_COOKIE_IDENTIFIER: string =
|
||||
"status-page-master-password";
|
||||
|
||||
export const MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS: number = 7;
|
||||
@@ -50,6 +50,70 @@ const StatusPageDelete: FunctionComponent<
|
||||
}}
|
||||
/>
|
||||
|
||||
<CardModelDetail<StatusPage>
|
||||
name="Status Page > Master Password"
|
||||
cardProps={{
|
||||
title: "Master Password",
|
||||
description:
|
||||
"Rotate the password required to unlock a private status page. This value is stored as a secure hash and cannot be retrieved. When master password is enabled, SSO/SCIM and Email + Password authentication are disabled.",
|
||||
}}
|
||||
editButtonText="Update Master Password"
|
||||
isEditable={true}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
enableMasterPassword: true,
|
||||
},
|
||||
title: "Require Master Password",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
description:
|
||||
"When enabled, visitors must enter the master password before viewing a private status page.",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
masterPassword: true,
|
||||
},
|
||||
title: "Master Password",
|
||||
fieldType: FormFieldSchemaType.Password,
|
||||
required: false,
|
||||
placeholder: "Enter a new master password",
|
||||
description:
|
||||
"Updating this value immediately replaces the existing master password.",
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
showDetailsInNumberOfColumns: 1,
|
||||
modelType: StatusPage,
|
||||
id: "model-detail-status-page-master-password",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
enableMasterPassword: true,
|
||||
},
|
||||
fieldType: FieldType.Boolean,
|
||||
title: "Require Master Password",
|
||||
placeholder: "No",
|
||||
},
|
||||
{
|
||||
title: "Master Password",
|
||||
fieldType: FieldType.Element,
|
||||
placeholder: "Hidden",
|
||||
getElement: (): ReactElement => {
|
||||
return (
|
||||
<p className="text-sm text-gray-500">
|
||||
For security reasons, the current master password is never
|
||||
displayed. Use the update button to set a new password at
|
||||
any time.
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
|
||||
<CardModelDetail<StatusPage>
|
||||
name="Status Page > IP Whitelist"
|
||||
cardProps={{
|
||||
|
||||
@@ -6,17 +6,64 @@ import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
|
||||
import Pill from "Common/UI/Components/Pill/Pill";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import StatusPage from "Common/Models/DatabaseModels/StatusPage";
|
||||
import StatusPagePrivateUser from "Common/Models/DatabaseModels/StatusPagePrivateUser";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
import Alert, { AlertType } from "Common/UI/Components/Alerts/Alert";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
|
||||
const StatusPageDelete: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
const [isMasterPasswordEnabled, setIsMasterPasswordEnabled] =
|
||||
useState<boolean>(false);
|
||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStatusPage: () => Promise<void> = async (): Promise<void> => {
|
||||
try {
|
||||
const statusPage: StatusPage | null = await ModelAPI.getItem({
|
||||
modelType: StatusPage,
|
||||
id: modelId,
|
||||
select: {
|
||||
enableMasterPassword: true,
|
||||
},
|
||||
});
|
||||
|
||||
setIsMasterPasswordEnabled(Boolean(statusPage?.enableMasterPassword));
|
||||
setFetchError(null);
|
||||
} catch (error) {
|
||||
const newErrorMessage: string =
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: "Failed to fetch status page details.";
|
||||
setFetchError(newErrorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
void fetchStatusPage();
|
||||
}, [modelId]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{fetchError && (
|
||||
<Alert className="mb-5" type={AlertType.DANGER} title={fetchError} />
|
||||
)}
|
||||
{isMasterPasswordEnabled && (
|
||||
<Alert
|
||||
className="mb-5"
|
||||
type={AlertType.INFO}
|
||||
title="Master password is enabled for this status page. Private users authentication is disabled while the master password is active."
|
||||
/>
|
||||
)}
|
||||
<ModelTable<StatusPagePrivateUser>
|
||||
modelType={StatusPagePrivateUser}
|
||||
id="status-page-group"
|
||||
|
||||
@@ -21,6 +21,7 @@ import { SubscribePageProps } from "./Pages/Subscribe/SubscribePageUtils";
|
||||
import { ComponentProps as ForgotPasswordComponentProps } from "./Pages/Accounts/ForgotPassword";
|
||||
import { ComponentProps as LoginComponentProps } from "./Pages/Accounts/Login";
|
||||
import { ComponentProps as ResetPasswordComponentProps } from "./Pages/Accounts/ResetPassword";
|
||||
import { ComponentProps as MasterPasswordComponentProps } from "./Pages/Accounts/MasterPassword";
|
||||
import { ComponentProps as SsoComponentProps } from "./Pages/Accounts/SSO";
|
||||
import PageComponentProps from "./Pages/PageComponentProps";
|
||||
|
||||
@@ -43,6 +44,11 @@ const ResetPassword: React.LazyExoticComponent<
|
||||
> = lazy(() => {
|
||||
return import("./Pages/Accounts/ResetPassword");
|
||||
});
|
||||
const MasterPassword: React.LazyExoticComponent<
|
||||
React.FunctionComponent<MasterPasswordComponentProps>
|
||||
> = lazy(() => {
|
||||
return import("./Pages/Accounts/MasterPassword");
|
||||
});
|
||||
const Sso: React.LazyExoticComponent<
|
||||
React.FunctionComponent<SsoComponentProps>
|
||||
> = lazy(() => {
|
||||
@@ -214,6 +220,13 @@ const App: () => JSX.Element = () => {
|
||||
"statusPage.isPublicStatusPage",
|
||||
) as boolean;
|
||||
|
||||
const enableMasterPassword: boolean = Boolean(
|
||||
JSONFunctions.getJSONValueInPath(
|
||||
masterpage || {},
|
||||
"statusPage.enableMasterPassword",
|
||||
) as boolean,
|
||||
);
|
||||
|
||||
const enableEmailSubscribers: boolean =
|
||||
JSONFunctions.getJSONValueInPath(
|
||||
masterpage || {},
|
||||
@@ -261,6 +274,7 @@ const App: () => JSX.Element = () => {
|
||||
|
||||
StatusPageUtil.setIsPrivateStatusPage(isPrivateStatusPage);
|
||||
setIsPrivateStatusPage(isPrivateStatusPage);
|
||||
StatusPageUtil.setRequiresMasterPassword(enableMasterPassword);
|
||||
|
||||
const statusPageId: string | null = JSONFunctions.getJSONValueInPath(
|
||||
masterpage || {},
|
||||
@@ -334,6 +348,16 @@ const App: () => JSX.Element = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.MASTER_PASSWORD]?.toString() || ""}
|
||||
element={
|
||||
<MasterPassword
|
||||
statusPageName={statusPageName}
|
||||
logoFileId={new ObjectID(statusPageLogoFileId)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.SSO]?.toString() || ""}
|
||||
element={
|
||||
@@ -831,6 +855,16 @@ const App: () => JSX.Element = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.PREVIEW_MASTER_PASSWORD]?.toString() || ""}
|
||||
element={
|
||||
<MasterPassword
|
||||
statusPageName={statusPageName}
|
||||
logoFileId={new ObjectID(statusPageLogoFileId)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.PREVIEW_RESET_PASSWORD]?.toString() || ""}
|
||||
element={
|
||||
|
||||
@@ -244,7 +244,8 @@ const DashboardMasterPage: FunctionComponent<ComponentProps> = (
|
||||
Navigation.getCurrentRoute().toString().includes("forgot-password") ||
|
||||
Navigation.getCurrentRoute().toString().includes("reset-password") ||
|
||||
Navigation.getCurrentRoute().toString().includes("sso") ||
|
||||
Navigation.getCurrentRoute().toString().includes("forbidden")
|
||||
Navigation.getCurrentRoute().toString().includes("forbidden") ||
|
||||
Navigation.getCurrentRoute().toString().includes("master-password")
|
||||
) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,20 @@ export interface ComponentProps {
|
||||
const LoginPage: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
) => {
|
||||
const statusPageId: ObjectID | null = StatusPageUtil.getStatusPageId();
|
||||
const statusPageIdString: string | undefined = statusPageId?.toString();
|
||||
const requiresMasterPasswordLock: boolean =
|
||||
StatusPageUtil.isPrivateStatusPage() &&
|
||||
StatusPageUtil.requiresMasterPassword() &&
|
||||
!StatusPageUtil.isMasterPasswordValidated();
|
||||
|
||||
useEffect(() => {
|
||||
if (props.forceSSO && StatusPageUtil.getStatusPageId()) {
|
||||
if (requiresMasterPasswordLock || !statusPageId) {
|
||||
StatusPageUtil.navigateToMasterPasswordPage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.forceSSO) {
|
||||
if (Navigation.getQueryStringByName("redirectUrl")) {
|
||||
// forward redirect url to sso page
|
||||
|
||||
@@ -36,11 +48,11 @@ const LoginPage: FunctionComponent<ComponentProps> = (
|
||||
(!StatusPageUtil.isPreviewPage()
|
||||
? RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SSO]!,
|
||||
StatusPageUtil.getStatusPageId()!,
|
||||
statusPageId,
|
||||
)
|
||||
: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.PREVIEW_SSO]!,
|
||||
StatusPageUtil.getStatusPageId()!,
|
||||
statusPageId,
|
||||
)
|
||||
).toString() +
|
||||
`?redirectUrl=${Navigation.getQueryStringByName("redirectUrl")}`,
|
||||
@@ -49,31 +61,33 @@ const LoginPage: FunctionComponent<ComponentProps> = (
|
||||
Navigation.navigate(navRoute);
|
||||
} else {
|
||||
const navRoute: Route = !StatusPageUtil.isPreviewPage()
|
||||
? RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SSO]!,
|
||||
StatusPageUtil.getStatusPageId()!,
|
||||
)
|
||||
? RouteUtil.populateRouteParams(RouteMap[PageMap.SSO]!, statusPageId)
|
||||
: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.PREVIEW_SSO]!,
|
||||
StatusPageUtil.getStatusPageId()!,
|
||||
statusPageId,
|
||||
);
|
||||
|
||||
Navigation.navigate(navRoute);
|
||||
}
|
||||
}
|
||||
}, [props.forceSSO, StatusPageUtil.getStatusPageId()]);
|
||||
}, [props.forceSSO, statusPageIdString, requiresMasterPasswordLock]);
|
||||
|
||||
const apiUrl: URL = LOGIN_API_URL;
|
||||
const statusPageId: string | undefined =
|
||||
const statusPageIdForLogo: string | undefined =
|
||||
StatusPageUtil.getStatusPageId()?.toString();
|
||||
const logoUrl: string | null =
|
||||
props.logoFileId && props.logoFileId.toString() && statusPageId
|
||||
props.logoFileId && props.logoFileId.toString() && statusPageIdForLogo
|
||||
? URL.fromString(STATUS_PAGE_API_URL.toString())
|
||||
.addRoute(`/logo/${statusPageId}`)
|
||||
.addRoute(`/logo/${statusPageIdForLogo}`)
|
||||
.toString()
|
||||
: null;
|
||||
|
||||
if (!StatusPageUtil.getStatusPageId()) {
|
||||
if (!statusPageId) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (requiresMasterPasswordLock) {
|
||||
StatusPageUtil.navigateToMasterPasswordPage();
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -87,10 +101,7 @@ const LoginPage: FunctionComponent<ComponentProps> = (
|
||||
Navigation.navigate(navRoute);
|
||||
}
|
||||
|
||||
if (
|
||||
StatusPageUtil.getStatusPageId() &&
|
||||
UserUtil.isLoggedIn(StatusPageUtil.getStatusPageId()!)
|
||||
) {
|
||||
if (statusPageId && UserUtil.isLoggedIn(statusPageId)) {
|
||||
if (Navigation.getQueryStringByName("redirectUrl")) {
|
||||
Navigation.navigate(
|
||||
new Route(Navigation.getQueryStringByName("redirectUrl")!),
|
||||
|
||||
186
StatusPage/src/Pages/Accounts/MasterPassword.tsx
Normal file
186
StatusPage/src/Pages/Accounts/MasterPassword.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { STATUS_PAGE_API_URL } from "../../Utils/Config";
|
||||
import StatusPageUtil from "../../Utils/StatusPage";
|
||||
import UserUtil from "../../Utils/User";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import BasicForm from "Common/UI/Components/Forms/BasicForm";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import API from "../../Utils/API";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
|
||||
export interface ComponentProps {
|
||||
statusPageName: string;
|
||||
logoFileId: ObjectID;
|
||||
}
|
||||
|
||||
const MasterPasswordPage: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): JSX.Element => {
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
const statusPageId: ObjectID | null = StatusPageUtil.getStatusPageId();
|
||||
|
||||
const redirectToOverview: () => void = (): void => {
|
||||
const path: string = StatusPageUtil.isPreviewPage()
|
||||
? `/status-page/${StatusPageUtil.getStatusPageId()?.toString()}`
|
||||
: "/";
|
||||
|
||||
Navigation.navigate(new Route(path), { forceNavigate: true });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!StatusPageUtil.isPrivateStatusPage()) {
|
||||
Navigation.navigate(new Route("/"), { forceNavigate: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!StatusPageUtil.requiresMasterPassword()) {
|
||||
redirectToOverview();
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusPageId && UserUtil.isLoggedIn(statusPageId)) {
|
||||
redirectToOverview();
|
||||
return;
|
||||
}
|
||||
|
||||
if (StatusPageUtil.isMasterPasswordValidated()) {
|
||||
redirectToOverview();
|
||||
return;
|
||||
}
|
||||
}, [statusPageId]);
|
||||
|
||||
if (!statusPageId || !StatusPageUtil.requiresMasterPassword()) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
const logoUrl: string | null =
|
||||
props.logoFileId && props.logoFileId.toString()
|
||||
? URL.fromString(STATUS_PAGE_API_URL.toString())
|
||||
.addRoute(`/logo/${statusPageId.toString()}`)
|
||||
.toString()
|
||||
: null;
|
||||
|
||||
const privatePageCopy: string = "Please enter the password to continue.";
|
||||
|
||||
const handleFormSubmit: (
|
||||
values: JSONObject,
|
||||
onSubmitSuccessful?: () => void,
|
||||
) => Promise<void> = async (
|
||||
values: JSONObject,
|
||||
onSubmitSuccessful?: () => void,
|
||||
): Promise<void> => {
|
||||
const submittedPassword: string =
|
||||
(values["password"] as { toString: () => string } | undefined)
|
||||
?.toString()
|
||||
.trim() || "";
|
||||
|
||||
if (!submittedPassword) {
|
||||
setFormError("password is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!statusPageId) {
|
||||
throw new BadDataException("Status Page ID not found");
|
||||
}
|
||||
|
||||
const url: URL = URL.fromString(STATUS_PAGE_API_URL.toString()).addRoute(
|
||||
`/master-password/${statusPageId.toString()}`,
|
||||
);
|
||||
|
||||
setIsSubmitting(true);
|
||||
setFormError(null);
|
||||
|
||||
try {
|
||||
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post<JSONObject>({
|
||||
url,
|
||||
data: {
|
||||
password: submittedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.isFailure()) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
StatusPageUtil.setMasterPasswordValidated(true);
|
||||
|
||||
const redirectUrl: string | null =
|
||||
Navigation.getQueryStringByName("redirectUrl");
|
||||
|
||||
if (redirectUrl) {
|
||||
Navigation.navigate(new Route(redirectUrl), { forceNavigate: true });
|
||||
} else {
|
||||
redirectToOverview();
|
||||
}
|
||||
|
||||
onSubmitSuccessful?.();
|
||||
} catch (err) {
|
||||
setFormError(API.getFriendlyMessage(err));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
{logoUrl ? (
|
||||
<img style={{ height: "70px", margin: "auto" }} src={logoUrl} />
|
||||
) : null}
|
||||
<h2 className="mt-6 text-center text-2xl tracking-tight text-gray-900">
|
||||
{props.statusPageName
|
||||
? `Enter ${props.statusPageName} Password`
|
||||
: "Enter Password"}
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
{privatePageCopy}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<BasicForm
|
||||
id="master-password-form"
|
||||
name="Password Unlock"
|
||||
initialValues={{
|
||||
password: "",
|
||||
}}
|
||||
fields={[
|
||||
{
|
||||
field: {
|
||||
password: true,
|
||||
},
|
||||
title: "Password",
|
||||
description: "Enter the password to unlock this page.",
|
||||
required: true,
|
||||
placeholder: "Enter password",
|
||||
fieldType: FormFieldSchemaType.Password,
|
||||
disableSpellCheck: true,
|
||||
},
|
||||
]}
|
||||
submitButtonText="Unlock Status Page"
|
||||
maxPrimaryButtonWidth={true}
|
||||
isLoading={isSubmitting}
|
||||
error={formError || undefined}
|
||||
onSubmit={(values: JSONObject, onSubmitSuccessful?: () => void) => {
|
||||
void handleFormSubmit(values, onSubmitSuccessful);
|
||||
}}
|
||||
footer={<></>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MasterPasswordPage;
|
||||
@@ -23,8 +23,12 @@ const LoginPage: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
) => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const statusPageId: string | undefined =
|
||||
StatusPageUtil.getStatusPageId()?.toString();
|
||||
const statusPageObjectId: ObjectID | null = StatusPageUtil.getStatusPageId();
|
||||
const statusPageId: string | undefined = statusPageObjectId?.toString();
|
||||
const requiresMasterPasswordLock: boolean =
|
||||
StatusPageUtil.isPrivateStatusPage() &&
|
||||
StatusPageUtil.requiresMasterPassword() &&
|
||||
!StatusPageUtil.isMasterPasswordValidated();
|
||||
const logoUrl: string | null =
|
||||
props.logoFileId && props.logoFileId.toString() && statusPageId
|
||||
? URL.fromString(STATUS_PAGE_API_URL.toString())
|
||||
@@ -32,7 +36,12 @@ const LoginPage: FunctionComponent<ComponentProps> = (
|
||||
.toString()
|
||||
: null;
|
||||
|
||||
if (!StatusPageUtil.getStatusPageId()) {
|
||||
if (!statusPageObjectId) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (requiresMasterPasswordLock) {
|
||||
StatusPageUtil.navigateToMasterPasswordPage();
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -46,10 +55,7 @@ const LoginPage: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
StatusPageUtil.getStatusPageId() &&
|
||||
UserUtil.isLoggedIn(StatusPageUtil.getStatusPageId()!)
|
||||
) {
|
||||
if (statusPageObjectId && UserUtil.isLoggedIn(statusPageObjectId)) {
|
||||
if (Navigation.getQueryStringByName("redirectUrl")) {
|
||||
Navigation.navigate(
|
||||
new Route(Navigation.getQueryStringByName("redirectUrl")!),
|
||||
|
||||
@@ -28,11 +28,19 @@ export default class API extends BaseAPI {
|
||||
}
|
||||
|
||||
public static override getLoginRoute(): Route {
|
||||
return new Route(
|
||||
StatusPageUtil.isPreviewPage()
|
||||
? `/status-page/${StatusPageUtil.getStatusPageId()?.toString()}/login`
|
||||
: "/login",
|
||||
);
|
||||
const basePath: string = StatusPageUtil.isPreviewPage()
|
||||
? `/status-page/${StatusPageUtil.getStatusPageId()?.toString()}`
|
||||
: "";
|
||||
|
||||
if (
|
||||
StatusPageUtil.isPrivateStatusPage() &&
|
||||
StatusPageUtil.requiresMasterPassword() &&
|
||||
!StatusPageUtil.isMasterPasswordValidated()
|
||||
) {
|
||||
return new Route(`${basePath}/master-password`);
|
||||
}
|
||||
|
||||
return new Route(`${basePath}/login`);
|
||||
}
|
||||
|
||||
public static override logoutUser(): void {
|
||||
|
||||
@@ -51,6 +51,9 @@ enum PageMap {
|
||||
PREVIEW_LOGOUT = "PREVIEW_LOGOUT",
|
||||
|
||||
PREVIEW_FORBIDDEN = "PREVIEW_FORBIDDEN",
|
||||
|
||||
MASTER_PASSWORD = "MASTER_PASSWORD",
|
||||
PREVIEW_MASTER_PASSWORD = "PREVIEW_MASTER_PASSWORD",
|
||||
}
|
||||
|
||||
export default PageMap;
|
||||
|
||||
@@ -28,6 +28,7 @@ const RouteMap: Dictionary<Route> = {
|
||||
[PageMap.LOGOUT]: new Route(`/logout`),
|
||||
[PageMap.FORGOT_PASSWORD]: new Route(`/forgot-password`),
|
||||
[PageMap.RESET_PASSWORD]: new Route(`/reset-password/:token`),
|
||||
[PageMap.MASTER_PASSWORD]: new Route(`/master-password`),
|
||||
|
||||
// forbidden page
|
||||
[PageMap.FORBIDDEN]: new Route(`/forbidden`),
|
||||
@@ -91,6 +92,9 @@ const RouteMap: Dictionary<Route> = {
|
||||
[PageMap.PREVIEW_RESET_PASSWORD]: new Route(
|
||||
`/status-page/${RouteParams.StatusPageId}/reset-password/:token`,
|
||||
),
|
||||
[PageMap.PREVIEW_MASTER_PASSWORD]: new Route(
|
||||
`/status-page/${RouteParams.StatusPageId}/master-password`,
|
||||
),
|
||||
|
||||
[PageMap.PREVIEW_SSO]: new Route(
|
||||
`/status-page/${RouteParams.StatusPageId}/sso`,
|
||||
|
||||
@@ -24,11 +24,104 @@ export default class StatusPageUtil {
|
||||
}
|
||||
|
||||
public static setIsPrivateStatusPage(isPrivate: boolean): void {
|
||||
LocalStorage.setItem("isPrivateStatusPage", isPrivate);
|
||||
const storageKey: string =
|
||||
StatusPageUtil.getIsPrivateStatusPageStorageKey();
|
||||
|
||||
LocalStorage.setItem(storageKey, isPrivate);
|
||||
}
|
||||
|
||||
public static isPrivateStatusPage(): boolean {
|
||||
return Boolean(LocalStorage.getItem("isPrivateStatusPage"));
|
||||
const storageKey: string =
|
||||
StatusPageUtil.getIsPrivateStatusPageStorageKey();
|
||||
|
||||
return Boolean(LocalStorage.getItem(storageKey));
|
||||
}
|
||||
|
||||
public static setRequiresMasterPassword(value: boolean): void {
|
||||
const storageKey: string =
|
||||
StatusPageUtil.getRequiresMasterPasswordStorageKey();
|
||||
|
||||
LocalStorage.setItem(storageKey, value);
|
||||
|
||||
if (!value) {
|
||||
StatusPageUtil.setMasterPasswordValidated(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static requiresMasterPassword(): boolean {
|
||||
const storageKey: string =
|
||||
StatusPageUtil.getRequiresMasterPasswordStorageKey();
|
||||
|
||||
return Boolean(LocalStorage.getItem(storageKey));
|
||||
}
|
||||
|
||||
private static getStatusPageScopedStorageKey(baseKey: string): string {
|
||||
const statusPageId: ObjectID | null = StatusPageUtil.getStatusPageId();
|
||||
|
||||
if (!statusPageId) {
|
||||
return baseKey;
|
||||
}
|
||||
|
||||
return `${baseKey}-${statusPageId.toString()}`;
|
||||
}
|
||||
|
||||
private static getIsPrivateStatusPageStorageKey(): string {
|
||||
return StatusPageUtil.getStatusPageScopedStorageKey("isPrivateStatusPage");
|
||||
}
|
||||
|
||||
private static getRequiresMasterPasswordStorageKey(): string {
|
||||
return StatusPageUtil.getStatusPageScopedStorageKey(
|
||||
"requiresMasterPassword",
|
||||
);
|
||||
}
|
||||
|
||||
private static getMasterPasswordValidationStorageKey(): string {
|
||||
const statusPageId: ObjectID | null = StatusPageUtil.getStatusPageId();
|
||||
|
||||
if (!statusPageId) {
|
||||
return "masterPasswordValidated";
|
||||
}
|
||||
|
||||
return `masterPasswordValidated-${statusPageId.toString()}`;
|
||||
}
|
||||
|
||||
public static setMasterPasswordValidated(value: boolean): void {
|
||||
const storageKey: string =
|
||||
StatusPageUtil.getMasterPasswordValidationStorageKey();
|
||||
|
||||
LocalStorage.setItem(storageKey, value);
|
||||
|
||||
if (storageKey !== "masterPasswordValidated") {
|
||||
LocalStorage.removeItem("masterPasswordValidated");
|
||||
}
|
||||
}
|
||||
|
||||
public static isMasterPasswordValidated(): boolean {
|
||||
const storageKey: string =
|
||||
StatusPageUtil.getMasterPasswordValidationStorageKey();
|
||||
|
||||
const currentValue: boolean = Boolean(LocalStorage.getItem(storageKey));
|
||||
|
||||
if (currentValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (storageKey === "masterPasswordValidated") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const legacyValue: boolean = Boolean(
|
||||
LocalStorage.getItem("masterPasswordValidated"),
|
||||
);
|
||||
|
||||
LocalStorage.removeItem("masterPasswordValidated");
|
||||
|
||||
if (legacyValue) {
|
||||
LocalStorage.setItem(storageKey, legacyValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static isPreviewPage(): boolean {
|
||||
@@ -36,6 +129,15 @@ export default class StatusPageUtil {
|
||||
}
|
||||
|
||||
public static navigateToLoginPage(): void {
|
||||
if (
|
||||
StatusPageUtil.isPrivateStatusPage() &&
|
||||
StatusPageUtil.requiresMasterPassword() &&
|
||||
!StatusPageUtil.isMasterPasswordValidated()
|
||||
) {
|
||||
StatusPageUtil.navigateToMasterPasswordPage();
|
||||
return;
|
||||
}
|
||||
|
||||
const route: Route = new Route(
|
||||
StatusPageUtil.isPreviewPage()
|
||||
? `/status-page/${StatusPageUtil.getStatusPageId()?.toString()}/login?redirectUrl=${Navigation.getCurrentPath()}`
|
||||
@@ -45,9 +147,37 @@ export default class StatusPageUtil {
|
||||
Navigation.navigate(route, { forceNavigate: true });
|
||||
}
|
||||
|
||||
public static navigateToMasterPasswordPage(): void {
|
||||
if (Navigation.getCurrentRoute().toString().includes("master-password")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const basePath: string = StatusPageUtil.isPreviewPage()
|
||||
? `/status-page/${StatusPageUtil.getStatusPageId()?.toString()}`
|
||||
: "";
|
||||
|
||||
const route: Route = new Route(
|
||||
`${basePath}/master-password?redirectUrl=${Navigation.getCurrentPath()}`,
|
||||
);
|
||||
|
||||
Navigation.navigate(route, { forceNavigate: true });
|
||||
}
|
||||
|
||||
public static checkIfUserHasLoggedIn(): void {
|
||||
const statusPageId: ObjectID | null = StatusPageUtil.getStatusPageId();
|
||||
|
||||
if (
|
||||
statusPageId &&
|
||||
StatusPageUtil.isPrivateStatusPage() &&
|
||||
StatusPageUtil.requiresMasterPassword() &&
|
||||
!UserUtil.isLoggedIn(statusPageId)
|
||||
) {
|
||||
if (!StatusPageUtil.isMasterPasswordValidated()) {
|
||||
StatusPageUtil.navigateToMasterPasswordPage();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
statusPageId &&
|
||||
StatusPageUtil.isPrivateStatusPage() &&
|
||||
@@ -64,6 +194,15 @@ export default class StatusPageUtil {
|
||||
errorResponse instanceof HTTPErrorResponse &&
|
||||
errorResponse.statusCode === 401
|
||||
) {
|
||||
if (
|
||||
StatusPageUtil.isPrivateStatusPage() &&
|
||||
StatusPageUtil.requiresMasterPassword() &&
|
||||
!StatusPageUtil.isMasterPasswordValidated()
|
||||
) {
|
||||
StatusPageUtil.navigateToMasterPasswordPage();
|
||||
return;
|
||||
}
|
||||
|
||||
await UserUtil.logout(StatusPageUtil.getStatusPageId()!);
|
||||
StatusPageUtil.navigateToLoginPage();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,15 @@ import { IDENTITY_URL } from "Common/UI/Config";
|
||||
import LocalStorage from "Common/UI/Utils/LocalStorage";
|
||||
|
||||
export default class User {
|
||||
private static getMasterPasswordStorageKeys(
|
||||
statusPageId: ObjectID,
|
||||
): Array<string> {
|
||||
return [
|
||||
`masterPasswordValidated-${statusPageId.toString()}`,
|
||||
"masterPasswordValidated",
|
||||
];
|
||||
}
|
||||
|
||||
public static setUserId(statusPageId: ObjectID, userId: ObjectID): void {
|
||||
LocalStorage.setItem(
|
||||
statusPageId.toString() + "user_id",
|
||||
@@ -82,5 +91,8 @@ export default class User {
|
||||
.addRoute("/" + statusPageId.toString()),
|
||||
});
|
||||
this.removeUser(statusPageId);
|
||||
for (const storageKey of this.getMasterPasswordStorageKeys(statusPageId)) {
|
||||
LocalStorage.removeItem(storageKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@oneuptime/probe-ingest",
|
||||
"name": "@oneuptime/telemetry",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
|
||||
Reference in New Issue
Block a user