Files
oneuptime/Common/Server/API/DashboardAPI.ts

345 lines
11 KiB
TypeScript

import UserMiddleware from "../Middleware/UserAuthorization";
import DashboardService, {
Service as DashboardServiceType,
} from "../Services/DashboardService";
import DashboardDomainService from "../Services/DashboardDomainService";
import CookieUtil from "../Utils/Cookie";
import logger from "../Utils/Logger";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import BadDataException from "../../Types/Exception/BadDataException";
import NotFoundException from "../../Types/Exception/NotFoundException";
import HashedString from "../../Types/HashedString";
import ObjectID from "../../Types/ObjectID";
import Dashboard from "../../Models/DatabaseModels/Dashboard";
import DashboardDomain from "../../Models/DatabaseModels/DashboardDomain";
import { EncryptionSecret } from "../EnvironmentConfig";
import { DASHBOARD_MASTER_PASSWORD_INVALID_MESSAGE } from "../../Types/Dashboard/MasterPassword";
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
import ForbiddenException from "../../Types/Exception/ForbiddenException";
import JSONFunctions from "../../Types/JSONFunctions";
export default class DashboardAPI extends BaseAPI<
Dashboard,
DashboardServiceType
> {
public constructor() {
super(Dashboard, DashboardService);
// SEO endpoint - resolve dashboard by ID or domain
this.router.get(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/seo/:dashboardIdOrDomain`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const dashboardIdOrDomain: string = req.params[
"dashboardIdOrDomain"
] as string;
let dashboardId: ObjectID | null = null;
if (dashboardIdOrDomain && dashboardIdOrDomain.includes(".")) {
// This is a domain - resolve to dashboard ID
const dashboardDomain: DashboardDomain | null =
await DashboardDomainService.findOneBy({
query: {
fullDomain: dashboardIdOrDomain,
domain: {
isVerified: true,
} as any,
},
select: {
dashboardId: true,
},
props: {
isRoot: true,
},
});
if (!dashboardDomain || !dashboardDomain.dashboardId) {
return Response.sendErrorResponse(
req,
res,
new NotFoundException("Dashboard not found"),
);
}
dashboardId = dashboardDomain.dashboardId;
} else {
try {
dashboardId = new ObjectID(dashboardIdOrDomain);
} catch (err) {
logger.error(err);
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid dashboard ID"),
);
}
}
const dashboard: Dashboard | null =
await DashboardService.findOneById({
id: dashboardId,
select: {
_id: true,
name: true,
description: true,
},
props: {
isRoot: true,
},
});
if (!dashboard) {
return Response.sendErrorResponse(
req,
res,
new NotFoundException("Dashboard not found"),
);
}
return Response.sendJsonObjectResponse(req, res, {
_id: dashboard._id?.toString() || "",
title: dashboard.name || "Dashboard",
description:
dashboard.description || "View dashboard metrics and insights.",
});
} catch (err) {
next(err);
}
},
);
// Domain resolution endpoint
this.router.post(
`${new this.entityType().getCrudApiPath()?.toString()}/domain`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
if (!req.body["domain"]) {
throw new BadDataException("domain is required in request body");
}
const domain: string = req.body["domain"] as string;
const dashboardDomain: DashboardDomain | null =
await DashboardDomainService.findOneBy({
query: {
fullDomain: domain,
domain: {
isVerified: true,
} as any,
},
select: {
dashboardId: true,
},
props: {
isRoot: true,
},
});
if (!dashboardDomain) {
throw new BadDataException("No dashboard found with this domain");
}
const objectId: ObjectID = dashboardDomain.dashboardId!;
return Response.sendJsonObjectResponse(req, res, {
dashboardId: objectId.toString(),
});
} catch (err) {
next(err);
}
},
);
// Metadata endpoint - returns dashboard info for the public viewer
this.router.post(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/metadata/:dashboardId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const dashboardId: ObjectID = new ObjectID(
req.params["dashboardId"] as string,
);
const dashboard: Dashboard | null =
await DashboardService.findOneById({
id: dashboardId,
select: {
_id: true,
name: true,
description: true,
isPublicDashboard: true,
enableMasterPassword: true,
},
props: {
isRoot: true,
},
});
if (!dashboard) {
throw new NotFoundException("Dashboard not found");
}
return Response.sendJsonObjectResponse(req, res, {
_id: dashboard._id?.toString() || "",
name: dashboard.name || "Dashboard",
description: dashboard.description || "",
isPublicDashboard: dashboard.isPublicDashboard || false,
enableMasterPassword: dashboard.enableMasterPassword || false,
});
} catch (err) {
next(err);
}
},
);
// Public view-config endpoint - returns dashboard view config for the public viewer
this.router.post(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/view-config/:dashboardId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const dashboardId: ObjectID = new ObjectID(
req.params["dashboardId"] as string,
);
// Check read access (handles public check, IP whitelist, master password)
const accessResult: {
hasReadAccess: boolean;
error?: NotAuthenticatedException | ForbiddenException;
} = await DashboardService.hasReadAccess({
dashboardId,
req,
});
if (!accessResult.hasReadAccess) {
throw (
accessResult.error ||
new BadDataException("Access denied to this dashboard.")
);
}
const dashboard: Dashboard | null =
await DashboardService.findOneById({
id: dashboardId,
select: {
_id: true,
name: true,
description: true,
dashboardViewConfig: true,
},
props: {
isRoot: true,
},
});
if (!dashboard) {
throw new NotFoundException("Dashboard not found");
}
return Response.sendJsonObjectResponse(req, res, {
_id: dashboard._id?.toString() || "",
name: dashboard.name || "Dashboard",
description: dashboard.description || "",
dashboardViewConfig: dashboard.dashboardViewConfig
? JSONFunctions.serialize(dashboard.dashboardViewConfig as any)
: null,
});
} catch (err) {
next(err);
}
},
);
this.router.post(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/master-password/:dashboardId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
if (!req.params["dashboardId"]) {
throw new BadDataException("Dashboard ID not found");
}
const dashboardId: ObjectID = new ObjectID(
req.params["dashboardId"] as string,
);
const password: string | undefined =
req.body && (req.body["password"] as string);
if (!password) {
throw new BadDataException("Master password is required.");
}
const dashboard: Dashboard | null =
await DashboardService.findOneById({
id: dashboardId,
select: {
_id: true,
projectId: true,
enableMasterPassword: true,
masterPassword: true,
isPublicDashboard: true,
},
props: {
isRoot: true,
},
});
if (!dashboard) {
throw new NotFoundException("Dashboard not found");
}
if (!dashboard.isPublicDashboard) {
throw new BadDataException(
"This dashboard is not publicly accessible.",
);
}
if (!dashboard.enableMasterPassword || !dashboard.masterPassword) {
throw new BadDataException(
"Master password has not been configured for this dashboard.",
);
}
const hashedInput: string = await HashedString.hashValue(
password,
EncryptionSecret,
);
if (hashedInput !== dashboard.masterPassword.toString()) {
throw new BadDataException(
DASHBOARD_MASTER_PASSWORD_INVALID_MESSAGE,
);
}
CookieUtil.setDashboardMasterPasswordCookie({
expressResponse: res,
dashboardId,
});
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
next(err);
}
},
);
}
}