mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: Enhance public dashboard functionality with access control and UI updates
This commit is contained in:
@@ -5,12 +5,14 @@ import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import Dashboard from "Common/Models/DatabaseModels/Dashboard";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
import React, { Fragment, FunctionComponent, ReactElement, useState } from "react";
|
||||
import DashboardPreviewLink from "./DashboardPreviewLink";
|
||||
|
||||
const DashboardAuthenticationSettings: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
const [isPublicDashboard, setIsPublicDashboard] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -47,112 +49,121 @@ const DashboardAuthenticationSettings: FunctionComponent<
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
onItemLoaded: (item: Dashboard) => {
|
||||
setIsPublicDashboard(Boolean(item.isPublicDashboard));
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<CardModelDetail<Dashboard>
|
||||
name="Dashboard > Master Password"
|
||||
cardProps={{
|
||||
title: "Master Password",
|
||||
description:
|
||||
"Rotate the password required to unlock a private dashboard. This value is stored as a secure hash and cannot be retrieved.",
|
||||
}}
|
||||
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 dashboard.",
|
||||
},
|
||||
{
|
||||
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: Dashboard,
|
||||
id: "model-detail-dashboard-master-password",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
enableMasterPassword: true,
|
||||
},
|
||||
fieldType: FieldType.Boolean,
|
||||
title: "Require Master Password",
|
||||
placeholder: "No",
|
||||
},
|
||||
{
|
||||
{isPublicDashboard && (
|
||||
<>
|
||||
<DashboardPreviewLink modelId={modelId} />
|
||||
|
||||
<CardModelDetail<Dashboard>
|
||||
name="Dashboard > Master Password"
|
||||
cardProps={{
|
||||
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>
|
||||
);
|
||||
description:
|
||||
"Rotate the password required to unlock a public dashboard. This value is stored as a secure hash and cannot be retrieved.",
|
||||
}}
|
||||
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 this public dashboard.",
|
||||
},
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
{
|
||||
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: Dashboard,
|
||||
id: "model-detail-dashboard-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<Dashboard>
|
||||
name="Dashboard > IP Whitelist"
|
||||
cardProps={{
|
||||
title: "IP Whitelist",
|
||||
description:
|
||||
"IP Whitelist for this dashboard. If the dashboard is public then only IP addresses in this whitelist will be able to access the dashboard. If the dashboard is not public then only users who have access from the IP addresses in this whitelist will be able to access the dashboard.",
|
||||
}}
|
||||
editButtonText="Edit IP Whitelist"
|
||||
isEditable={true}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
ipWhitelist: true,
|
||||
},
|
||||
title: "IP Whitelist",
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
placeholder:
|
||||
"Please enter the IP addresses or CIDR ranges to whitelist. One per line. This can be IPv4 or IPv6 addresses.",
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
showDetailsInNumberOfColumns: 1,
|
||||
modelType: Dashboard,
|
||||
id: "model-detail-dashboard-ip-whitelist",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
ipWhitelist: true,
|
||||
},
|
||||
fieldType: FieldType.LongText,
|
||||
<CardModelDetail<Dashboard>
|
||||
name="Dashboard > IP Whitelist"
|
||||
cardProps={{
|
||||
title: "IP Whitelist",
|
||||
placeholder:
|
||||
"No IP addresses or CIDR ranges whitelisted. This will allow all IP addresses to access the dashboard.",
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
description:
|
||||
"IP Whitelist for this dashboard. Only IP addresses in this whitelist will be able to access the public dashboard.",
|
||||
}}
|
||||
editButtonText="Edit IP Whitelist"
|
||||
isEditable={true}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
ipWhitelist: true,
|
||||
},
|
||||
title: "IP Whitelist",
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
placeholder:
|
||||
"Please enter the IP addresses or CIDR ranges to whitelist. One per line. This can be IPv4 or IPv6 addresses.",
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
showDetailsInNumberOfColumns: 1,
|
||||
modelType: Dashboard,
|
||||
id: "model-detail-dashboard-ip-whitelist",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
ipWhitelist: true,
|
||||
},
|
||||
fieldType: FieldType.LongText,
|
||||
title: "IP Whitelist",
|
||||
placeholder:
|
||||
"No IP addresses or CIDR ranges whitelisted. This will allow all IP addresses to access the dashboard.",
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ import Navigation from "Common/UI/Utils/Navigation";
|
||||
import Label from "Common/Models/DatabaseModels/Label";
|
||||
import Dashboard from "Common/Models/DatabaseModels/Dashboard";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
import DashboardPreviewLink from "./DashboardPreviewLink";
|
||||
|
||||
const DashboardView: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -17,7 +16,6 @@ const DashboardView: FunctionComponent<
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<DashboardPreviewLink modelId={modelId} />
|
||||
{/* Dashboard View */}
|
||||
<CardModelDetail<Dashboard>
|
||||
name="Dashboard > Dashboard Details"
|
||||
|
||||
@@ -65,6 +65,7 @@ const App: () => JSX.Element = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [dashboardId, setDashboardId] = useState<ObjectID | null>(null);
|
||||
const [dashboardName, setDashboardName] = useState<string>("Dashboard");
|
||||
const [isPublicDashboard, setIsPublicDashboard] = useState<boolean>(false);
|
||||
type GetIdFunction = () => Promise<ObjectID>;
|
||||
|
||||
const getId: GetIdFunction = async (): Promise<ObjectID> => {
|
||||
@@ -121,11 +122,13 @@ const App: () => JSX.Element = () => {
|
||||
const enableMasterPassword: boolean = Boolean(
|
||||
response.data["enableMasterPassword"],
|
||||
);
|
||||
const isPublicDashboard: boolean = Boolean(
|
||||
const isPublic: boolean = Boolean(
|
||||
response.data["isPublicDashboard"],
|
||||
);
|
||||
|
||||
if (!isPublicDashboard && enableMasterPassword) {
|
||||
setIsPublicDashboard(isPublic);
|
||||
|
||||
if (isPublic && enableMasterPassword) {
|
||||
PublicDashboardUtil.setRequiresMasterPassword(true);
|
||||
} else {
|
||||
PublicDashboardUtil.setRequiresMasterPassword(false);
|
||||
@@ -151,6 +154,15 @@ const App: () => JSX.Element = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// If dashboard is not public, show 404
|
||||
if (!isPublicDashboard) {
|
||||
return (
|
||||
<Suspense fallback={<PageLoader isVisible={true} />}>
|
||||
<NotFoundPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if master password is required and not validated
|
||||
if (
|
||||
dashboardId &&
|
||||
|
||||
@@ -243,9 +243,9 @@ export default class DashboardAPI extends BaseAPI<
|
||||
throw new NotFoundException("Dashboard not found");
|
||||
}
|
||||
|
||||
if (dashboard.isPublicDashboard) {
|
||||
if (!dashboard.isPublicDashboard) {
|
||||
throw new BadDataException(
|
||||
"This dashboard is already visible to everyone.",
|
||||
"This dashboard is not publicly accessible.",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,16 @@ export class Service extends DatabaseService<Model> {
|
||||
},
|
||||
});
|
||||
|
||||
// If dashboard is not public, deny access
|
||||
if (dashboard && !dashboard.isPublicDashboard) {
|
||||
return {
|
||||
hasReadAccess: false,
|
||||
error: new NotAuthenticatedException(
|
||||
"This dashboard is not available.",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (dashboard?.ipWhitelist && dashboard.ipWhitelist.length > 0) {
|
||||
const ipWhitelist: Array<string> = dashboard.ipWhitelist?.split("\n");
|
||||
|
||||
@@ -129,17 +139,11 @@ export class Service extends DatabaseService<Model> {
|
||||
}
|
||||
}
|
||||
|
||||
if (dashboard && dashboard.isPublicDashboard) {
|
||||
return {
|
||||
hasReadAccess: true,
|
||||
};
|
||||
}
|
||||
|
||||
const shouldEnforceMasterPassword: boolean = Boolean(
|
||||
dashboard &&
|
||||
dashboard.isPublicDashboard &&
|
||||
dashboard.enableMasterPassword &&
|
||||
dashboard.masterPassword &&
|
||||
!dashboard.isPublicDashboard,
|
||||
dashboard.masterPassword,
|
||||
);
|
||||
|
||||
if (shouldEnforceMasterPassword) {
|
||||
@@ -162,6 +166,13 @@ export class Service extends DatabaseService<Model> {
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Public dashboard without master password - grant access
|
||||
if (dashboard && dashboard.isPublicDashboard) {
|
||||
return {
|
||||
hasReadAccess: true,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
@@ -169,7 +180,7 @@ export class Service extends DatabaseService<Model> {
|
||||
return {
|
||||
hasReadAccess: false,
|
||||
error: new NotAuthenticatedException(
|
||||
"You do not have access to this dashboard. Please login to view the dashboard.",
|
||||
"You do not have access to this dashboard.",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user