mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Implement master password feature for private status pages
This commit is contained in:
@@ -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,71 @@ 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,
|
||||
],
|
||||
read: [],
|
||||
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,82 @@ 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 NotAuthenticatedException(
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -481,6 +489,34 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
hasReadAccess: true,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -32,6 +32,27 @@ const StatusPageDelete: FunctionComponent<
|
||||
required: false,
|
||||
placeholder: "Is this status page visible to public",
|
||||
},
|
||||
{
|
||||
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,
|
||||
@@ -45,6 +66,14 @@ const StatusPageDelete: FunctionComponent<
|
||||
fieldType: FieldType.Boolean,
|
||||
title: "Is Visible to Public",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
enableMasterPassword: true,
|
||||
},
|
||||
fieldType: FieldType.Boolean,
|
||||
title: "Require Master Password",
|
||||
placeholder: "No",
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
|
||||
193
StatusPage/src/Pages/Accounts/MasterPassword.tsx
Normal file
193
StatusPage/src/Pages/Accounts/MasterPassword.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
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 Alert, {
|
||||
AlertType,
|
||||
} from "Common/UI/Components/Alerts/Alert";
|
||||
import Button, {
|
||||
ButtonSize,
|
||||
ButtonStyleType,
|
||||
} from "Common/UI/Components/Button/Button";
|
||||
import ButtonType from "Common/UI/Components/Button/ButtonTypes";
|
||||
import API from "../../Utils/API";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import React, { FormEvent, FunctionComponent, useEffect, useState } from "react";
|
||||
|
||||
export interface ComponentProps {
|
||||
statusPageName: string;
|
||||
logoFileId: ObjectID;
|
||||
}
|
||||
|
||||
const MasterPasswordPage: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): JSX.Element => {
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const statusPageId: ObjectID | null = StatusPageUtil.getStatusPageId();
|
||||
|
||||
const redirectToOverview = (): 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 onSubmit = async (): Promise<void> => {
|
||||
if (!password) {
|
||||
setError("Master 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);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await API.post({
|
||||
url,
|
||||
data: {
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
StatusPageUtil.setMasterPasswordValidated(true);
|
||||
|
||||
const redirectUrl: string | null =
|
||||
Navigation.getQueryStringByName("redirectUrl");
|
||||
|
||||
if (redirectUrl) {
|
||||
Navigation.navigate(new Route(redirectUrl), { forceNavigate: true });
|
||||
} else {
|
||||
redirectToOverview();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setPassword("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
|
||||
event.preventDefault();
|
||||
void onSubmit();
|
||||
};
|
||||
|
||||
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">
|
||||
Enter Master Password
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
This status page is private. Please enter the master password to
|
||||
continue.
|
||||
</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">
|
||||
{error ? (
|
||||
<Alert
|
||||
className="mb-4"
|
||||
strongTitle="Unable to unlock status page"
|
||||
title={error}
|
||||
type={AlertType.DANGER}
|
||||
/>
|
||||
) : null}
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="master-password"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Master Password
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="master-password"
|
||||
name="master-password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required={true}
|
||||
value={password}
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value);
|
||||
}}
|
||||
className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 shadow-sm placeholder:text-gray-400 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
buttonStyle={ButtonStyleType.PRIMARY}
|
||||
buttonSize={ButtonSize.Large}
|
||||
type={ButtonType.Submit}
|
||||
title={isSubmitting ? "Unlocking..." : "Unlock Status Page"}
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MasterPasswordPage;
|
||||
@@ -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`,
|
||||
|
||||
@@ -31,6 +31,25 @@ export default class StatusPageUtil {
|
||||
return Boolean(LocalStorage.getItem("isPrivateStatusPage"));
|
||||
}
|
||||
|
||||
public static setRequiresMasterPassword(value: boolean): void {
|
||||
LocalStorage.setItem("requiresMasterPassword", value);
|
||||
if (!value) {
|
||||
StatusPageUtil.setMasterPasswordValidated(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static requiresMasterPassword(): boolean {
|
||||
return Boolean(LocalStorage.getItem("requiresMasterPassword"));
|
||||
}
|
||||
|
||||
public static setMasterPasswordValidated(value: boolean): void {
|
||||
LocalStorage.setItem("masterPasswordValidated", value);
|
||||
}
|
||||
|
||||
public static isMasterPasswordValidated(): boolean {
|
||||
return Boolean(LocalStorage.getItem("masterPasswordValidated"));
|
||||
}
|
||||
|
||||
public static isPreviewPage(): boolean {
|
||||
return Navigation.containsInPath("/status-page/");
|
||||
}
|
||||
@@ -45,9 +64,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() &&
|
||||
|
||||
Reference in New Issue
Block a user