Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2170267e8 | ||
|
|
321ca59380 | ||
|
|
e3ecfe6ad8 | ||
|
|
f4664eec16 | ||
|
|
56f601e34b | ||
|
|
1244ff6d93 | ||
|
|
c0ea804c98 | ||
|
|
c21cf29db6 | ||
|
|
3d0da70ce6 | ||
|
|
feb061240b | ||
|
|
a924227805 | ||
|
|
2144d3f8c0 | ||
|
|
fcd6786149 | ||
|
|
ea968ebf3c | ||
|
|
8a7b852974 | ||
|
|
c8d3e3b70c | ||
|
|
7a90a6f9f4 |
No files matched your search
@@ -46,11 +46,11 @@ You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.
|
||||
|
||||
## User's guide
|
||||
|
||||
Usage instructions can be found [here](https://checkmate.so/docs).
|
||||
Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). It's still WIP and some of the information there might be outdated as we continuously add features weekly. Rest assured, we are doing our best! :)
|
||||
|
||||
## Installation
|
||||
|
||||
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
|
||||
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
|
||||
|
||||
Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
|
||||
|
||||
@@ -157,11 +157,12 @@ Here's how you can contribute:
|
||||
|
||||
0. Star this repo :)
|
||||
1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag.
|
||||
2. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
|
||||
3. Open an issue if you believe you've encountered a bug.
|
||||
4. Check for good-first-issue's if you are a newcomer.
|
||||
5. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||
6. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram).
|
||||
2. Check [project structure](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
|
||||
3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
|
||||
4. Open an issue if you believe you've encountered a bug.
|
||||
5. Check for good-first-issue's if you are a newcomer.
|
||||
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||
7. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram).
|
||||
|
||||
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||
@@ -174,3 +175,9 @@ Here's how you can contribute:
|
||||
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
|
||||
|
||||
If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features).
|
||||
|
||||
Also check other developer and contributor-friendly projects of BlueWave:
|
||||
|
||||
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
|
||||
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
|
||||
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.
|
||||
@@ -11,26 +11,20 @@ import type { ResponsiveStyleValue } from "@mui/system";
|
||||
interface SliderInputProps extends SliderProps {
|
||||
sx?: SliderProps["sx"];
|
||||
showValue?: boolean;
|
||||
formatDisplayValue?: (value: number) => string;
|
||||
}
|
||||
|
||||
export const SliderInput = forwardRef<HTMLSpanElement, SliderInputProps>(
|
||||
({ sx, showValue = false, formatDisplayValue, ...props }, ref) => {
|
||||
({ sx, showValue = false, ...props }, ref) => {
|
||||
const theme = useTheme();
|
||||
const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : [];
|
||||
|
||||
const displayValue =
|
||||
showValue && formatDisplayValue && typeof props.value === "number"
|
||||
? formatDisplayValue(props.value)
|
||||
: props.value;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(8)}
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
>
|
||||
{showValue && <Typography>{displayValue}</Typography>}
|
||||
{showValue && <Typography>{props.value}</Typography>}
|
||||
<Slider
|
||||
{...props}
|
||||
ref={ref}
|
||||
@@ -69,21 +63,12 @@ interface SliderWithLabelProps extends SliderProps {
|
||||
fieldLabel?: string;
|
||||
required?: boolean;
|
||||
showValue?: boolean;
|
||||
formatDisplayValue?: (value: number) => string;
|
||||
sliderMaxWidth?: ResponsiveStyleValue<number | string>;
|
||||
}
|
||||
|
||||
export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>(
|
||||
(
|
||||
{
|
||||
fieldLabel,
|
||||
required,
|
||||
showValue = true,
|
||||
formatDisplayValue,
|
||||
value,
|
||||
sliderMaxWidth = "100%",
|
||||
...props
|
||||
},
|
||||
{ fieldLabel, required, showValue = true, value, sliderMaxWidth = "100%", ...props },
|
||||
ref
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
@@ -97,7 +82,6 @@ export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>
|
||||
<SliderInput
|
||||
{...props}
|
||||
showValue={showValue}
|
||||
formatDisplayValue={formatDisplayValue}
|
||||
value={value}
|
||||
ref={ref}
|
||||
/>
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
} from "@/Features/UI/uiSlice.js";
|
||||
import timezones from "@/Utils/timezones.json";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { CHECK_TTL_SENTINEL } from "@/Types/Check";
|
||||
|
||||
interface Timezone {
|
||||
id: string;
|
||||
@@ -463,43 +462,6 @@ export const SettingsPage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Check Retention */}
|
||||
{isAdmin && (
|
||||
<ConfigBox
|
||||
title={t("pages.settings.form.retention.title")}
|
||||
subtitle={t("pages.settings.form.retention.description")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="checkTTL"
|
||||
control={form.control}
|
||||
defaultValue={defaults.checkTTL}
|
||||
render={({ field }) => (
|
||||
<SliderWithLabel
|
||||
{...field}
|
||||
fieldLabel={t("pages.settings.form.retention.option.days.label")}
|
||||
min={1}
|
||||
max={CHECK_TTL_SENTINEL}
|
||||
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
||||
value={field.value || 30}
|
||||
onChange={(_, value) => field.onChange(value)}
|
||||
valueLabelDisplay="auto"
|
||||
valueLabelFormat={(value: number) =>
|
||||
value >= CHECK_TTL_SENTINEL
|
||||
? t("pages.settings.form.retention.option.days.unlimited")
|
||||
: `${value}`
|
||||
}
|
||||
formatDisplayValue={(value: number) =>
|
||||
value >= CHECK_TTL_SENTINEL
|
||||
? t("pages.settings.form.retention.option.days.unlimited")
|
||||
: `${value}`
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Global Thresholds */}
|
||||
{isAdmin && (
|
||||
<ConfigBox
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export const CHECK_TTL_SENTINEL = 366;
|
||||
|
||||
export interface CheckMetadata {
|
||||
monitorId: string;
|
||||
teamId: string;
|
||||
@@ -127,6 +125,9 @@ export interface Check {
|
||||
timings?: CheckTimings;
|
||||
statusCode: number;
|
||||
message: string;
|
||||
ack: boolean;
|
||||
ackAt?: string | null;
|
||||
expiry: string;
|
||||
cpu?: CheckCpuInfo;
|
||||
memory?: CheckMemoryInfo;
|
||||
disk?: CheckDiskInfo[];
|
||||
@@ -139,6 +140,7 @@ export interface Check {
|
||||
seo?: number;
|
||||
performance?: number;
|
||||
audits?: CheckAudits;
|
||||
__v: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -241,7 +243,10 @@ export interface ChecksSummary {
|
||||
downChecks: number;
|
||||
}
|
||||
|
||||
export type CheckSnapshot = Omit<Check, "metadata" | "__v" | "updatedAt"> & {
|
||||
export type CheckSnapshot = Omit<
|
||||
Check,
|
||||
"metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt"
|
||||
> & {
|
||||
originalResponseTime: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ export const NotificationChannels = [
|
||||
"webhook",
|
||||
"pager_duty",
|
||||
"matrix",
|
||||
"teams",
|
||||
] as const;
|
||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||
|
||||
|
||||
@@ -45,11 +45,6 @@ const matrixSchema = baseSchema.extend({
|
||||
accessToken: z.string().min(1, "Access token is required"),
|
||||
});
|
||||
|
||||
const teamsSchema = baseSchema.extend({
|
||||
type: z.literal("teams"),
|
||||
address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"),
|
||||
});
|
||||
|
||||
export const notificationSchema = z.discriminatedUnion("type", [
|
||||
emailSchema,
|
||||
slackSchema,
|
||||
@@ -57,7 +52,6 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
webhookSchema,
|
||||
pagerDutySchema,
|
||||
matrixSchema,
|
||||
teamsSchema,
|
||||
]);
|
||||
|
||||
export type NotificationFormData = z.infer<typeof notificationSchema>;
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CHECK_TTL_SENTINEL } from "@/Types/Check";
|
||||
import { z } from "zod";
|
||||
|
||||
export const settingsSchema = z.object({
|
||||
@@ -12,11 +11,7 @@ export const settingsSchema = z.object({
|
||||
systemEmailSecure: z.boolean().optional(),
|
||||
systemEmailPool: z.boolean().optional(),
|
||||
showURL: z.boolean().optional(),
|
||||
checkTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, "Please enter a value")
|
||||
.max(CHECK_TTL_SENTINEL, `Maximum ${CHECK_TTL_SENTINEL}`),
|
||||
checkTTL: z.number().int().min(1, "Please enter a value").max(365, "Maximum 365 days"),
|
||||
pagespeedApiKey: z
|
||||
.string()
|
||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
||||
|
||||
@@ -1041,7 +1041,7 @@
|
||||
},
|
||||
"stats": {
|
||||
"title": "Monitor history",
|
||||
"description": "Clear all monitoring history and statistics for your team. This action is irreversible.",
|
||||
"description": "Define how long you want to retain historical data. You can also clear all existing data.",
|
||||
"option": {
|
||||
"clear": {
|
||||
"label": "Clear all stats. This is irreversible."
|
||||
@@ -1052,16 +1052,6 @@
|
||||
"description": "This cannot be undone"
|
||||
}
|
||||
},
|
||||
"retention": {
|
||||
"title": "Check retention",
|
||||
"description": "Set how long check data is retained before being automatically cleaned up.",
|
||||
"option": {
|
||||
"days": {
|
||||
"label": "Retention period (days)",
|
||||
"unlimited": "Unlimited"
|
||||
}
|
||||
}
|
||||
},
|
||||
"thresholds": {
|
||||
"title": "Global Thresholds",
|
||||
"description": "Set global CPU, Memory, Disk, and Temperature thresholds for infrastructure monitoring. These apply to all hardware monitors unless overridden.",
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
DiscordProvider,
|
||||
PagerDutyProvider,
|
||||
MatrixProvider,
|
||||
TeamsProvider,
|
||||
// Interfaces
|
||||
INetworkService,
|
||||
IEmailService,
|
||||
@@ -229,7 +228,6 @@ export const initializeServices = async ({
|
||||
const discordProvider = new DiscordProvider(logger);
|
||||
const pagerDutyProvider = new PagerDutyProvider(logger);
|
||||
const matrixProvider = new MatrixProvider(logger);
|
||||
const teamsProvider = new TeamsProvider(logger);
|
||||
|
||||
const notificationsService = new NotificationsService(
|
||||
notificationsRepository,
|
||||
@@ -240,7 +238,6 @@ export const initializeServices = async ({
|
||||
discordProvider,
|
||||
pagerDutyProvider,
|
||||
matrixProvider,
|
||||
teamsProvider,
|
||||
settingsService,
|
||||
logger,
|
||||
notificationMessageBuilder
|
||||
@@ -252,7 +249,6 @@ export const initializeServices = async ({
|
||||
statusService,
|
||||
notificationsService,
|
||||
checkService,
|
||||
settingsService,
|
||||
bufferService,
|
||||
incidentService,
|
||||
maintenanceWindowsRepository,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { requireTeamId, requireUserEmail, requireUserId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
||||
import { requireTeamId, requireUserRoles } from "./controllerUtils.js";
|
||||
import type { UserRole } from "@/types/user.js";
|
||||
|
||||
import {
|
||||
registrationBodyValidation,
|
||||
@@ -19,16 +20,15 @@ import {
|
||||
editSuperadminUserByIdBodyValidation,
|
||||
editUserPasswordByIdBodyValidation,
|
||||
} from "@/validation/userValidation.js";
|
||||
import { IUserService } from "@/service/index.js";
|
||||
|
||||
const SERVICE_NAME = "authController";
|
||||
|
||||
class AuthController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private userService: IUserService;
|
||||
private userService: any;
|
||||
|
||||
constructor(userService: IUserService) {
|
||||
constructor(userService: any) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@@ -41,12 +41,11 @@ class AuthController {
|
||||
const newUser = req.body.user;
|
||||
const newUserToken = req.body.token;
|
||||
if (newUser?.email) {
|
||||
const newUserEmail = requireUserEmail(newUser.email);
|
||||
newUser.email = newUserEmail.toLowerCase();
|
||||
newUser.email = newUser.email.toLowerCase();
|
||||
}
|
||||
const validatedBody = registrationBodyValidation.parse(newUser);
|
||||
registrationBodyValidation.parse(newUser);
|
||||
|
||||
const { user, token } = await this.userService.registerUser(validatedBody, newUserToken, req?.file ?? null);
|
||||
const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file);
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
msg: "User registered successfully",
|
||||
@@ -63,11 +62,11 @@ class AuthController {
|
||||
if (userData?.email) {
|
||||
userData.email = userData.email.toLowerCase();
|
||||
}
|
||||
const validatedBody = createUserBodyValidation.parse(userData);
|
||||
createUserBodyValidation.parse(userData);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const actorRoles = requireUserRoles(req.user?.role);
|
||||
const newUser = await this.userService.createUser(validatedBody, teamId, actorRoles, req?.file ?? null);
|
||||
const actorRoles = requireUserRoles(req.user?.role) as UserRole[];
|
||||
const newUser = await this.userService.createUser(userData, teamId, actorRoles, req.file);
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
msg: "User created successfully",
|
||||
@@ -101,10 +100,8 @@ class AuthController {
|
||||
|
||||
editUser = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedBody = editUserBodyValidation.parse(req.body);
|
||||
const userId = requireUserId(req.user?.id);
|
||||
const userEmail = requireUserEmail(req.user?.email);
|
||||
const updatedUser = await this.userService.editUser(validatedBody, req?.file ?? null, userId, userEmail);
|
||||
editUserBodyValidation.parse(req.body);
|
||||
const updatedUser = await this.userService.editUser(req.body, req.file, req.user);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
@@ -173,15 +170,7 @@ class AuthController {
|
||||
|
||||
deleteUser = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userId = requireUserId(req.user?.id);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const roles = requireUserRoles(req.user?.role);
|
||||
|
||||
await this.userService.deleteUser({
|
||||
userId,
|
||||
teamId,
|
||||
roles,
|
||||
});
|
||||
await this.userService.deleteUser(req.user);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "User deleted successfully",
|
||||
@@ -193,12 +182,9 @@ class AuthController {
|
||||
|
||||
deleteUserById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getUserByIdParamValidation.parse(req.params);
|
||||
const targetUserId = validatedParams.userId;
|
||||
const actorId = requireUserId(req.user?.id);
|
||||
const actorTeamId = requireTeamId(req.user?.teamId);
|
||||
const actorRoles = requireUserRoles(req.user?.role);
|
||||
await this.userService.deleteUserById({ actorId, actorTeamId, actorRoles, targetUserId });
|
||||
getUserByIdParamValidation.parse(req.params);
|
||||
const targetUserId = req.params.userId;
|
||||
await this.userService.deleteUserById(req.user, targetUserId);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "User removed successfully",
|
||||
@@ -223,9 +209,19 @@ class AuthController {
|
||||
|
||||
getUserById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getUserByIdParamValidation.parse(req.params);
|
||||
const actorRoles = requireUserRoles(req.user?.role);
|
||||
const user = await this.userService.getUserById(actorRoles, validatedParams.userId);
|
||||
getUserByIdParamValidation.parse(req.params);
|
||||
const userId = req?.params?.userId;
|
||||
const roles = req?.user?.role;
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("No user ID in request");
|
||||
}
|
||||
|
||||
if (!roles || roles.length === 0) {
|
||||
throw new Error("No roles in request");
|
||||
}
|
||||
|
||||
const user = await this.userService.getUserById(roles, userId);
|
||||
|
||||
return res.status(200).json({ success: true, msg: "ok", data: user });
|
||||
} catch (error) {
|
||||
@@ -235,19 +231,24 @@ class AuthController {
|
||||
|
||||
editUserById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const actorRoles = requireUserRoles(req.user?.role);
|
||||
const actorId = requireUserId(req.user?.id);
|
||||
const roles = req?.user?.role;
|
||||
|
||||
if (!actorRoles.includes("superadmin")) {
|
||||
if (!roles || !roles.includes("superadmin")) {
|
||||
throw new AppError({ message: "Unauthorized", status: 403 });
|
||||
}
|
||||
|
||||
const validatedParams = editUserByIdParamValidation.parse(req.params);
|
||||
// If this is superadmin self edit, allow "superadmin" role
|
||||
const validatedBody =
|
||||
validatedParams.userId === actorId ? editSuperadminUserByIdBodyValidation.parse(req.body) : editUserByIdBodyValidation.parse(req.body);
|
||||
const userId = req.params.userId as string;
|
||||
const user = { ...req.body };
|
||||
|
||||
await this.userService.editUserById(validatedParams.userId, validatedBody);
|
||||
editUserByIdParamValidation.parse(req.params);
|
||||
// If this is superadmin self edit, allow "superadmin" role
|
||||
if (userId === req.user?.id) {
|
||||
editSuperadminUserByIdBodyValidation.parse(req.body);
|
||||
} else {
|
||||
editUserByIdBodyValidation.parse(req.body);
|
||||
}
|
||||
|
||||
await this.userService.editUserById(userId, user);
|
||||
return res.status(200).json({ success: true, msg: "ok" });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -256,14 +257,16 @@ class AuthController {
|
||||
|
||||
editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const actorRoles = requireUserRoles(req.user?.role);
|
||||
if (!actorRoles.includes("superadmin")) {
|
||||
const roles = req?.user?.role;
|
||||
if (!roles || !roles.includes("superadmin")) {
|
||||
throw new AppError({ message: "Unauthorized", status: 403 });
|
||||
}
|
||||
|
||||
const validatedParams = editUserByIdParamValidation.parse(req.params);
|
||||
const validatedBody = editUserPasswordByIdBodyValidation.parse(req.body);
|
||||
await this.userService.setPasswordByUserId(validatedParams.userId, validatedBody.password);
|
||||
const userId = req.params.userId as string;
|
||||
editUserByIdParamValidation.parse(req.params);
|
||||
editUserPasswordByIdBodyValidation.parse(req.body);
|
||||
const updatedPassword = req.body.password;
|
||||
await this.userService.setPasswordByUserId(userId, updatedPassword);
|
||||
return res.status(200).json({ success: true, msg: "Password reset successfully" });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -4,19 +4,18 @@ import {
|
||||
getChecksParamValidation,
|
||||
getChecksQueryValidation,
|
||||
getTeamChecksQueryValidation,
|
||||
getChecksSummaryByTeamIdQueryValidation,
|
||||
deleteChecksParamValidation,
|
||||
deleteChecksByTeamIdParamValidation,
|
||||
updateChecksTTLBodyValidation,
|
||||
} from "@/validation/checkValidation.js";
|
||||
import { ICheckService } from "@/service/index.js";
|
||||
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
||||
|
||||
const SERVICE_NAME = "checkController";
|
||||
|
||||
class CheckController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private checkService: ICheckService;
|
||||
constructor(checkService: ICheckService) {
|
||||
private checkService: any;
|
||||
constructor(checkService: any) {
|
||||
this.checkService = checkService;
|
||||
}
|
||||
|
||||
@@ -26,20 +25,13 @@ class CheckController {
|
||||
|
||||
getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getChecksParamValidation.parse(req.params);
|
||||
const validatedQuery = getChecksQueryValidation.parse(req.query);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
getChecksParamValidation.parse(req.params);
|
||||
getChecksQueryValidation.parse(req.query);
|
||||
|
||||
const result = await this.checkService.getChecksByMonitor({
|
||||
monitorId: validatedParams.monitorId,
|
||||
teamId,
|
||||
sortOrder: validatedQuery.sortOrder,
|
||||
dateRange: validatedQuery.dateRange,
|
||||
filter: validatedQuery.filter,
|
||||
page: validatedQuery.page,
|
||||
rowsPerPage: validatedQuery.rowsPerPage,
|
||||
status: validatedQuery.status,
|
||||
monitorId: req?.params?.monitorId,
|
||||
query: req?.query,
|
||||
teamId: req?.user?.teamId,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -54,16 +46,10 @@ class CheckController {
|
||||
|
||||
getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedQuery = getTeamChecksQueryValidation.parse(req.query);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
getTeamChecksQueryValidation.parse(req.query);
|
||||
const checkData = await this.checkService.getChecksByTeam({
|
||||
teamId,
|
||||
sortOrder: validatedQuery.sortOrder,
|
||||
dateRange: validatedQuery.dateRange,
|
||||
page: validatedQuery.page,
|
||||
rowsPerPage: validatedQuery.rowsPerPage,
|
||||
filter: validatedQuery.filter,
|
||||
teamId: req?.user?.teamId,
|
||||
query: req?.query,
|
||||
});
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -77,11 +63,8 @@ class CheckController {
|
||||
|
||||
getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedQuery = getChecksSummaryByTeamIdQueryValidation.parse(req.query);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const dateRange = validatedQuery.dateRange ?? "hour";
|
||||
|
||||
const summary = await this.checkService.getChecksSummaryByTeamId({ teamId, dateRange });
|
||||
const dateRange = req.query?.dateRange || "hour";
|
||||
const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId, dateRange });
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Checks summary retrieved successfully",
|
||||
@@ -94,12 +77,11 @@ class CheckController {
|
||||
|
||||
deleteChecks = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = deleteChecksParamValidation.parse(req.params);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
deleteChecksParamValidation.parse(req.params);
|
||||
|
||||
const deletedCount = await this.checkService.deleteChecks({
|
||||
monitorId: validatedParams.monitorId,
|
||||
teamId,
|
||||
monitorId: req.params.monitorId as string,
|
||||
teamId: req?.user?.teamId,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -114,9 +96,9 @@ class CheckController {
|
||||
|
||||
deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
deleteChecksByTeamIdParamValidation.parse(req.params);
|
||||
|
||||
const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId });
|
||||
const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -127,6 +109,24 @@ class CheckController {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
updateChecksTTLBodyValidation.parse(req.body);
|
||||
|
||||
await this.checkService.updateChecksTTL({
|
||||
teamId: req?.user?.teamId,
|
||||
ttl: req?.body?.ttl,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Checks TTL updated successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default CheckController;
|
||||
@@ -1,25 +1,24 @@
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index.js";
|
||||
import sslChecker, { SSLDetails } from "ssl-checker";
|
||||
type SSLCheckerType = typeof sslChecker;
|
||||
import { type MonitorType, MonitorTypes } from "@/types/index.js";
|
||||
|
||||
export const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise<SSLDetails> => {
|
||||
const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise<any> => {
|
||||
const monitorUrl = new URL(monitor.url);
|
||||
const hostname = monitorUrl.hostname;
|
||||
const cert = await checker(hostname);
|
||||
const cert = await sslChecker(hostname);
|
||||
// Throw an error if no cert or if cert.validTo is not present
|
||||
if (cert?.validTo === null || cert?.validTo === undefined) {
|
||||
throw new Error("Certificate not found");
|
||||
}
|
||||
return cert;
|
||||
};
|
||||
export const requireString = (value: unknown, fieldName: string): string => {
|
||||
const requireString = (value: unknown, fieldName: string): string => {
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
return value;
|
||||
}
|
||||
throw new AppError({ message: `${fieldName} is required`, status: 400 });
|
||||
};
|
||||
|
||||
export const optionalString = (value: unknown, fieldName: string): string | undefined => {
|
||||
const optionalString = (value: unknown, fieldName: string): string | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -29,7 +28,7 @@ export const optionalString = (value: unknown, fieldName: string): string | unde
|
||||
throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
|
||||
};
|
||||
|
||||
export const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
|
||||
const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -45,7 +44,7 @@ export const optionalNumber = (value: unknown, fieldName: string): number | unde
|
||||
throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
|
||||
};
|
||||
|
||||
export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
|
||||
const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -63,7 +62,7 @@ export const optionalBoolean = (value: unknown, fieldName: string): boolean | un
|
||||
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
|
||||
};
|
||||
|
||||
export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
|
||||
const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
|
||||
const parseSingle = (input: unknown): MonitorType => {
|
||||
if (typeof input !== "string") {
|
||||
throw new AppError({ message: "Monitor type must be a string", status: 400 });
|
||||
@@ -83,7 +82,7 @@ export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorTyp
|
||||
return parseSingle(value);
|
||||
};
|
||||
|
||||
export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
||||
const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -93,36 +92,42 @@ export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
||||
throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 });
|
||||
};
|
||||
|
||||
export const requireTeamId = (teamId?: string): string => {
|
||||
const requireTeamId = (teamId?: string): string => {
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
return teamId;
|
||||
};
|
||||
|
||||
export const requireUserId = (userId?: string): string => {
|
||||
const requireUserId = (userId?: string): string => {
|
||||
if (!userId) {
|
||||
throw new AppError({ message: "User ID is required", status: 400 });
|
||||
}
|
||||
return userId;
|
||||
};
|
||||
export const requireUserEmail = (userEmail?: string): string => {
|
||||
const requireUserEmail = (userEmail?: string): string => {
|
||||
if (!userEmail) {
|
||||
throw new AppError({ message: "User email is required", status: 400 });
|
||||
}
|
||||
return userEmail;
|
||||
};
|
||||
|
||||
export const requireFirstName = (firstName?: string): string => {
|
||||
if (!firstName) {
|
||||
throw new AppError({ message: "First name is required", status: 400 });
|
||||
}
|
||||
return firstName;
|
||||
};
|
||||
|
||||
export const requireUserRoles = (userRoles?: UserRole[]): UserRole[] => {
|
||||
export const requireUserRoles = (userRoles?: string[]): string[] => {
|
||||
if (!userRoles || userRoles.length === 0) {
|
||||
throw new AppError({ message: "User roles are required", status: 400 });
|
||||
}
|
||||
return userRoles;
|
||||
};
|
||||
|
||||
export {
|
||||
fetchMonitorCertificate,
|
||||
requireString,
|
||||
optionalString,
|
||||
optionalNumber,
|
||||
optionalBoolean,
|
||||
parseMonitorTypeFilter,
|
||||
parseSortOrder,
|
||||
requireTeamId,
|
||||
requireUserId,
|
||||
requireUserEmail,
|
||||
};
|
||||
@@ -1,13 +1,12 @@
|
||||
import { IDiagnosticService } from "@/service/index.js";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
const SERVICE_NAME = "diagnosticController";
|
||||
class DiagnosticController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private diagnosticService: IDiagnosticService;
|
||||
private diagnosticService: any;
|
||||
|
||||
constructor(diagnosticService: IDiagnosticService) {
|
||||
constructor(diagnosticService: any) {
|
||||
this.diagnosticService = diagnosticService;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js";
|
||||
import { IIncidentService } from "@/service/index.js";
|
||||
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
|
||||
|
||||
const SERVICE_NAME = "IncidentController";
|
||||
const SERVICE_NAME = "incidentController";
|
||||
|
||||
class IncidentController {
|
||||
private incidentService: IIncidentService;
|
||||
constructor(incidentService: IIncidentService) {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private incidentService: any;
|
||||
constructor(incidentService: any) {
|
||||
this.incidentService = incidentService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return IncidentController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedQuery = getIncidentsByTeamQueryValidation.parse(req.query);
|
||||
const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {};
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const result = await this.incidentService.getIncidentsByTeam(
|
||||
teamId,
|
||||
validatedQuery.sortOrder,
|
||||
validatedQuery.dateRange,
|
||||
validatedQuery.page,
|
||||
validatedQuery.rowsPerPage,
|
||||
validatedQuery.status,
|
||||
validatedQuery.monitorId,
|
||||
validatedQuery.resolutionType
|
||||
sortOrder,
|
||||
dateRange,
|
||||
page,
|
||||
rowsPerPage,
|
||||
status,
|
||||
monitorId,
|
||||
resolutionType
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -40,9 +45,10 @@ class IncidentController {
|
||||
getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const validatedQuery = getIncidentSummaryQueryValidation.parse(req.query);
|
||||
|
||||
const summary = await this.incidentService.getIncidentSummary(teamId, validatedQuery.limit);
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10;
|
||||
|
||||
const summary = await this.incidentService.getIncidentSummary(teamId, limit);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Incident summary retrieved successfully",
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js";
|
||||
import { requireFirstName, requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
||||
import { IInviteService } from "@/service/index.js";
|
||||
import { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
||||
const SERVICE_NAME = "inviteController";
|
||||
|
||||
class InviteController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private inviteService: IInviteService;
|
||||
constructor(inviteService: IInviteService) {
|
||||
private inviteService: any;
|
||||
constructor(inviteService: any) {
|
||||
this.inviteService = inviteService;
|
||||
}
|
||||
|
||||
@@ -35,9 +34,8 @@ class InviteController {
|
||||
|
||||
sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const userRoles = requireUserRoles(req.user?.role);
|
||||
const firstName = requireFirstName(req.user?.firstName);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const userRoles = requireUserRoles(req?.user?.role);
|
||||
|
||||
const inviteRequest = req.body;
|
||||
inviteRequest.teamId = teamId;
|
||||
@@ -45,7 +43,7 @@ class InviteController {
|
||||
|
||||
const inviteToken = await this.inviteService.sendInviteEmail({
|
||||
invite: inviteRequest,
|
||||
firstName,
|
||||
firstName: req?.user?.firstName,
|
||||
userRoles,
|
||||
});
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
const SERVICE_NAME = "LogController";
|
||||
|
||||
class LogController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private logger: ILogger;
|
||||
constructor(logger: ILogger) {
|
||||
private logger: any;
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
get serviceName() {
|
||||
@@ -15,7 +14,7 @@ class LogController {
|
||||
|
||||
getLogs = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const logs = this.logger.getLogs();
|
||||
const logs = await this.logger.getLogs();
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
msg: "Logs fetched successfully",
|
||||
|
||||
@@ -9,19 +9,26 @@ import {
|
||||
deleteMaintenanceWindowByIdParamValidation,
|
||||
} from "@/validation/maintenanceWindowValidation.js";
|
||||
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
||||
import { IMaintenanceWindowService } from "@/service/index.js";
|
||||
|
||||
const SERVICE_NAME = "maintenanceWindowController";
|
||||
|
||||
class MaintenanceWindowController {
|
||||
private maintenanceWindowService: IMaintenanceWindowService;
|
||||
constructor(maintenanceWindowService: IMaintenanceWindowService) {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private maintenanceWindowService: any;
|
||||
constructor(maintenanceWindowService: any) {
|
||||
this.maintenanceWindowService = maintenanceWindowService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return MaintenanceWindowController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body);
|
||||
createMaintenanceWindowBodyValidation.parse(req.body);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: validatedBody });
|
||||
|
||||
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -33,11 +40,11 @@ class MaintenanceWindowController {
|
||||
};
|
||||
getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||
getMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: validatedParams.id, teamId });
|
||||
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id as string, teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -51,18 +58,11 @@ class MaintenanceWindowController {
|
||||
|
||||
getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedQuery = getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query);
|
||||
getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query);
|
||||
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({
|
||||
teamId,
|
||||
active: validatedQuery.active,
|
||||
page: validatedQuery.page,
|
||||
rowsPerPage: validatedQuery.rowsPerPage,
|
||||
field: validatedQuery.field,
|
||||
order: validatedQuery.order,
|
||||
});
|
||||
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -76,12 +76,12 @@ class MaintenanceWindowController {
|
||||
|
||||
getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params);
|
||||
getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params);
|
||||
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({
|
||||
monitorId: validatedParams.monitorId,
|
||||
monitorId: req.params.monitorId as string,
|
||||
teamId,
|
||||
});
|
||||
|
||||
@@ -96,11 +96,11 @@ class MaintenanceWindowController {
|
||||
};
|
||||
deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = deleteMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||
deleteMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: validatedParams.id, teamId });
|
||||
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id as string, teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -113,14 +113,14 @@ class MaintenanceWindowController {
|
||||
|
||||
editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = editMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||
const validatedBody = editMaintenanceByIdWindowBodyValidation.parse(req.body);
|
||||
editMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||
editMaintenanceByIdWindowBodyValidation.parse(req.body);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({
|
||||
id: validatedParams.id,
|
||||
body: validatedBody,
|
||||
id: req.params.id as string,
|
||||
body: req.body,
|
||||
teamId,
|
||||
});
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Request, Response, NextFunction } from "express";
|
||||
import {
|
||||
getMonitorByIdParamValidation,
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsByTeamIdParamValidation,
|
||||
getMonitorsByTeamIdQueryValidation,
|
||||
getMonitorsWithChecksQueryValidation,
|
||||
createMonitorBodyValidation,
|
||||
@@ -11,14 +12,22 @@ import {
|
||||
getCertificateParamValidation,
|
||||
getHardwareDetailsByIdParamValidation,
|
||||
getHardwareDetailsByIdQueryValidation,
|
||||
getUptimeDetailsByIdParamValidation,
|
||||
getUptimeDetailsByIdQueryValidation,
|
||||
importMonitorsBodyValidation,
|
||||
} from "@/validation/monitorValidation.js";
|
||||
import sslChecker from "ssl-checker";
|
||||
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "./controllerUtils.js";
|
||||
import {
|
||||
fetchMonitorCertificate,
|
||||
requireString,
|
||||
optionalString,
|
||||
optionalNumber,
|
||||
optionalBoolean,
|
||||
parseMonitorTypeFilter,
|
||||
parseSortOrder,
|
||||
requireTeamId,
|
||||
requireUserId,
|
||||
} from "./controllerUtils.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorService } from "@/service/index.js";
|
||||
import { GeoContinent } from "@/types/geoCheck.js";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
class MonitorController {
|
||||
@@ -36,9 +45,10 @@ class MonitorController {
|
||||
|
||||
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getCertificateParamValidation.parse(req.params);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId });
|
||||
getCertificateParamValidation.parse(req.params);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const monitorId = requireString(req.params?.monitorId, "Monitor ID");
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -55,14 +65,10 @@ class MonitorController {
|
||||
|
||||
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getUptimeDetailsByIdParamValidation.parse(req.params);
|
||||
const validatedQuery = getUptimeDetailsByIdQueryValidation.parse(req.query);
|
||||
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const dateRange = validatedQuery.dateRange;
|
||||
const normalize = validatedQuery.normalize;
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||
const normalize = optionalBoolean(req?.query?.normalize, "normalize");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const data = await this.monitorService.getUptimeDetailsById({
|
||||
teamId,
|
||||
@@ -82,12 +88,12 @@ class MonitorController {
|
||||
|
||||
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params);
|
||||
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query);
|
||||
getHardwareDetailsByIdParamValidation.parse(req.params);
|
||||
getHardwareDetailsByIdQueryValidation.parse(req.query);
|
||||
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const dateRange = validatedQuery.dateRange || "recent";
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent";
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const data = await this.monitorService.getHardwareDetailsById({
|
||||
teamId,
|
||||
@@ -106,12 +112,12 @@ class MonitorController {
|
||||
};
|
||||
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params);
|
||||
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query);
|
||||
getHardwareDetailsByIdParamValidation.parse(req.params);
|
||||
getHardwareDetailsByIdQueryValidation.parse(req.query);
|
||||
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const dateRange = validatedQuery.dateRange || "recent";
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const data = await this.monitorService.getPageSpeedDetailsById({
|
||||
teamId,
|
||||
@@ -131,14 +137,18 @@ class MonitorController {
|
||||
|
||||
getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||
const validatedQuery = getMonitorByIdQueryValidation.parse(req.query);
|
||||
getMonitorByIdParamValidation.parse(req.params);
|
||||
getMonitorByIdQueryValidation.parse(req.query);
|
||||
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const dateRange = validatedQuery.dateRange || "recent";
|
||||
const continentParam = validatedQuery.continent;
|
||||
const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined;
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||
const continentParam = req?.query?.continent;
|
||||
const continents = continentParam
|
||||
? Array.isArray(continentParam)
|
||||
? (continentParam as GeoContinent[])
|
||||
: [continentParam as GeoContinent]
|
||||
: undefined;
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const data = await this.monitorService.getGeoChecksByMonitorId({
|
||||
teamId,
|
||||
@@ -159,10 +169,11 @@ class MonitorController {
|
||||
|
||||
getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||
getMonitorByIdParamValidation.parse(req.params);
|
||||
getMonitorByIdQueryValidation.parse(req.query);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||
|
||||
@@ -178,12 +189,12 @@ class MonitorController {
|
||||
|
||||
createMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedBody = createMonitorBodyValidation.parse(req.body);
|
||||
createMonitorBodyValidation.parse(req.body);
|
||||
|
||||
const userId = requireUserId(req.user?.id);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const userId = requireString(req?.user?.id, "User ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody);
|
||||
const monitor = await this.monitorService.createMonitor(teamId, userId, req.body);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -197,10 +208,14 @@ class MonitorController {
|
||||
|
||||
importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const userId = requireUserId(req.user?.id);
|
||||
const validatedBody = importMonitorsBodyValidation.parse(req.body);
|
||||
const monitors = validatedBody.monitors;
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const userId = requireUserId(req?.user?.id);
|
||||
|
||||
const { monitors } = req.body;
|
||||
|
||||
if (!monitors || !Array.isArray(monitors)) {
|
||||
throw new AppError({ message: "Invalid request: monitors array is required", status: 400 });
|
||||
}
|
||||
|
||||
const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors });
|
||||
|
||||
@@ -216,9 +231,9 @@ class MonitorController {
|
||||
|
||||
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const monitorId = validatedParams.monitorId;
|
||||
getMonitorByIdParamValidation.parse(req.params);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
|
||||
|
||||
@@ -234,7 +249,8 @@ class MonitorController {
|
||||
|
||||
deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -248,12 +264,12 @@ class MonitorController {
|
||||
|
||||
editMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||
const validatedBody = editMonitorBodyValidation.parse(req.body);
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
getMonitorByIdParamValidation.parse(req.params);
|
||||
editMonitorBodyValidation.parse(req.body);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody });
|
||||
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -267,10 +283,10 @@ class MonitorController {
|
||||
|
||||
pauseMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedParams = pauseMonitorParamValidation.parse(req.params);
|
||||
pauseMonitorParamValidation.parse(req.params);
|
||||
|
||||
const monitorId = validatedParams.monitorId;
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
|
||||
|
||||
@@ -286,8 +302,8 @@ class MonitorController {
|
||||
|
||||
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const id = requireUserId(req.user?.id);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const id = requireString(req?.user?.id, "User ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -300,13 +316,32 @@ class MonitorController {
|
||||
}
|
||||
};
|
||||
|
||||
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { to } = req.body;
|
||||
if (!to || typeof to !== "string") {
|
||||
throw new AppError({ message: "Invalid 'to' email address", status: 400 });
|
||||
}
|
||||
|
||||
const messageId = await this.monitorService.sendTestEmail({ to });
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Test email sent successfully",
|
||||
data: { messageId },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query);
|
||||
getMonitorsByTeamIdParamValidation.parse(req.params);
|
||||
getMonitorsByTeamIdQueryValidation.parse(req.query);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const type = validatedQuery.type;
|
||||
const filter = validatedQuery.filter;
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const type = parseMonitorTypeFilter(req.query?.type);
|
||||
const filter = optionalString(req.query?.filter, "filter");
|
||||
|
||||
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
|
||||
|
||||
@@ -322,17 +357,17 @@ class MonitorController {
|
||||
|
||||
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query);
|
||||
|
||||
const explain = validatedQuery.explain;
|
||||
const limit = validatedQuery.limit;
|
||||
const page = validatedQuery.page;
|
||||
const rowsPerPage = validatedQuery.rowsPerPage;
|
||||
const filter = validatedQuery.filter;
|
||||
const field = validatedQuery.field;
|
||||
const order = validatedQuery.order;
|
||||
const type = validatedQuery.type;
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
getMonitorsByTeamIdParamValidation.parse(req.params);
|
||||
getMonitorsWithChecksQueryValidation.parse(req.query);
|
||||
const explain = optionalBoolean(req?.query?.explain, "explain");
|
||||
const limit = optionalNumber(req?.query?.limit, "limit");
|
||||
const page = optionalNumber(req?.query?.page, "page");
|
||||
const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage");
|
||||
const filter = optionalString(req?.query?.filter, "filter");
|
||||
const field = optionalString(req?.query?.field, "field");
|
||||
const order = parseSortOrder(req?.query?.order);
|
||||
const type = parseMonitorTypeFilter(req?.query?.type);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
|
||||
teamId,
|
||||
@@ -357,7 +392,11 @@ class MonitorController {
|
||||
|
||||
exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
|
||||
const json = await this.monitorService.exportMonitorsToJSON({ teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -385,7 +424,11 @@ class MonitorController {
|
||||
|
||||
getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
|
||||
const groups = await this.monitorService.getGroupsByTeamId({ teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
import {
|
||||
createNotificationBodyValidation,
|
||||
deleteNotificationParamValidation,
|
||||
getNotificationByIdParamValidation,
|
||||
testNotificationBodyValidation,
|
||||
editNotificationParamValidation,
|
||||
testAllNotificationsBodyValidation,
|
||||
} from "@/validation/notificationValidation.js";
|
||||
import { Notification } from "@/types/index.js";
|
||||
import { createNotificationBodyValidation } from "@/validation/notificationValidation.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import { INotificationsService } from "@/service/index.js";
|
||||
import { requireTeamId, requireUserId } from "./controllerUtils.js";
|
||||
import { requireTeamId } from "./controllerUtils.js";
|
||||
|
||||
const SERVICE_NAME = "NotificationController";
|
||||
|
||||
class NotificationController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private notificationsService: INotificationsService;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
|
||||
@@ -23,9 +18,13 @@ class NotificationController {
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return NotificationController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
testNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const notification = testNotificationBodyValidation.parse(req.body);
|
||||
const notification: Notification = req.body;
|
||||
const success = await this.notificationsService.sendTestNotification(notification);
|
||||
|
||||
if (!success) {
|
||||
@@ -35,7 +34,6 @@ class NotificationController {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Notification sent successfully",
|
||||
details: { service: SERVICE_NAME },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -44,12 +42,23 @@ class NotificationController {
|
||||
|
||||
createNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedBody = createNotificationBodyValidation.parse(req.body);
|
||||
createNotificationBodyValidation.parse(req.body);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const userId = requireUserId(req.user?.id);
|
||||
const body = req.body;
|
||||
|
||||
const notification = await this.notificationsService.createNotification(validatedBody, userId, teamId);
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
|
||||
const userId = req?.user?.id;
|
||||
if (!userId) {
|
||||
throw new AppError({ message: "User ID is required", status: 400 });
|
||||
}
|
||||
body.userId = userId;
|
||||
body.teamId = teamId;
|
||||
|
||||
const notification = await this.notificationsService.createNotification(body);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Notification created successfully",
|
||||
@@ -62,7 +71,11 @@ class NotificationController {
|
||||
|
||||
getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
|
||||
const notifications = await this.notificationsService.findNotificationsByTeamId(teamId);
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -77,10 +90,17 @@ class NotificationController {
|
||||
|
||||
deleteNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const validatedParams = deleteNotificationParamValidation.parse(req.params);
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
|
||||
await this.notificationsService.deleteById(validatedParams.id, teamId);
|
||||
const notificationId = req.params.id as string;
|
||||
if (!notificationId) {
|
||||
throw new AppError({ message: "Notification ID is required", status: 400 });
|
||||
}
|
||||
|
||||
await this.notificationsService.deleteById(notificationId, teamId);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Notification deleted successfully",
|
||||
@@ -93,9 +113,12 @@ class NotificationController {
|
||||
getNotificationById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const validatedParams = getNotificationByIdParamValidation.parse(req.params);
|
||||
const notificationId = req.params.id as string;
|
||||
if (!notificationId) {
|
||||
throw new AppError({ message: "Notification ID is required", status: 400 });
|
||||
}
|
||||
|
||||
const notification = await this.notificationsService.findById(validatedParams.id, teamId);
|
||||
const notification = await this.notificationsService.findById(notificationId, teamId);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -109,13 +132,14 @@ class NotificationController {
|
||||
|
||||
editNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedBody = createNotificationBodyValidation.parse(req.body);
|
||||
const validatedParams = editNotificationParamValidation.parse(req.params);
|
||||
createNotificationBodyValidation.parse(req.body);
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const notificationId = validatedParams.id;
|
||||
|
||||
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody);
|
||||
const notificationId = req.params.id as string;
|
||||
if (!notificationId) {
|
||||
throw new AppError({ message: "Notification ID is required", status: 400 });
|
||||
}
|
||||
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Notification updated successfully",
|
||||
@@ -128,11 +152,13 @@ class NotificationController {
|
||||
|
||||
testAllNotifications = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const validatedBody = testAllNotificationsBodyValidation.parse(req.body);
|
||||
const monitorId = req.body.monitorId;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId);
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
|
||||
const notifications = monitor.notifications;
|
||||
if (notifications.length === 0) {
|
||||
|
||||
@@ -21,10 +21,13 @@ type CheckMetadataDocument = Omit<CheckMetadata, "monitorId" | "teamId"> & {
|
||||
type: MonitorType;
|
||||
};
|
||||
|
||||
type CheckDocumentBase = Omit<Check, "id" | "metadata" | "createdAt" | "updatedAt"> & {
|
||||
type CheckDocumentBase = Omit<Check, "id" | "metadata" | "expiry" | "ackAt" | "createdAt" | "updatedAt"> & {
|
||||
metadata: CheckMetadataDocument;
|
||||
expiry: Date;
|
||||
ackAt?: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
__v: number;
|
||||
};
|
||||
|
||||
interface CheckDocument extends CheckDocumentBase {
|
||||
@@ -222,7 +225,18 @@ const CheckSchema = new Schema<CheckDocument>(
|
||||
message: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
ack: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
ackAt: {
|
||||
type: Date,
|
||||
default: undefined,
|
||||
},
|
||||
cpu: {
|
||||
type: cpuSchema,
|
||||
default: () => ({}),
|
||||
|
||||
@@ -25,7 +25,7 @@ const NotificationSchema = new Schema<NotificationDocument>(
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as NotificationChannel[],
|
||||
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as NotificationChannel[],
|
||||
required: true,
|
||||
},
|
||||
notificationName: {
|
||||
|
||||
@@ -47,5 +47,4 @@ export interface IChecksRepository {
|
||||
deleteByMonitorId(monitorId: string): Promise<number>;
|
||||
deleteByTeamId(teamId: string): Promise<number>;
|
||||
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
|
||||
deleteOlderThan(date: Date): Promise<number>;
|
||||
}
|
||||
@@ -13,10 +13,9 @@ import type {
|
||||
GotTimings,
|
||||
MonitorType,
|
||||
} from "@/types/index.js";
|
||||
import { CheckModel, type CheckDocument } from "@/db/models/index.js";
|
||||
import { CheckModel, MonitorModel, type CheckDocument } from "@/db/models/index.js";
|
||||
import mongoose from "mongoose";
|
||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
|
||||
const SERVICE_NAME = "StatusService";
|
||||
|
||||
@@ -27,8 +26,8 @@ type HardwareUpChecks = { totalChecks: number };
|
||||
class MongoChecksRepository implements IChecksRepository {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private logger: ILogger;
|
||||
constructor(logger: ILogger) {
|
||||
private logger: any;
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@@ -47,6 +46,13 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
||||
};
|
||||
|
||||
const toOptionalDateString = (value?: Date | string | null): string | undefined => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return toDateString(value);
|
||||
};
|
||||
|
||||
const mapTimings = (timings?: GotTimings): GotTimings => {
|
||||
const phases = timings?.phases ?? {
|
||||
wait: 0,
|
||||
@@ -165,6 +171,9 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
timings: mapTimings(doc.timings),
|
||||
statusCode: doc.statusCode ?? 0,
|
||||
message: doc.message ?? "",
|
||||
ack: doc.ack ?? false,
|
||||
ackAt: toOptionalDateString(doc.ackAt),
|
||||
expiry: toDateString(doc.expiry),
|
||||
cpu: mapCpu(doc.cpu),
|
||||
memory: mapMemory(doc.memory),
|
||||
disk: mapDisks(doc.disk),
|
||||
@@ -177,6 +186,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
seo: doc.seo,
|
||||
performance: doc.performance,
|
||||
audits: mapAudits(doc.audits),
|
||||
__v: doc.__v ?? 0,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
};
|
||||
@@ -227,7 +237,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
status: boolean | undefined
|
||||
) => {
|
||||
// Match
|
||||
const matchStage: Record<string, unknown> = {
|
||||
const matchStage: Record<string, any> = {
|
||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||
...(typeof status !== "undefined" && { status }),
|
||||
...(getDateForRange(dateRange) && {
|
||||
@@ -279,7 +289,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
};
|
||||
|
||||
findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => {
|
||||
const matchStage: Record<string, unknown> = {
|
||||
const matchStage: Record<string, any> = {
|
||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||
...(getDateForRange(dateRange) && {
|
||||
createdAt: {
|
||||
@@ -348,7 +358,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
);
|
||||
|
||||
const mapped = results.reduce<LatestChecksMap>((acc, { monitorId, docs }) => {
|
||||
acc[monitorId] = docs.map((doc: CheckDocument) => this.toEntity(doc));
|
||||
acc[monitorId] = docs.map((doc: any) => this.toEntity(doc as CheckDocument));
|
||||
return acc;
|
||||
}, {});
|
||||
return mapped;
|
||||
@@ -402,31 +412,6 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
return result.deletedCount ?? 0;
|
||||
};
|
||||
|
||||
deleteOlderThan = async (cutoffDate: Date, batchDays: number = 30): Promise<number> => {
|
||||
// Find the oldest check that is older than the cutoff
|
||||
const oldest = await CheckModel.findOne({ createdAt: { $lt: cutoffDate } })
|
||||
.sort({ createdAt: 1 })
|
||||
.select({ createdAt: 1 });
|
||||
if (!oldest?.createdAt) return 0;
|
||||
|
||||
let totalDeleted = 0;
|
||||
let batchStart = new Date(oldest.createdAt);
|
||||
const batchMs = batchDays * 24 * 60 * 60 * 1000;
|
||||
|
||||
// Delete in 30 day chunks until we reach the cutoff
|
||||
while (batchStart < cutoffDate) {
|
||||
// Advance startTime by batchMs to get to the end of last batch
|
||||
const nextStart = batchStart.getTime() + batchMs;
|
||||
const batchEnd = new Date(Math.min(nextStart, cutoffDate.getTime()));
|
||||
const result = await CheckModel.deleteMany({
|
||||
createdAt: { $gte: batchStart, $lt: batchEnd },
|
||||
});
|
||||
totalDeleted += result.deletedCount ?? 0;
|
||||
batchStart = batchEnd;
|
||||
}
|
||||
|
||||
return totalDeleted;
|
||||
};
|
||||
private findUptimeDateRangeChecks = async (
|
||||
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">,
|
||||
monitorObjectId: mongoose.Types.ObjectId,
|
||||
|
||||
@@ -5,12 +5,12 @@ export interface IMaintenanceWindowsRepository {
|
||||
// fetch
|
||||
findById(id: string, teamId: string): Promise<MaintenanceWindow>;
|
||||
findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>;
|
||||
findByTeamId(teamId: string, page: number, rowsPerPage: number, field?: string, order?: string, active?: boolean): Promise<MaintenanceWindow[]>;
|
||||
findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise<MaintenanceWindow[]>;
|
||||
|
||||
// update
|
||||
updateById(id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>;
|
||||
// delete
|
||||
deleteById(id: string, teamId: string): Promise<MaintenanceWindow>;
|
||||
// other
|
||||
countByTeamId(teamId: string, active?: boolean): Promise<number>;
|
||||
countByTeamId(teamId: string, active: boolean): Promise<number>;
|
||||
}
|
||||
@@ -75,11 +75,11 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
|
||||
|
||||
findByTeamId = async (
|
||||
teamId: string,
|
||||
active: boolean,
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
field?: string,
|
||||
order?: string,
|
||||
active?: boolean
|
||||
field: string,
|
||||
order: string
|
||||
): Promise<MaintenanceWindow[]> => {
|
||||
const maintenanceQuery: Record<string, any> = { teamId };
|
||||
|
||||
@@ -126,7 +126,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
|
||||
}
|
||||
return this.toEntity(deleted);
|
||||
};
|
||||
countByTeamId = async (teamId: string, active?: boolean) => {
|
||||
countByTeamId = async (teamId: string, active: boolean) => {
|
||||
const maintenanceQuery: Record<string, any> = { teamId };
|
||||
|
||||
if (active !== undefined) maintenanceQuery.active = active;
|
||||
|
||||
@@ -15,6 +15,7 @@ class CheckRoutes {
|
||||
initRoutes() {
|
||||
this.router.get("/team/summary", this.checkController.getChecksSummaryByTeamId);
|
||||
this.router.get("/team", this.checkController.getChecksByTeam);
|
||||
this.router.put("/team/ttl", isAllowed(["admin", "superadmin"]), this.checkController.updateChecksTTL);
|
||||
this.router.delete("/team", isAllowed(["admin", "superadmin"]), this.checkController.deleteChecksByTeamId);
|
||||
this.router.get("/:monitorId", this.checkController.getChecksByMonitor);
|
||||
this.router.delete("/:monitorId", this.checkController.deleteChecks);
|
||||
|
||||
@@ -50,6 +50,7 @@ class MonitorRoutes {
|
||||
this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON);
|
||||
this.router.post("/import/json", isAllowed(["admin", "superadmin"]), this.monitorController.importMonitorsFromJSON);
|
||||
|
||||
this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail);
|
||||
this.router.get("/games", this.monitorController.getAllGames);
|
||||
|
||||
// Individual monitor CRUD routes
|
||||
|
||||
@@ -1,45 +1,22 @@
|
||||
import { Types } from "mongoose";
|
||||
import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||
import type {
|
||||
MonitorStatusResponse,
|
||||
CheckErrorInfo,
|
||||
Check,
|
||||
ILighthouseAudit,
|
||||
ChecksQueryResult,
|
||||
ChecksSummary,
|
||||
MonitorPayloadMap,
|
||||
} from "@/types/index.js";
|
||||
import type { MonitorStatusResponse, CheckErrorInfo, Check, ILighthouseAudit, ChecksQueryResult, ChecksSummary } from "@/types/index.js";
|
||||
import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ParseBoolean } from "@/utils/utils.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
|
||||
const SERVICE_NAME = "checkService";
|
||||
|
||||
export interface ICheckService {
|
||||
createChecks(checks: Check[]): Promise<Check[]>;
|
||||
buildCheck(statusResponse: MonitorStatusResponse<MonitorPayloadMap[keyof MonitorPayloadMap]>): Check | undefined;
|
||||
getChecksByMonitor(params: {
|
||||
monitorId: string;
|
||||
teamId: string;
|
||||
sortOrder: string;
|
||||
dateRange: string;
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
filter?: string;
|
||||
status?: boolean;
|
||||
}): Promise<ChecksQueryResult>;
|
||||
getChecksByTeam(params: {
|
||||
teamId: string;
|
||||
sortOrder: string;
|
||||
dateRange: string;
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
filter?: string;
|
||||
}): Promise<ChecksQueryResult>;
|
||||
buildCheck(statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>): Partial<Check> | undefined;
|
||||
getChecksByMonitor(params: { monitorId: string; query: Record<string, string>; teamId: string }): Promise<ChecksQueryResult>;
|
||||
getChecksByTeam(params: { teamId: string; query: Record<string, string> }): Promise<ChecksQueryResult>;
|
||||
getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise<ChecksSummary>;
|
||||
deleteChecks(params: { monitorId: string; teamId: string }): Promise<number>;
|
||||
deleteChecksByTeamId(params: { teamId: string }): Promise<number>;
|
||||
deleteOlderThan(date: Date): Promise<number>;
|
||||
updateChecksTTL(params: { teamId: string; ttl: string }): Promise<void>;
|
||||
}
|
||||
|
||||
export class CheckService implements ICheckService {
|
||||
@@ -62,14 +39,12 @@ export class CheckService implements ICheckService {
|
||||
return this.checksRepository.createChecks(checks);
|
||||
};
|
||||
|
||||
buildCheck = (statusResponse: MonitorStatusResponse<MonitorPayloadMap[keyof MonitorPayloadMap]>): Check | undefined => {
|
||||
buildCheck = (statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>) => {
|
||||
const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const check: Check = {
|
||||
const check: Partial<Check> = {
|
||||
id: new Types.ObjectId().toString(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdAt: new Date().toISOString(),
|
||||
metadata: {
|
||||
monitorId,
|
||||
teamId,
|
||||
@@ -137,25 +112,7 @@ export class CheckService implements ICheckService {
|
||||
return check;
|
||||
};
|
||||
|
||||
getChecksByMonitor = async ({
|
||||
monitorId,
|
||||
teamId,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
status,
|
||||
}: {
|
||||
monitorId: string;
|
||||
teamId: string;
|
||||
sortOrder: string;
|
||||
dateRange: string;
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
status?: boolean;
|
||||
filter?: string;
|
||||
}) => {
|
||||
getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: Record<string, string>; teamId: string }) => {
|
||||
if (!monitorId) {
|
||||
throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
|
||||
}
|
||||
@@ -166,54 +123,74 @@ export class CheckService implements ICheckService {
|
||||
// For verification, throws an error if monitor doesn't belong to team
|
||||
await this.monitorsRepository.findById(monitorId, teamId);
|
||||
|
||||
const parsedPage = page ?? 0;
|
||||
const parsedRowsPerPage = rowsPerPage ?? 5;
|
||||
const { sortOrder, dateRange, filter, page, rowsPerPage, status } = query;
|
||||
if (!sortOrder || !dateRange || !page || !rowsPerPage) {
|
||||
throw new AppError({
|
||||
message: `Missing required query parameters sortOrder: ${sortOrder}, dateRange: ${dateRange}, filter: ${filter}, page: ${page}, rowsPerPage: ${rowsPerPage}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getChecksByMonitor",
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status);
|
||||
const parsedPage = page ? parseInt(page) : 0;
|
||||
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5;
|
||||
|
||||
const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, status);
|
||||
const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
getChecksByTeam = async ({
|
||||
teamId,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
}: {
|
||||
teamId: string;
|
||||
sortOrder: string;
|
||||
dateRange: string;
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
filter?: string;
|
||||
}) => {
|
||||
const parsedPage = page ?? 0;
|
||||
const parsedRowsPerPage = rowsPerPage ?? 5;
|
||||
getChecksByTeam = async ({ teamId, query }: { teamId: string; query: Record<string, string> }) => {
|
||||
const { sortOrder, dateRange, filter, page, rowsPerPage } = query;
|
||||
if (!sortOrder || !dateRange || !page || !rowsPerPage) {
|
||||
throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
|
||||
}
|
||||
|
||||
const parsedPage = page ? parseInt(page) : 0;
|
||||
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5;
|
||||
|
||||
const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId);
|
||||
return checkData;
|
||||
};
|
||||
|
||||
getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: string }) => {
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksSummaryByTeamId", status: 400 });
|
||||
}
|
||||
const summary = await this.checksRepository.findSummaryByTeamId(teamId, dateRange);
|
||||
return summary;
|
||||
};
|
||||
|
||||
deleteChecks = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => {
|
||||
if (!monitorId) {
|
||||
throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 });
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 });
|
||||
}
|
||||
|
||||
// For verification, throws an error if monitor doesn't belong to team
|
||||
await this.monitorsRepository.findById(monitorId, teamId);
|
||||
|
||||
const deletedCount = await this.checksRepository.deleteByMonitorId(monitorId);
|
||||
return deletedCount;
|
||||
};
|
||||
deleteChecksByTeamId = async ({ teamId }: { teamId: string }) => {
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecksByTeamId", status: 400 });
|
||||
}
|
||||
|
||||
const deletedCount = await this.checksRepository.deleteByTeamId(teamId);
|
||||
return deletedCount;
|
||||
};
|
||||
|
||||
deleteOlderThan = async (date: Date) => {
|
||||
const deletedCount = await this.checksRepository.deleteOlderThan(date);
|
||||
return deletedCount;
|
||||
updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => {
|
||||
throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } });
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@ const SERVICE_NAME = "incidentService";
|
||||
import type { Monitor } from "@/types/monitor.js";
|
||||
import type { MonitorStatusResponse } from "@/types/network.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ParseBoolean } from "@/utils/utils.js";
|
||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
|
||||
import type { Incident, IncidentSummary, User } from "@/types/index.js";
|
||||
@@ -21,13 +22,13 @@ export interface IIncidentService {
|
||||
teamId: string,
|
||||
sortOrder: string,
|
||||
dateRange: string,
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
status: boolean | undefined,
|
||||
monitorId: string | undefined,
|
||||
resolutionType: string | undefined
|
||||
page: string,
|
||||
rowsPerPage: string,
|
||||
status: string,
|
||||
monitorId: string,
|
||||
resolutionType: string
|
||||
): Promise<{ incidents: Incident[]; count: number }>;
|
||||
getIncidentSummary(teamId: string, limit?: number): Promise<IncidentSummary>;
|
||||
getIncidentSummary(teamId: string, limit?: string): Promise<IncidentSummary>;
|
||||
getIncidentById(incidentId: string, teamId: string): Promise<{ incident: Incident; monitor: Monitor; user: User | null }>;
|
||||
}
|
||||
|
||||
@@ -163,13 +164,13 @@ export class IncidentService implements IIncidentService {
|
||||
});
|
||||
|
||||
return resolvedIncident;
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
service: SERVICE_NAME,
|
||||
method: "resolveIncident",
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
message: error.message,
|
||||
details: { id: incidentId },
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -179,11 +180,11 @@ export class IncidentService implements IIncidentService {
|
||||
teamId: string,
|
||||
sortOrder: string,
|
||||
dateRange: string,
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
status: boolean | undefined,
|
||||
monitorId: string | undefined,
|
||||
resolutionType: string | undefined
|
||||
page: string,
|
||||
rowsPerPage: string,
|
||||
status: string,
|
||||
monitorId: string,
|
||||
resolutionType: string
|
||||
) => {
|
||||
try {
|
||||
if (!teamId) {
|
||||
@@ -192,8 +193,9 @@ export class IncidentService implements IIncidentService {
|
||||
|
||||
const startDate = getDateForRange(dateRange);
|
||||
|
||||
const parsedPage = page ?? 0;
|
||||
const parsedRowsPerPage = rowsPerPage ?? 20;
|
||||
const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
|
||||
const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20;
|
||||
const parsedStatus = typeof status === "undefined" ? undefined : ParseBoolean(status);
|
||||
|
||||
const incidents = await this.incidentsRepository.findByTeamId(
|
||||
teamId,
|
||||
@@ -201,43 +203,43 @@ export class IncidentService implements IIncidentService {
|
||||
parsedPage,
|
||||
parsedRowsPerPage,
|
||||
sortOrder,
|
||||
status,
|
||||
parsedStatus,
|
||||
monitorId,
|
||||
resolutionType
|
||||
);
|
||||
|
||||
const count = await this.incidentsRepository.countByTeamId(teamId, startDate, status, monitorId, resolutionType);
|
||||
const count = await this.incidentsRepository.countByTeamId(teamId, startDate, parsedStatus, monitorId, resolutionType);
|
||||
|
||||
return { incidents, count };
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
service: SERVICE_NAME,
|
||||
method: "getIncidentsByTeam",
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
message: error.message,
|
||||
details: { teamId },
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getIncidentSummary = async (teamId: string, limit?: number) => {
|
||||
getIncidentSummary = async (teamId: string, limit?: string) => {
|
||||
try {
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentSummary", status: 400 });
|
||||
}
|
||||
|
||||
const parsedLimit = limit ?? 10;
|
||||
const parsedLimit = limit && Number.isFinite(parseInt(limit, 10)) ? parseInt(limit, 10) : 10;
|
||||
const summary = await this.incidentsRepository.findSummaryByTeamId(teamId, parsedLimit);
|
||||
|
||||
return summary;
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
service: SERVICE_NAME,
|
||||
method: "getIncidentSummary",
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
message: error.message,
|
||||
details: { teamId },
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -252,13 +254,13 @@ export class IncidentService implements IIncidentService {
|
||||
user = await this.usersRepository.findById(incident.resolvedBy);
|
||||
}
|
||||
return { incident, monitor, user };
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
service: SERVICE_NAME,
|
||||
method: "getIncidentById",
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
message: error.message,
|
||||
details: { incidentId },
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||
import type { MaintenanceWindow } from "@/types/index.js";
|
||||
import { ParseBoolean } from "@/utils/utils.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
const SERVICE_NAME = "maintenanceWindowService";
|
||||
@@ -9,11 +10,7 @@ export interface IMaintenanceWindowService {
|
||||
getMaintenanceWindowById(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
|
||||
getMaintenanceWindowsByTeamId(params: {
|
||||
teamId: string;
|
||||
active?: boolean;
|
||||
page?: number;
|
||||
rowsPerPage?: number;
|
||||
field?: string;
|
||||
order?: string;
|
||||
query: Record<string, string>;
|
||||
}): Promise<{ maintenanceWindows: MaintenanceWindow[]; maintenanceWindowCount: number }>;
|
||||
getMaintenanceWindowsByMonitorId(params: { monitorId: string; teamId: string }): Promise<MaintenanceWindow[]>;
|
||||
deleteMaintenanceWindow(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
|
||||
@@ -75,25 +72,14 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
|
||||
return await this.maintenanceWindowsRepository.findById(id, teamId);
|
||||
};
|
||||
|
||||
getMaintenanceWindowsByTeamId = async ({
|
||||
teamId,
|
||||
active,
|
||||
page,
|
||||
rowsPerPage,
|
||||
field,
|
||||
order,
|
||||
}: {
|
||||
teamId: string;
|
||||
active?: boolean;
|
||||
page?: number;
|
||||
rowsPerPage?: number;
|
||||
field?: string;
|
||||
order?: string;
|
||||
}) => {
|
||||
page = page ?? 0;
|
||||
rowsPerPage = rowsPerPage ?? 10;
|
||||
getMaintenanceWindowsByTeamId = async ({ teamId, query }: { teamId: string; query: any }) => {
|
||||
let { active, page, rowsPerPage, field, order } = query || {};
|
||||
|
||||
const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, page, rowsPerPage, field, order, active);
|
||||
active = typeof active === "undefined" ? undefined : ParseBoolean(active);
|
||||
page = parseInt(page) || 0;
|
||||
rowsPerPage = parseInt(rowsPerPage) || 10;
|
||||
|
||||
const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, active, page, rowsPerPage, field, order);
|
||||
const maintenanceWindowCount = await this.maintenanceWindowsRepository.countByTeamId(teamId, active);
|
||||
return { maintenanceWindows, maintenanceWindowCount };
|
||||
};
|
||||
|
||||
@@ -18,7 +18,9 @@ import type {
|
||||
IMonitorStatsRepository,
|
||||
IStatusPagesRepository,
|
||||
} from "@/repositories/index.js";
|
||||
import demoMonitorsData from "@/utils/demoMonitors.json" with { type: "json" };
|
||||
import fs from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||
|
||||
@@ -29,7 +31,7 @@ export interface IMonitorService {
|
||||
readonly serviceName: string;
|
||||
|
||||
// create
|
||||
createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<void>;
|
||||
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
|
||||
createMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
|
||||
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
|
||||
|
||||
@@ -64,7 +66,7 @@ export interface IMonitorService {
|
||||
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
|
||||
|
||||
// update
|
||||
editMonitor(args: { teamId: string; monitorId: string; body: Partial<Monitor> }): Promise<Monitor>;
|
||||
editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise<Monitor>;
|
||||
pauseMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
||||
|
||||
// delete
|
||||
@@ -72,6 +74,7 @@ export interface IMonitorService {
|
||||
deleteAllMonitors(args: { teamId: string }): Promise<number>;
|
||||
|
||||
// other
|
||||
sendTestEmail(args: { to: string }): Promise<string>;
|
||||
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
|
||||
importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: any[] }): Promise<{ imported: number; errors: string[] }>;
|
||||
}
|
||||
@@ -174,17 +177,23 @@ export class MonitorService implements IMonitorService {
|
||||
};
|
||||
|
||||
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<any[]> => {
|
||||
const demoData = demoMonitorsData;
|
||||
const monitors = demoData.map((monitor) => ({
|
||||
userId,
|
||||
teamId,
|
||||
name: monitor.name,
|
||||
description: monitor.name,
|
||||
type: "http" as const,
|
||||
url: monitor.url,
|
||||
interval: 60000,
|
||||
}));
|
||||
const demoMonitors = await this.monitorsRepository.createMonitors(monitors as unknown as Monitor[]);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const demoMonitorsPath = path.resolve(__dirname, "../../utils/demoMonitors.json");
|
||||
|
||||
const demoData = JSON.parse(fs.readFileSync(demoMonitorsPath, "utf8"));
|
||||
const monitors: Monitor[] = demoData.map((monitor: Monitor) => {
|
||||
return {
|
||||
userId,
|
||||
teamId,
|
||||
name: monitor.name,
|
||||
description: monitor.name,
|
||||
type: "http",
|
||||
url: monitor.url,
|
||||
interval: 60000,
|
||||
};
|
||||
});
|
||||
const demoMonitors = await this.monitorsRepository.createMonitors(monitors);
|
||||
|
||||
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
|
||||
return demoMonitors;
|
||||
@@ -429,7 +438,7 @@ export class MonitorService implements IMonitorService {
|
||||
return groups;
|
||||
};
|
||||
|
||||
editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Partial<Monitor> }) => {
|
||||
editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => {
|
||||
const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body);
|
||||
await this.jobQueue.updateJob(editedMonitor);
|
||||
return editedMonitor;
|
||||
@@ -508,6 +517,20 @@ export class MonitorService implements IMonitorService {
|
||||
return deletedCount;
|
||||
};
|
||||
|
||||
sendTestEmail = async ({ to }: { to: string }): Promise<string> => {
|
||||
const subject = "Test email from Checkmate";
|
||||
const context = { testName: "Monitoring System" };
|
||||
|
||||
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
||||
const messageId = await this.emailService.sendEmail(to, subject, html);
|
||||
|
||||
if (!messageId) {
|
||||
throw new AppError({ message: "Failed to send test email.", service: SERVICE_NAME, method: "sendTestEmail", status: 500 });
|
||||
}
|
||||
|
||||
return messageId;
|
||||
};
|
||||
|
||||
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<any[]> => {
|
||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
|
||||
|
||||
|
||||
@@ -12,31 +12,24 @@ import bcrypt from "bcryptjs";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||
import { IEmailService } from "@/service/infrastructure/emailService.js";
|
||||
import { EnvConfig, ISettingsService } from "@/service/system/settingsService.js";
|
||||
import { ISettingsService } from "@/service/system/settingsService.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import crypto from "crypto";
|
||||
type CryptoType = typeof crypto;
|
||||
type JwtType = typeof jwt;
|
||||
const SERVICE_NAME = "userService";
|
||||
|
||||
export interface IUserService {
|
||||
issueToken(payload: Partial<User>, appSettings: EnvConfig): string;
|
||||
registerUser(user: Partial<User>, inviteToken: string, file: Express.Multer.File | null): Promise<{ user: User; token: string }>;
|
||||
createUser(userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null): Promise<User>;
|
||||
issueToken(payload: Partial<User>, appSettings: { jwtTTL?: string; jwtSecret?: string }): string;
|
||||
registerUser(user: Partial<User>, inviteToken: string, file: unknown): Promise<{ user: User; token: string }>;
|
||||
createUser(userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: unknown): Promise<User>;
|
||||
loginUser(email: string, password: string): Promise<{ user: User; token: string }>;
|
||||
editUser(
|
||||
updates: Partial<User & { newPassword?: string }>,
|
||||
file: Express.Multer.File | null,
|
||||
currentUserId: string,
|
||||
currentUserEmail: string
|
||||
): Promise<User>;
|
||||
editUser(updates: Partial<User & { newPassword?: string }>, file: unknown, currentUser: User): Promise<User>;
|
||||
checkSuperadminExists(): Promise<boolean>;
|
||||
requestRecovery(email: string): Promise<string | false | undefined>;
|
||||
validateRecovery(recoveryToken: string): Promise<void>;
|
||||
resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>;
|
||||
deleteUser(params: { userId: string; teamId: string; roles: UserRole[] }): Promise<void>;
|
||||
deleteUserById(params: { actorId: string; actorTeamId: string; actorRoles: UserRole[]; targetUserId: string }): Promise<void>;
|
||||
deleteUser(user: User): Promise<void>;
|
||||
deleteUserById(actor: User, targetUserId: string): Promise<void>;
|
||||
getAllUsers(): Promise<User[]>;
|
||||
getUserById(roles: UserRole[], userId: string): Promise<User>;
|
||||
editUserById(userId: string, patch: Partial<User>): Promise<void>;
|
||||
@@ -56,7 +49,7 @@ export class UserService implements IUserService {
|
||||
private logger: ILogger;
|
||||
private jwt: JwtType;
|
||||
private jobQueue: ISuperSimpleQueue;
|
||||
private crypto: CryptoType;
|
||||
private crypto: any;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
private usersRepository: IUsersRepository;
|
||||
private invitesRepository: IInvitesRepository;
|
||||
@@ -78,7 +71,7 @@ export class UserService implements IUserService {
|
||||
settingsRepository,
|
||||
teamsRepository,
|
||||
}: {
|
||||
crypto: CryptoType;
|
||||
crypto: any;
|
||||
emailService: IEmailService;
|
||||
settingsService: ISettingsService;
|
||||
logger: ILogger;
|
||||
@@ -109,14 +102,14 @@ export class UserService implements IUserService {
|
||||
return UserService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
issueToken = (payload: Partial<User>, appSettings: EnvConfig) => {
|
||||
issueToken = (payload: any, appSettings: any) => {
|
||||
const tokenTTL = appSettings?.jwtTTL ?? "2h";
|
||||
const tokenSecret = appSettings?.jwtSecret;
|
||||
const payloadData = payload;
|
||||
return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
|
||||
};
|
||||
|
||||
registerUser = async (user: Partial<User>, inviteToken: string, file: Express.Multer.File | null) => {
|
||||
registerUser = async (user: Partial<User>, inviteToken: string, file: any) => {
|
||||
// Create a new user
|
||||
// If superAdmin exists, a token should be attached to all further register requests
|
||||
const superAdminExists = await this.usersRepository.findSuperAdmin();
|
||||
@@ -186,7 +179,7 @@ export class UserService implements IUserService {
|
||||
return { user: newUser, token };
|
||||
};
|
||||
|
||||
createUser = async (userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null) => {
|
||||
createUser = async (userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: any) => {
|
||||
// Validate that the creator can assign the requested roles
|
||||
const targetRoles = userData.role ?? [];
|
||||
for (const targetRole of targetRoles) {
|
||||
@@ -246,19 +239,14 @@ export class UserService implements IUserService {
|
||||
return { user: userWithoutPassword, token };
|
||||
};
|
||||
|
||||
editUser = async (
|
||||
updates: Partial<User & { newPassword?: string }>,
|
||||
file: Express.Multer.File | null,
|
||||
currentUserId: string,
|
||||
currentUserEmail: string
|
||||
) => {
|
||||
editUser = async (updates: Partial<User & { newPassword?: string }>, file: any, currentUser: any) => {
|
||||
// Change Password check
|
||||
if (updates?.password && updates?.newPassword) {
|
||||
// Get user's email
|
||||
// Add user email to body for DB operation
|
||||
updates.email = currentUserEmail;
|
||||
updates.email = currentUser.email;
|
||||
// Get user
|
||||
const user = await this.usersRepository.findByEmail(currentUserEmail);
|
||||
const user = await this.usersRepository.findByEmail(currentUser.email);
|
||||
// Compare passwords
|
||||
const match = await bcrypt.compare(updates?.password, user.password);
|
||||
// If not a match, throw a 403
|
||||
@@ -271,7 +259,7 @@ export class UserService implements IUserService {
|
||||
delete updates.newPassword;
|
||||
}
|
||||
|
||||
return await this.usersRepository.updateById(currentUserId, updates, file);
|
||||
return await this.usersRepository.updateById(currentUser.id, updates, file);
|
||||
};
|
||||
|
||||
checkSuperadminExists = async () => {
|
||||
@@ -285,8 +273,8 @@ export class UserService implements IUserService {
|
||||
await this.recoveryTokensRepository.deleteManyByEmail(email);
|
||||
const recoveryToken = await this.recoveryTokensRepository.create(email);
|
||||
const name = user.firstName;
|
||||
const settings = this.settingsService.getSettings();
|
||||
const url = `${settings.clientHost!}/set-new-password/${recoveryToken.token}`;
|
||||
const { clientHost } = this.settingsService.getSettings();
|
||||
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
|
||||
|
||||
const html = await this.emailService.buildEmail("passwordResetTemplate", {
|
||||
name,
|
||||
@@ -331,7 +319,24 @@ export class UserService implements IUserService {
|
||||
return { user: existingUser, token };
|
||||
};
|
||||
|
||||
deleteUser = async ({ userId, teamId, roles }: { userId: string; teamId: string; roles: UserRole[] }) => {
|
||||
deleteUser = async (user: User) => {
|
||||
const email = user?.email;
|
||||
if (!email) {
|
||||
throw new AppError({ message: "No email in request", service: SERVICE_NAME, method: "deleteUser", status: 400 });
|
||||
}
|
||||
|
||||
const teamId = user?.teamId;
|
||||
const userId = user?.id;
|
||||
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteUser", status: 400 });
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
throw new AppError({ message: "No user ID in request", service: SERVICE_NAME, method: "deleteUser", status: 400 });
|
||||
}
|
||||
|
||||
const roles = user?.role;
|
||||
if (roles.includes("demo")) {
|
||||
throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUser", status: 400 });
|
||||
}
|
||||
@@ -344,7 +349,7 @@ export class UserService implements IUserService {
|
||||
if (res && res.length > 0) {
|
||||
await Promise.all(
|
||||
res.map(async (monitor) => {
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.jobQueue.deleteJob(monitor.id);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -353,24 +358,14 @@ export class UserService implements IUserService {
|
||||
await this.usersRepository.deleteById(userId);
|
||||
};
|
||||
|
||||
deleteUserById = async ({
|
||||
actorId,
|
||||
actorRoles,
|
||||
actorTeamId,
|
||||
targetUserId,
|
||||
}: {
|
||||
actorId: string;
|
||||
actorTeamId: string;
|
||||
actorRoles: UserRole[];
|
||||
targetUserId: string;
|
||||
}) => {
|
||||
if (actorId === targetUserId) {
|
||||
deleteUserById = async (actor: User, targetUserId: string) => {
|
||||
if (actor.id === targetUserId) {
|
||||
throw new AppError({ message: "Cannot delete your own account from here", service: SERVICE_NAME, method: "deleteUserById", status: 400 });
|
||||
}
|
||||
|
||||
const targetUser = await this.usersRepository.findById(targetUserId);
|
||||
|
||||
if (targetUser.teamId !== actorTeamId) {
|
||||
if (targetUser.teamId !== actor.teamId) {
|
||||
throw new AppError({ message: "User is not on your team", service: SERVICE_NAME, method: "deleteUserById", status: 403 });
|
||||
}
|
||||
|
||||
@@ -378,6 +373,7 @@ export class UserService implements IUserService {
|
||||
throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUserById", status: 400 });
|
||||
}
|
||||
|
||||
const actorRoles = actor.role;
|
||||
const targetRoles = targetUser.role;
|
||||
|
||||
// Check actor can manage all of target's roles
|
||||
@@ -396,7 +392,7 @@ export class UserService implements IUserService {
|
||||
await this.usersRepository.deleteById(targetUserId);
|
||||
|
||||
this.logger.info({
|
||||
message: `User ${targetUserId} deleted by ${actorId}`,
|
||||
message: `User ${targetUserId} deleted by ${actor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteUserById",
|
||||
});
|
||||
@@ -406,7 +402,7 @@ export class UserService implements IUserService {
|
||||
return await this.usersRepository.findAll();
|
||||
};
|
||||
|
||||
getUserById = async (roles: UserRole[], userId: string) => {
|
||||
getUserById = async (roles: any, userId: any) => {
|
||||
if (!roles.includes("superadmin") && !roles.includes("admin")) {
|
||||
throw new AppError({ message: "Insufficient permissions", service: SERVICE_NAME, status: 403 });
|
||||
}
|
||||
@@ -415,11 +411,11 @@ export class UserService implements IUserService {
|
||||
return user;
|
||||
};
|
||||
|
||||
editUserById = async (userId: string, patch: Partial<User>) => {
|
||||
editUserById = async (userId: any, patch: Partial<User>) => {
|
||||
await this.usersRepository.updateById(userId, patch, null);
|
||||
};
|
||||
|
||||
setPasswordByUserId = async (userId: string, password: string) => {
|
||||
setPasswordByUserId = async (userId: any, password: string) => {
|
||||
const hashedPassword = this.hashPassword(password);
|
||||
const updatedUser = await this.usersRepository.updateById(userId, { password: hashedPassword }, null);
|
||||
return updatedUser;
|
||||
|
||||
@@ -27,7 +27,6 @@ export * from "@/service/infrastructure/notificationProviders/INotificationProvi
|
||||
export * from "@/service/infrastructure/notificationProviders/matrix.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/pagerduty.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/slack.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/teams.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
||||
|
||||
// System services
|
||||
|
||||
@@ -2,7 +2,7 @@ import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import Scheduler from "super-simple-scheduler";
|
||||
import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
import { Monitor, MonitorType, supportsGeoCheck } from "@/types/monitor.js";
|
||||
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
|
||||
type QueueJobFailure = {
|
||||
@@ -42,11 +42,11 @@ type QueueJobSummary = {
|
||||
export interface ISuperSimpleQueue {
|
||||
readonly serviceName: string;
|
||||
init(): Promise<boolean>;
|
||||
addJob(monitorId: string, monitor: Monitor): Promise<void>;
|
||||
deleteJob(monitor: Monitor): Promise<void>;
|
||||
pauseJob(monitor: Monitor): Promise<void>;
|
||||
resumeJob(monitor: Monitor): Promise<void>;
|
||||
updateJob(monitor: Monitor): Promise<void>;
|
||||
addJob(monitorId: string, monitor: any): Promise<void>;
|
||||
deleteJob(monitor: any): Promise<void>;
|
||||
pauseJob(monitor: any): Promise<void>;
|
||||
resumeJob(monitor: any): Promise<void>;
|
||||
updateJob(monitor: any): Promise<void>;
|
||||
shutdown(): Promise<void>;
|
||||
getMetrics(): Promise<QueueMetrics>;
|
||||
getJobs(): Promise<QueueJobSummary[]>;
|
||||
@@ -89,10 +89,9 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
try {
|
||||
this.scheduler.start();
|
||||
|
||||
this.scheduler.addTemplate("monitor-job", this.helper.getHeartbeatJob());
|
||||
this.scheduler.addTemplate("geo-check-job", this.helper.getHeartbeatGeoJob());
|
||||
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
||||
this.scheduler.addTemplate("geo-check-job", this.helper.getGeoCheckJob());
|
||||
this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob());
|
||||
this.scheduler.addTemplate("cleanup-retention-job", this.helper.getCleanupRetentionJob());
|
||||
const monitors = await this.monitorsRepository.findAll();
|
||||
if (!monitors) {
|
||||
return true;
|
||||
@@ -105,20 +104,20 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
}
|
||||
|
||||
this.scheduler.addJob({ id: "cleanup-orphaned", template: "cleanup-orphaned", active: true });
|
||||
this.scheduler.addJob({ id: "cleanup-retention", template: "cleanup-retention-job", active: true, repeat: 24 * 60 * 60 * 1000 });
|
||||
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: `Failed to initialize SuperSimpleQueue: ${error instanceof Error ? error.message : String(error)}`,
|
||||
message: "Failed to initialize SuperSimpleQueue",
|
||||
service: SERVICE_NAME,
|
||||
method: "init",
|
||||
details: error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
addJob = async (monitorId: string, monitor: Monitor) => {
|
||||
addJob = async (monitorId: string, monitor: any) => {
|
||||
this.scheduler.addJob({
|
||||
id: monitorId,
|
||||
template: "monitor-job",
|
||||
@@ -144,12 +143,12 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
}
|
||||
};
|
||||
|
||||
deleteJob = async (monitor: Monitor) => {
|
||||
deleteJob = async (monitor: any) => {
|
||||
this.scheduler.removeJob(monitor.id);
|
||||
this.scheduler.removeJob(`${monitor.id}-geo`);
|
||||
};
|
||||
|
||||
pauseJob = async (monitor: Monitor) => {
|
||||
pauseJob = async (monitor: any) => {
|
||||
const result = await this.scheduler.pauseJob(monitor.id);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to pause monitor");
|
||||
@@ -162,7 +161,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
});
|
||||
};
|
||||
|
||||
resumeJob = async (monitor: Monitor) => {
|
||||
resumeJob = async (monitor: any) => {
|
||||
const result = await this.scheduler.resumeJob(monitor.id);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
@@ -177,7 +176,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
});
|
||||
};
|
||||
|
||||
updateJob = async (monitor: Monitor) => {
|
||||
updateJob = async (monitor: any) => {
|
||||
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
||||
|
||||
// Handle geo check job lifecycle
|
||||
@@ -245,8 +244,8 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
failingJobs: 0,
|
||||
jobsWithFailures: [] as Array<{
|
||||
monitorId: string | number;
|
||||
monitorUrl: string;
|
||||
monitorType: MonitorType;
|
||||
monitorUrl: any;
|
||||
monitorType: any;
|
||||
failedAt: number | null;
|
||||
failCount: number;
|
||||
failReason: string | null;
|
||||
|
||||
@@ -2,16 +2,8 @@ const SERVICE_NAME = "JobQueueHelper";
|
||||
import type { Monitor } from "@/types/monitor.js";
|
||||
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import {
|
||||
ICheckService,
|
||||
INetworkService,
|
||||
INotificationsService,
|
||||
ISettingsService,
|
||||
IStatusService,
|
||||
IncidentService,
|
||||
type IGeoChecksService,
|
||||
} from "@/service/index.js";
|
||||
import { CHECK_TTL_SENTINEL, type MaintenanceWindow, type StatusChangeResult } from "@/types/index.js";
|
||||
import { INetworkService, INotificationsService, IStatusService, IncidentService, type IGeoChecksService } from "@/service/index.js";
|
||||
import type { StatusChangeResult } from "@/types/index.js";
|
||||
import {
|
||||
IMaintenanceWindowsRepository,
|
||||
IMonitorsRepository,
|
||||
@@ -26,10 +18,9 @@ import { IBufferService } from "@/service/index.js";
|
||||
|
||||
export interface ISuperSimpleQueueHelper {
|
||||
readonly serviceName: string;
|
||||
getHeartbeatJob(): (monitor: Monitor) => Promise<void>;
|
||||
getHeartbeatGeoJob(): (monitor: Monitor) => Promise<void>;
|
||||
getMonitorJob(): (monitor: Monitor) => Promise<void>;
|
||||
getCleanupOrphanedJob(): () => Promise<void>;
|
||||
getCleanupRetentionJob(): () => Promise<void>;
|
||||
getGeoCheckJob(): (monitor: Monitor) => Promise<void>;
|
||||
isInMaintenanceWindow(monitorId: string, teamId: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -50,12 +41,11 @@ export interface MonitorActionDecision {
|
||||
export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private logger: ILogger;
|
||||
private logger: any;
|
||||
private networkService: INetworkService;
|
||||
private statusService: IStatusService;
|
||||
private notificationsService: INotificationsService;
|
||||
private checkService: ICheckService;
|
||||
private settingsService: ISettingsService;
|
||||
private checkService: any;
|
||||
private buffer: IBufferService;
|
||||
private incidentService: IncidentService;
|
||||
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
@@ -72,8 +62,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
networkService: INetworkService,
|
||||
statusService: IStatusService,
|
||||
notificationsService: INotificationsService,
|
||||
checkService: ICheckService,
|
||||
settingsService: ISettingsService,
|
||||
checkService: any,
|
||||
buffer: IBufferService,
|
||||
incidentService: IncidentService,
|
||||
maintenanceWindowsRepository: IMaintenanceWindowsRepository,
|
||||
@@ -89,7 +78,6 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
this.networkService = networkService;
|
||||
this.statusService = statusService;
|
||||
this.checkService = checkService;
|
||||
this.settingsService = settingsService;
|
||||
this.buffer = buffer;
|
||||
this.notificationsService = notificationsService;
|
||||
this.incidentService = incidentService;
|
||||
@@ -107,7 +95,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
return SuperSimpleQueueHelper.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getHeartbeatJob = () => {
|
||||
getMonitorJob = () => {
|
||||
return async (monitor: Monitor) => {
|
||||
try {
|
||||
const monitorId = monitor.id;
|
||||
@@ -138,7 +126,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
}
|
||||
|
||||
// Step 3. Build check
|
||||
const check = this.checkService.buildCheck(status);
|
||||
const check = await this.checkService.buildCheck(status);
|
||||
if (!check) {
|
||||
this.logger.warn({
|
||||
message: `No check could be built for monitor ${monitorId}`,
|
||||
@@ -158,31 +146,33 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
|
||||
// Step 6. Handle notifications (best effort, continue even in event of failure, don't wait)
|
||||
if (decision.shouldSendNotification) {
|
||||
this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: unknown) => {
|
||||
this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: any) => {
|
||||
this.logger.error({
|
||||
message: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
details: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Step 7. Handle incidents (best effort, don't wait)
|
||||
this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: unknown) => {
|
||||
this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: any) => {
|
||||
this.logger.warn({
|
||||
message: `Error handling incident for job ${monitor.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
details: `Error handling incident for job ${monitor.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
this.logger.warn({
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
message: error.message,
|
||||
service: error.service || SERVICE_NAME,
|
||||
method: error.method || "getMonitorJob",
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -268,19 +258,19 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupOrphanedJob",
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
this.logger.warn({
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupOrphanedJob",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
getHeartbeatGeoJob = () => {
|
||||
getGeoCheckJob = () => {
|
||||
return async (monitor: Monitor) => {
|
||||
try {
|
||||
const monitorId = monitor.id;
|
||||
@@ -288,7 +278,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
|
||||
// Step 1: Validate monitor eligibility
|
||||
if (!monitorId) {
|
||||
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getHeartbeatGeoJob" });
|
||||
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" });
|
||||
}
|
||||
|
||||
if (!monitor.geoCheckEnabled) {
|
||||
@@ -298,7 +288,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
this.logger.debug({
|
||||
message: `Monitor ${monitorId} type does not support geo checks, skipping`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getHeartbeatGeoJob",
|
||||
method: "getGeoCheckJob",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -307,7 +297,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
this.logger.warn({
|
||||
message: `No geo check locations configured for monitor ${monitorId}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getHeartbeatGeoJob",
|
||||
method: "getGeoCheckJob",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -318,7 +308,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
this.logger.debug({
|
||||
message: `Monitor ${monitorId} is in maintenance window, skipping geo check`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getHeartbeatGeoJob",
|
||||
method: "getGeoCheckJob",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -329,7 +319,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
this.logger.warn({
|
||||
message: `No geo check could be built for monitor ${monitorId}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getHeartbeatGeoJob",
|
||||
method: "getGeoCheckJob",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -340,14 +330,14 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
this.logger.debug({
|
||||
message: `Geo check job executed for monitor ${monitorId}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getHeartbeatGeoJob",
|
||||
method: "getGeoCheckJob",
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getHeartbeatGeoJob",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
method: "getGeoCheckJob",
|
||||
stack: error.stack,
|
||||
});
|
||||
// Don't throw - geo check failures shouldn't crash the job scheduler
|
||||
}
|
||||
@@ -357,7 +347,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
async isInMaintenanceWindow(monitorId: string, teamId: string) {
|
||||
const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId);
|
||||
// Check for active maintenance window:
|
||||
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: boolean, window: MaintenanceWindow) => {
|
||||
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => {
|
||||
if (window.active) {
|
||||
const start = new Date(window.start);
|
||||
const end = new Date(window.end);
|
||||
@@ -384,40 +374,6 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
return maintenanceWindowIsActive;
|
||||
}
|
||||
|
||||
getCleanupRetentionJob = () => {
|
||||
return async () => {
|
||||
try {
|
||||
const settings = await this.settingsService.getDBSettings();
|
||||
|
||||
const checkTTL = settings.checkTTL; // Check TTL is in DAYS, not MS
|
||||
|
||||
if (checkTTL === CHECK_TTL_SENTINEL) {
|
||||
this.logger.info({
|
||||
message: `Check TTL is set to unlimited, skipping cleanup`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupRetentionJob",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const checkTTLInMs = checkTTL * 24 * 60 * 60 * 1000;
|
||||
const cutoffDate = new Date(Date.now() - checkTTLInMs);
|
||||
const deleteCount = await this.checkService.deleteOlderThan(cutoffDate);
|
||||
this.logger.info({
|
||||
message: `Deleted ${deleteCount} checks older than ${cutoffDate.toISOString()}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupRetentionJob",
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
this.logger.error({
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupRetentionJob",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
private evaluateMonitorAction(statusChangeResult: StatusChangeResult): MonitorActionDecision {
|
||||
const { monitor, statusChanged, prevStatus } = statusChangeResult;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
private got: Got,
|
||||
private advancedMatcher: AdvancedMatcher
|
||||
) {
|
||||
const cacheable = new CacheableLookup({ maxTtl: 300, errorTtl: 30 });
|
||||
const cacheable = new CacheableLookup();
|
||||
this.got = got.extend({
|
||||
dnsCache: cacheable,
|
||||
timeout: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Monitor, MonitorStatusResponse, MonitorPayloadMap, MonitorType } from "@/types/index.js";
|
||||
import type { Monitor, MonitorStatusResponse } from "@/types/index.js";
|
||||
import type { AxiosStatic } from "axios";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
|
||||
@@ -8,7 +8,7 @@ const SERVICE_NAME = "NetworkService";
|
||||
|
||||
export interface INetworkService {
|
||||
readonly serviceName: string;
|
||||
requestStatus<T extends MonitorType>(monitor: Monitor & { type: T }): Promise<MonitorStatusResponse<MonitorPayloadMap[T]>>;
|
||||
requestStatus(monitor: Monitor): Promise<MonitorStatusResponse<unknown>>;
|
||||
requestWebhook(
|
||||
type: string,
|
||||
url: string,
|
||||
@@ -43,12 +43,12 @@ export class NetworkService implements INetworkService {
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
async requestStatus<T extends MonitorType>(monitor: Monitor & { type: T }): Promise<MonitorStatusResponse<MonitorPayloadMap[T]>> {
|
||||
async requestStatus(monitor: Monitor) {
|
||||
const provider = this.providers.find((p) => p.supports(monitor.type));
|
||||
if (!provider) {
|
||||
return this.handleUnsupportedType(monitor.type) as Promise<MonitorStatusResponse<MonitorPayloadMap[T]>>;
|
||||
return this.handleUnsupportedType(monitor.type);
|
||||
}
|
||||
return provider.handle(monitor) as Promise<MonitorStatusResponse<MonitorPayloadMap[T]>>;
|
||||
return provider.handle(monitor);
|
||||
}
|
||||
|
||||
private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
|
||||
|
||||
@@ -3,5 +3,5 @@ import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||
|
||||
export interface INotificationProvider {
|
||||
sendMessage: (notification: Notification, message: NotificationMessage) => Promise<boolean>;
|
||||
sendTestAlert(notification: Partial<Notification>): Promise<boolean>;
|
||||
sendTestAlert(notification: Notification): Promise<boolean>;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export class DiscordProvider implements INotificationProvider {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
sendTestAlert = async (notification: Partial<Notification>) => {
|
||||
sendTestAlert = async (notification: Notification) => {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export class EmailProvider implements INotificationProvider {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
||||
const subject = "Test notification";
|
||||
const html = await buildTestEmail(this.emailService);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export class MatrixProvider implements INotificationProvider {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
sendTestAlert = async (notification: Partial<Notification>) => {
|
||||
sendTestAlert = async (notification: Notification) => {
|
||||
const { homeserverUrl, accessToken, roomId } = notification;
|
||||
if (!homeserverUrl || !accessToken || !roomId) {
|
||||
return false;
|
||||
|
||||
@@ -14,7 +14,7 @@ export class PagerDutyProvider implements INotificationProvider {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
||||
try {
|
||||
await got.post("https://events.pagerduty.com/v2/enqueue", {
|
||||
json: {
|
||||
|
||||
@@ -13,7 +13,7 @@ export class SlackProvider implements INotificationProvider {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
const SERVICE_NAME = "TeamsProvider";
|
||||
import type { Notification } from "@/types/index.js";
|
||||
import { INotificationProvider } from "@/service/index.js";
|
||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import type { ILogger } from "@/utils/logger.js";
|
||||
import got, { HTTPError } from "got";
|
||||
|
||||
// Types for Adaptive Card elements
|
||||
type TextBlock = {
|
||||
type: "TextBlock";
|
||||
text: string;
|
||||
weight?: "Bolder" | "Normal" | "Lighter";
|
||||
size?: "Small" | "Medium" | "Large" | "ExtraLarge";
|
||||
color?: "Default" | "Dark" | "Light" | "Accent" | "Good" | "Warning" | "Attention";
|
||||
wrap?: boolean;
|
||||
spacing?: "None" | "Small" | "Medium" | "Large" | "ExtraLarge" | "Auto";
|
||||
isSubtle?: boolean;
|
||||
};
|
||||
|
||||
type ColumnSet = {
|
||||
type: "ColumnSet";
|
||||
separator?: boolean;
|
||||
spacing?: "None" | "Small" | "Medium" | "Large" | "ExtraLarge" | "Auto";
|
||||
columns: unknown[];
|
||||
};
|
||||
|
||||
type Fact = {
|
||||
title: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type FactSet = {
|
||||
type: "FactSet";
|
||||
facts: Fact[];
|
||||
};
|
||||
|
||||
type Action = {
|
||||
type: string;
|
||||
title: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
type AdaptiveCard = {
|
||||
type: "AdaptiveCard";
|
||||
$schema: "http://adaptivecards.io/schemas/adaptive-card.json";
|
||||
version: "1.4";
|
||||
body: (TextBlock | ColumnSet | FactSet)[];
|
||||
actions?: Action[];
|
||||
};
|
||||
|
||||
type TeamsMessage = {
|
||||
type: "message";
|
||||
attachments: Array<{
|
||||
contentType: "application/vnd.microsoft.card.adaptive";
|
||||
contentUrl: null;
|
||||
content: AdaptiveCard;
|
||||
}>;
|
||||
};
|
||||
|
||||
export class TeamsProvider implements INotificationProvider {
|
||||
private logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = this.wrapAdaptiveCard({
|
||||
type: "AdaptiveCard",
|
||||
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
version: "1.4",
|
||||
body: [
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: getTestMessage(),
|
||||
weight: "Bolder",
|
||||
size: "Medium",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await got.post(notification.address, {
|
||||
json: payload,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as HTTPError;
|
||||
this.logger.warn({
|
||||
message: "Teams test alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendTestAlert",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const payload = this.wrapAdaptiveCard(this.buildAdaptiveCard(message));
|
||||
|
||||
try {
|
||||
await got.post(notification.address, {
|
||||
json: payload,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
this.logger.info({
|
||||
message: "Teams notification sent via sendMessage",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as HTTPError;
|
||||
this.logger.warn({
|
||||
message: "Teams alert failed via sendMessage",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap an Adaptive Card in the Teams webhook envelope format
|
||||
*/
|
||||
private wrapAdaptiveCard(card: AdaptiveCard): TeamsMessage {
|
||||
return {
|
||||
type: "message",
|
||||
attachments: [
|
||||
{
|
||||
contentType: "application/vnd.microsoft.card.adaptive",
|
||||
contentUrl: null,
|
||||
content: card,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an Adaptive Card from NotificationMessage
|
||||
*/
|
||||
private buildAdaptiveCard(message: NotificationMessage): AdaptiveCard {
|
||||
const colorMap: Record<string, string> = {
|
||||
critical: "attention",
|
||||
warning: "warning",
|
||||
success: "good",
|
||||
info: "accent",
|
||||
};
|
||||
const color = (colorMap[message.severity] || "default") as "Default" | "Dark" | "Light" | "Accent" | "Good" | "Warning" | "Attention" | undefined;
|
||||
|
||||
const body: (TextBlock | ColumnSet | FactSet)[] = [];
|
||||
|
||||
// Header with colored status indicator
|
||||
body.push({
|
||||
type: "TextBlock",
|
||||
text: message.content.title,
|
||||
weight: "Bolder",
|
||||
size: "Large",
|
||||
color,
|
||||
wrap: true,
|
||||
});
|
||||
|
||||
// Summary
|
||||
body.push({
|
||||
type: "TextBlock",
|
||||
text: message.content.summary,
|
||||
wrap: true,
|
||||
spacing: "Small",
|
||||
});
|
||||
|
||||
// Separator
|
||||
body.push({
|
||||
type: "ColumnSet",
|
||||
separator: true,
|
||||
spacing: "Medium",
|
||||
columns: [],
|
||||
});
|
||||
|
||||
// Monitor details as a FactSet
|
||||
body.push({
|
||||
type: "FactSet",
|
||||
facts: [
|
||||
{ title: "Name", value: message.monitor.name },
|
||||
{ title: "Type", value: message.monitor.type },
|
||||
{ title: "Status", value: message.monitor.status },
|
||||
{ title: "URL", value: message.monitor.url },
|
||||
],
|
||||
});
|
||||
|
||||
// Threshold breaches
|
||||
if (message.content.thresholds && message.content.thresholds.length > 0) {
|
||||
body.push({
|
||||
type: "TextBlock",
|
||||
text: "**Threshold Breaches**",
|
||||
weight: "Bolder",
|
||||
spacing: "Medium",
|
||||
wrap: true,
|
||||
});
|
||||
|
||||
for (const breach of message.content.thresholds) {
|
||||
body.push({
|
||||
type: "TextBlock",
|
||||
text: `• **${breach.metric.toUpperCase()}**: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`,
|
||||
wrap: true,
|
||||
spacing: "Small",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Additional details
|
||||
if (message.content.details && message.content.details.length > 0) {
|
||||
body.push({
|
||||
type: "TextBlock",
|
||||
text: "**Additional Information**",
|
||||
weight: "Bolder",
|
||||
spacing: "Medium",
|
||||
wrap: true,
|
||||
});
|
||||
|
||||
for (const detail of message.content.details) {
|
||||
body.push({
|
||||
type: "TextBlock",
|
||||
text: `• ${detail}`,
|
||||
wrap: true,
|
||||
spacing: "Small",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamp footer
|
||||
body.push({
|
||||
type: "TextBlock",
|
||||
text: `Checkmate | ${new Date(message.content.timestamp).toUTCString()}`,
|
||||
size: "Small",
|
||||
isSubtle: true,
|
||||
spacing: "Medium",
|
||||
wrap: true,
|
||||
});
|
||||
|
||||
// Actions (incident link)
|
||||
const actions: Action[] = [];
|
||||
if (message.content.incident) {
|
||||
actions.push({
|
||||
type: "Action.OpenUrl",
|
||||
title: "View Incident",
|
||||
url: `${message.clientHost}/incidents/${message.content.incident.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: "AdaptiveCard",
|
||||
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
version: "1.4",
|
||||
body,
|
||||
...(actions.length > 0 ? { actions } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export class WebhookProvider implements INotificationProvider {
|
||||
};
|
||||
}
|
||||
|
||||
sendTestAlert = async (notification: Partial<Notification>) => {
|
||||
sendTestAlert = async (notification: Notification) => {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ import { ILogger } from "@/utils/logger.js";
|
||||
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
||||
|
||||
export interface INotificationsService {
|
||||
createNotification: (notificationData: Partial<Notification>, userId: string, teamId: string) => Promise<Notification>;
|
||||
createNotification: (notificationData: Partial<Notification>) => Promise<Notification>;
|
||||
findById: (id: string, teamId: string) => Promise<Notification>;
|
||||
findNotificationsByTeamId: (teamId: string) => Promise<Notification[]>;
|
||||
updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>;
|
||||
deleteById: (id: string, teamId: string) => Promise<Notification>;
|
||||
handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise<boolean>;
|
||||
|
||||
sendTestNotification: (notification: Partial<Notification>) => Promise<boolean>;
|
||||
sendTestNotification: (notification: Notification) => Promise<boolean>;
|
||||
testAllNotifications: (notificationIds: string[]) => Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ export class NotificationsService implements INotificationsService {
|
||||
private discordProvider: INotificationProvider;
|
||||
private pagerDutyProvider: INotificationProvider;
|
||||
private matrixProvider: INotificationProvider;
|
||||
private teamsProvider: INotificationProvider;
|
||||
private logger: ILogger;
|
||||
private settingsService: ISettingsService;
|
||||
private notificationMessageBuilder: INotificationMessageBuilder;
|
||||
@@ -46,7 +45,6 @@ export class NotificationsService implements INotificationsService {
|
||||
discordProvider: INotificationProvider,
|
||||
pagerDutyProvider: INotificationProvider,
|
||||
matrixProvider: INotificationProvider,
|
||||
teamsProvider: INotificationProvider,
|
||||
settingsService: ISettingsService,
|
||||
logger: ILogger,
|
||||
notificationMessageBuilder: INotificationMessageBuilder
|
||||
@@ -59,7 +57,6 @@ export class NotificationsService implements INotificationsService {
|
||||
this.discordProvider = discordProvider;
|
||||
this.pagerDutyProvider = pagerDutyProvider;
|
||||
this.matrixProvider = matrixProvider;
|
||||
this.teamsProvider = teamsProvider;
|
||||
this.settingsService = settingsService;
|
||||
this.logger = logger;
|
||||
this.notificationMessageBuilder = notificationMessageBuilder;
|
||||
@@ -95,8 +92,6 @@ export class NotificationsService implements INotificationsService {
|
||||
return await this.discordProvider.sendMessage!(notification, notificationMessage);
|
||||
case "email":
|
||||
return await this.emailProvider.sendMessage!(notification, notificationMessage);
|
||||
case "teams":
|
||||
return await this.teamsProvider.sendMessage!(notification, notificationMessage);
|
||||
default:
|
||||
this.logger.warn({
|
||||
message: `Unknown notification type: ${notification.type}`,
|
||||
@@ -141,7 +136,7 @@ export class NotificationsService implements INotificationsService {
|
||||
return await this.sendNotifications(monitor, monitorStatusResponse, decision);
|
||||
};
|
||||
|
||||
sendTestNotification = async (notification: Partial<Notification>) => {
|
||||
sendTestNotification = async (notification: Notification) => {
|
||||
switch (notification.type) {
|
||||
case "email":
|
||||
return await this.emailProvider.sendTestAlert(notification);
|
||||
@@ -155,8 +150,6 @@ export class NotificationsService implements INotificationsService {
|
||||
return await this.matrixProvider.sendTestAlert(notification);
|
||||
case "webhook":
|
||||
return await this.webhookProvider.sendTestAlert(notification);
|
||||
case "teams":
|
||||
return await this.teamsProvider.sendTestAlert(notification);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -174,9 +167,7 @@ export class NotificationsService implements INotificationsService {
|
||||
return true;
|
||||
};
|
||||
|
||||
createNotification = async (notificationData: Partial<Notification>, userId: string, teamId: string): Promise<Notification> => {
|
||||
notificationData.userId = userId;
|
||||
notificationData.teamId = teamId;
|
||||
createNotification = async (notificationData: Partial<Notification>): Promise<Notification> => {
|
||||
return await this.notificationsRepository.create(notificationData);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ import { ISettingsRepository } from "@/repositories/index.js";
|
||||
import { Settings } from "@/types/index.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ValidatedEnv } from "@/validation/envValidation.js";
|
||||
import type { StringValue } from "ms";
|
||||
|
||||
const SERVICE_NAME = "SettingsService";
|
||||
|
||||
export type EnvConfig = {
|
||||
jwtSecret: string;
|
||||
jwtTTL: StringValue;
|
||||
jwtTTL: string;
|
||||
nodeEnv: string;
|
||||
logLevel: string;
|
||||
clientHost: string;
|
||||
@@ -30,7 +30,7 @@ export class SettingsService implements ISettingsService {
|
||||
this.settingsRepository = settingsRepository;
|
||||
this.settings = {
|
||||
jwtSecret: env.JWT_SECRET,
|
||||
jwtTTL: env.TOKEN_TTL as StringValue,
|
||||
jwtTTL: env.TOKEN_TTL,
|
||||
nodeEnv: env.NODE_ENV,
|
||||
logLevel: env.LOG_LEVEL,
|
||||
clientHost: env.CLIENT_HOST,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { MonitorType } from "@/types/index.js";
|
||||
import type { Response } from "got";
|
||||
|
||||
export const CHECK_TTL_SENTINEL = 366;
|
||||
|
||||
export type GotTimings = Response["timings"];
|
||||
|
||||
export interface CheckMetadata {
|
||||
@@ -101,6 +98,9 @@ export interface Check {
|
||||
timings?: GotTimings;
|
||||
statusCode: number;
|
||||
message: string;
|
||||
ack: boolean;
|
||||
ackAt?: string | null;
|
||||
expiry: string;
|
||||
cpu?: CheckCpuInfo;
|
||||
memory?: CheckMemoryInfo;
|
||||
disk?: CheckDiskInfo[];
|
||||
@@ -113,6 +113,7 @@ export interface Check {
|
||||
seo?: number;
|
||||
performance?: number;
|
||||
audits?: CheckAudits;
|
||||
__v: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -204,4 +205,4 @@ export type NormalizedUptimeCheck<T extends GroupedCheck = GroupedCheck> = T & {
|
||||
originalAvgResponseTime: number;
|
||||
};
|
||||
|
||||
export type CheckSnapshot = Omit<Check, "metadata" | "updatedAt">;
|
||||
export type CheckSnapshot = Omit<Check, "metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt">;
|
||||
@@ -12,8 +12,7 @@ export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupporte
|
||||
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export const MonitorMatchMethods = ["equal", "include", "regex"] as const;
|
||||
export type MonitorMatchMethod = (typeof MonitorMatchMethods)[number] | "";
|
||||
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
|
||||
|
||||
export interface Monitor {
|
||||
id: string;
|
||||
|
||||
@@ -126,7 +126,7 @@ export interface MonitorPayloadMap {
|
||||
game: GameStatusPayload;
|
||||
grpc: GrpcStatusPayload;
|
||||
websocket: WebSocketStatusPayload;
|
||||
unknown: unknown;
|
||||
default: unknown;
|
||||
}
|
||||
|
||||
export type StatusChangeResult = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as const;
|
||||
export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as const;
|
||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||
|
||||
export interface Notification {
|
||||
|
||||
Vendored
-24
@@ -1,24 +0,0 @@
|
||||
declare module "ssl-checker" {
|
||||
import { Agent } from "https";
|
||||
|
||||
export interface SSLDetails {
|
||||
daysRemaining: number;
|
||||
valid: boolean;
|
||||
validFrom: string;
|
||||
validTo: string;
|
||||
validFor: string[];
|
||||
}
|
||||
|
||||
export interface SSLOptions {
|
||||
method?: "GET" | "HEAD";
|
||||
port?: number;
|
||||
agent?: Agent;
|
||||
rejectUnauthorized?: boolean;
|
||||
validateSubjectAltName?: boolean;
|
||||
}
|
||||
|
||||
function sslChecker(hostname: string, options?: SSLOptions): Promise<SSLDetails>;
|
||||
|
||||
// Change 'export default' to 'export =' to allow it to be callable
|
||||
export = sslChecker;
|
||||
}
|
||||
@@ -12,31 +12,33 @@ export const getChecksParamValidation = z.object({
|
||||
|
||||
export const getChecksQueryValidation = z.object({
|
||||
type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"]).optional(),
|
||||
sortOrder: z.enum(["asc", "desc"]),
|
||||
sortOrder: z.enum(["asc", "desc"]).optional(),
|
||||
limit: z.coerce.number().optional(),
|
||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
|
||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
|
||||
filter: z.enum(["all", "up", "down", "resolve"]).optional(),
|
||||
ack: booleanCoercion.optional(),
|
||||
page: z.coerce.number(),
|
||||
rowsPerPage: z.coerce.number(),
|
||||
page: z.coerce.number().optional(),
|
||||
rowsPerPage: z.coerce.number().optional(),
|
||||
status: booleanCoercion.optional(),
|
||||
continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(),
|
||||
});
|
||||
|
||||
export const getTeamChecksQueryValidation = z.object({
|
||||
sortOrder: z.enum(["asc", "desc"]),
|
||||
sortOrder: z.enum(["asc", "desc"]).optional(),
|
||||
limit: z.coerce.number().optional(),
|
||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
|
||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
|
||||
filter: z.enum(["all", "up", "down", "resolve"]).optional(),
|
||||
ack: booleanCoercion.optional(),
|
||||
page: z.coerce.number(),
|
||||
rowsPerPage: z.coerce.number(),
|
||||
page: z.coerce.number().optional(),
|
||||
rowsPerPage: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
export const deleteChecksParamValidation = z.object({
|
||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||
});
|
||||
|
||||
export const getChecksSummaryByTeamIdQueryValidation = z.object({
|
||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
|
||||
export const deleteChecksByTeamIdParamValidation = z.object({});
|
||||
|
||||
export const updateChecksTTLBodyValidation = z.object({
|
||||
ttl: z.number().min(1, "TTL is required"),
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion } from "./shared.js";
|
||||
|
||||
//****************************************
|
||||
// Incident Validations
|
||||
//****************************************
|
||||
|
||||
export const getIncidentsByTeamQueryValidation = z.object({
|
||||
sortOrder: z.enum(["asc", "desc"]),
|
||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
|
||||
page: z.coerce.number().int().min(0),
|
||||
rowsPerPage: z.coerce.number().int().min(1),
|
||||
status: booleanCoercion.optional(),
|
||||
monitorId: z.string().optional(),
|
||||
resolutionType: z.enum(["manual", "automatic"]).optional(),
|
||||
});
|
||||
|
||||
export const getIncidentSummaryQueryValidation = z.object({
|
||||
limit: z.coerce.number().int().min(1).optional(),
|
||||
});
|
||||
@@ -20,4 +20,3 @@ export * from "./settingsValidation.js";
|
||||
export * from "./statusPageValidation.js";
|
||||
export * from "./notificationValidation.js";
|
||||
export * from "./userValidation.js";
|
||||
export * from "./incidentValidation.js";
|
||||
@@ -1,8 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion } from "./shared.js";
|
||||
|
||||
const dateToString = z.coerce.date().transform((d) => d.toISOString());
|
||||
|
||||
//****************************************
|
||||
// Maintenance Window Validations
|
||||
//****************************************
|
||||
@@ -13,10 +11,10 @@ export const createMaintenanceWindowBodyValidation = z.object({
|
||||
active: z.boolean().optional(),
|
||||
duration: z.number().min(1, "Duration is required"),
|
||||
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]),
|
||||
start: dateToString,
|
||||
end: dateToString,
|
||||
start: z.coerce.date(),
|
||||
end: z.coerce.date(),
|
||||
repeat: z.number().min(0, "Repeat must be a non-negative number"),
|
||||
expiry: dateToString.optional(),
|
||||
expiry: z.coerce.date().optional(),
|
||||
});
|
||||
|
||||
export const getMaintenanceWindowByIdParamValidation = z.object({
|
||||
@@ -47,10 +45,10 @@ export const editMaintenanceByIdWindowBodyValidation = z.object({
|
||||
active: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
repeat: z.number().optional(),
|
||||
start: dateToString.optional(),
|
||||
end: dateToString.optional(),
|
||||
expiry: dateToString.optional(),
|
||||
monitors: z.array(z.string()).optional(),
|
||||
start: z.coerce.date().optional(),
|
||||
end: z.coerce.date().optional(),
|
||||
expiry: z.coerce.date().optional(),
|
||||
monitors: z.array(z.unknown()).optional(),
|
||||
duration: z.number().optional(),
|
||||
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(),
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion } from "./shared.js";
|
||||
import { GeoContinents } from "@/types/geoCheck.js";
|
||||
import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js";
|
||||
|
||||
export const getMonitorByIdParamValidation = z.object({
|
||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||
@@ -14,7 +13,7 @@ export const getMonitorByIdQueryValidation = z.object({
|
||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
|
||||
numToDisplay: z.coerce.number().optional(),
|
||||
normalize: booleanCoercion.optional(),
|
||||
continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(),
|
||||
continent: z.enum(GeoContinents).optional(),
|
||||
});
|
||||
|
||||
export const getMonitorsByTeamIdParamValidation = z.object({});
|
||||
@@ -26,14 +25,14 @@ export const getMonitorsByTeamIdQueryValidation = z.object({
|
||||
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])),
|
||||
])
|
||||
.optional(),
|
||||
filter: z.union([z.string(), z.literal("")]).optional(),
|
||||
filter: z.union([z.string(), z.literal(""), z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const getMonitorsWithChecksQueryValidation = z.object({
|
||||
limit: z.coerce.number().int().min(1).max(100).optional(),
|
||||
page: z.coerce.number().int().min(0).optional(),
|
||||
rowsPerPage: z.coerce.number().int().min(1).max(100).optional(),
|
||||
filter: z.union([z.string(), z.literal("")]).optional(),
|
||||
filter: z.union([z.string(), z.literal(""), z.null()]).optional(),
|
||||
field: z.string().optional(),
|
||||
order: z.enum(["asc", "desc"]).optional(),
|
||||
type: z
|
||||
@@ -52,8 +51,8 @@ export const getCertificateParamValidation = z.object({
|
||||
export const createMonitorBodyValidation = z.object({
|
||||
_id: z.string().optional(),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
description: z.union([z.string(), z.literal("")]).optional(),
|
||||
type: z.enum(MonitorTypes, "Invalid monitor type"),
|
||||
description: z.union([z.string(), z.null(), z.literal("")]).optional(),
|
||||
type: z.string().min(1, "Type is required"),
|
||||
statusWindowSize: z.number().min(1).max(20).default(5),
|
||||
statusWindowThreshold: z.number().min(1).max(100).default(60),
|
||||
url: z.string().min(1, "URL is required"),
|
||||
@@ -70,7 +69,7 @@ export const createMonitorBodyValidation = z.object({
|
||||
secret: z.string().optional(),
|
||||
jsonPath: z.union([z.string(), z.literal("")]).optional(),
|
||||
expectedValue: z.union([z.string(), z.literal("")]).optional(),
|
||||
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
|
||||
matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(),
|
||||
gameId: z.union([z.string(), z.literal("")]).optional(),
|
||||
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
|
||||
selectedDisks: z.array(z.string()).optional(),
|
||||
@@ -82,11 +81,11 @@ export const createMonitorBodyValidation = z.object({
|
||||
|
||||
export const editMonitorBodyValidation = z.object({
|
||||
name: z.string().optional(),
|
||||
type: z.enum(MonitorTypes).optional(),
|
||||
type: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
statusWindowSize: z.number().min(1).max(20).default(5),
|
||||
statusWindowThreshold: z.number().min(1).max(100).default(60),
|
||||
description: z.union([z.string(), z.literal("")]).optional(),
|
||||
description: z.union([z.string(), z.null(), z.literal("")]).optional(),
|
||||
interval: z.number().optional(),
|
||||
notifications: z.array(z.string()).optional(),
|
||||
secret: z.string().optional(),
|
||||
@@ -94,7 +93,7 @@ export const editMonitorBodyValidation = z.object({
|
||||
useAdvancedMatching: z.boolean().optional(),
|
||||
jsonPath: z.union([z.string(), z.literal("")]).optional(),
|
||||
expectedValue: z.union([z.string(), z.literal("")]).optional(),
|
||||
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
|
||||
matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(),
|
||||
port: z.number().min(1).max(65535).optional(),
|
||||
cpuAlertThreshold: z.number().optional(),
|
||||
memoryAlertThreshold: z.number().optional(),
|
||||
@@ -113,62 +112,6 @@ export const pauseMonitorParamValidation = z.object({
|
||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||
});
|
||||
|
||||
export const getUptimeDetailsByIdParamValidation = z.object({
|
||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||
});
|
||||
|
||||
export const getUptimeDetailsByIdQueryValidation = z.object({
|
||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
|
||||
normalize: booleanCoercion.optional(),
|
||||
});
|
||||
|
||||
const importedMonitorSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
teamId: z.string().optional(),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
description: z.union([z.string(), z.literal("")]).optional(),
|
||||
status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).optional(),
|
||||
statusWindow: z.array(z.boolean()).optional(),
|
||||
statusWindowSize: z.number().optional(),
|
||||
statusWindowThreshold: z.number().optional(),
|
||||
type: z.enum(MonitorTypes, "Invalid monitor type"),
|
||||
ignoreTlsErrors: z.boolean().optional(),
|
||||
useAdvancedMatching: z.boolean().optional(),
|
||||
jsonPath: z.union([z.string(), z.literal("")]).optional(),
|
||||
expectedValue: z.union([z.string(), z.literal("")]).optional(),
|
||||
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
|
||||
url: z.string().min(1, "URL is required"),
|
||||
port: z.number().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
interval: z.number().optional(),
|
||||
uptimePercentage: z.number().optional(),
|
||||
notifications: z.array(z.string()).optional(),
|
||||
secret: z.string().optional(),
|
||||
cpuAlertThreshold: z.number().optional(),
|
||||
cpuAlertCounter: z.number().optional(),
|
||||
memoryAlertThreshold: z.number().optional(),
|
||||
memoryAlertCounter: z.number().optional(),
|
||||
diskAlertThreshold: z.number().optional(),
|
||||
diskAlertCounter: z.number().optional(),
|
||||
tempAlertThreshold: z.number().optional(),
|
||||
tempAlertCounter: z.number().optional(),
|
||||
selectedDisks: z.array(z.string()).optional(),
|
||||
gameId: z.union([z.string(), z.literal("")]).optional(),
|
||||
grpcServiceName: z.union([z.string(), z.literal("")]).optional(),
|
||||
group: z.union([z.string(), z.null()]).optional(),
|
||||
geoCheckEnabled: z.boolean().optional(),
|
||||
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
|
||||
geoCheckInterval: z.number().optional(),
|
||||
recentChecks: z.array(z.unknown()).optional(),
|
||||
createdAt: z.string().optional(),
|
||||
updatedAt: z.string().optional(),
|
||||
});
|
||||
|
||||
export const importMonitorsBodyValidation = z.object({
|
||||
monitors: z.array(importedMonitorSchema).min(1, "At least one monitor is required"),
|
||||
});
|
||||
|
||||
export const getHardwareDetailsByIdParamValidation = z.object({
|
||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
notificationName: z.string().min(1, "Notification name is required"),
|
||||
type: z.literal("webhook"),
|
||||
address: z.url({ message: "Please enter a valid Webhook URL" }),
|
||||
address: z.string().url("Please enter a valid Webhook URL"),
|
||||
homeserverUrl: z.union([z.string(), z.literal("")]).optional(),
|
||||
roomId: z.union([z.string(), z.literal("")]).optional(),
|
||||
accessToken: z.union([z.string(), z.literal("")]).optional(),
|
||||
@@ -27,7 +27,7 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
notificationName: z.string().min(1, "Notification name is required"),
|
||||
type: z.literal("slack"),
|
||||
address: z.url({ message: "Please enter a valid Webhook URL" }),
|
||||
address: z.string().url("Please enter a valid Webhook URL"),
|
||||
homeserverUrl: z.union([z.string(), z.literal("")]).optional(),
|
||||
roomId: z.union([z.string(), z.literal("")]).optional(),
|
||||
accessToken: z.union([z.string(), z.literal("")]).optional(),
|
||||
@@ -36,7 +36,7 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
notificationName: z.string().min(1, "Notification name is required"),
|
||||
type: z.literal("discord"),
|
||||
address: z.url({ message: "Please enter a valid Webhook URL" }),
|
||||
address: z.string().url("Please enter a valid Webhook URL"),
|
||||
homeserverUrl: z.union([z.string(), z.literal("")]).optional(),
|
||||
roomId: z.union([z.string(), z.literal("")]).optional(),
|
||||
accessToken: z.union([z.string(), z.literal("")]).optional(),
|
||||
@@ -55,34 +55,12 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [
|
||||
notificationName: z.string().min(1, "Notification name is required"),
|
||||
type: z.literal("matrix"),
|
||||
address: z.union([z.string(), z.literal("")]).optional(),
|
||||
homeserverUrl: z.url({ message: "Please enter a valid Homeserver URL" }),
|
||||
homeserverUrl: z.string().url("Please enter a valid Homeserver URL"),
|
||||
roomId: z.string().min(1, "Room ID is required"),
|
||||
accessToken: z.string().min(1, "Access Token is required"),
|
||||
}),
|
||||
// Teams notification
|
||||
z.object({
|
||||
notificationName: z.string().min(1, "Notification name is required"),
|
||||
type: z.literal("teams"),
|
||||
address: z.url({ message: "Please enter a valid Webhook URL" }),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const testNotificationBodyValidation = createNotificationBodyValidation;
|
||||
|
||||
export const deleteNotificationParamValidation = z.object({
|
||||
id: z.string().min(1, "Notification ID is required"),
|
||||
});
|
||||
export const getNotificationByIdParamValidation = z.object({
|
||||
id: z.string().min(1, "Notification ID is required"),
|
||||
});
|
||||
export const editNotificationParamValidation = z.object({
|
||||
id: z.string().min(1, "Notification ID is required"),
|
||||
});
|
||||
|
||||
export const testAllNotificationsBodyValidation = z.object({
|
||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||
});
|
||||
|
||||
export const sendTestEmailBodyValidation = z.object({
|
||||
to: z.string().min(1, "To field is required"),
|
||||
systemEmailHost: z.string().optional(),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CHECK_TTL_SENTINEL } from "@/types/check.js";
|
||||
import { z } from "zod";
|
||||
|
||||
//****************************************
|
||||
@@ -7,8 +6,9 @@ import { z } from "zod";
|
||||
|
||||
export const updateAppSettingsBodyValidation = z
|
||||
.object({
|
||||
checkTTL: z.union([z.number().int().min(1).max(CHECK_TTL_SENTINEL), z.literal("")]).optional(),
|
||||
checkTTL: z.union([z.number(), z.literal("")]).optional(),
|
||||
systemEmailPort: z.union([z.number(), z.literal("")]).optional(),
|
||||
|
||||
pagespeedApiKey: z.union([z.string(), z.literal("")]).optional(),
|
||||
language: z.union([z.string(), z.literal("")]).optional(),
|
||||
timezone: z.union([z.string(), z.literal("")]).optional(),
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
|
||||
import { TeamsProvider } from "../src/service/infrastructure/notificationProviders/teams.ts";
|
||||
import type { Notification } from "../src/types/notification.ts";
|
||||
import type { NotificationMessage } from "../src/types/notificationMessage.ts";
|
||||
|
||||
// Mock got
|
||||
jest.unstable_mockModule("got", () => ({
|
||||
default: { post: jest.fn() },
|
||||
}));
|
||||
|
||||
const createLogger = () => ({
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
});
|
||||
|
||||
const createNotification = (overrides?: Partial<Notification>): Notification => ({
|
||||
id: "notif-1",
|
||||
userId: "user-1",
|
||||
teamId: "team-1",
|
||||
type: "teams",
|
||||
notificationName: "Teams Alert",
|
||||
address: "https://xxxxx.webhook.office.com/webhookb2/test",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createMessage = (overrides?: Partial<NotificationMessage>): NotificationMessage => ({
|
||||
type: "monitor_down",
|
||||
severity: "critical",
|
||||
monitor: {
|
||||
id: "mon-1",
|
||||
name: "API Server",
|
||||
url: "https://api.example.com",
|
||||
type: "http",
|
||||
status: "down",
|
||||
},
|
||||
content: {
|
||||
title: "🔴 Monitor Down",
|
||||
summary: "API Server is not responding",
|
||||
details: ["Response timeout after 30s"],
|
||||
thresholds: [],
|
||||
incident: {
|
||||
id: "inc-1",
|
||||
url: "https://app.example.com/incidents/inc-1",
|
||||
createdAt: new Date(),
|
||||
},
|
||||
timestamp: new Date("2026-01-15T12:00:00Z"),
|
||||
},
|
||||
clientHost: "https://app.example.com",
|
||||
metadata: {
|
||||
teamId: "team-1",
|
||||
notificationReason: "monitor_down",
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("TeamsProvider", () => {
|
||||
let provider: TeamsProvider;
|
||||
let logger: ReturnType<typeof createLogger>;
|
||||
let gotPost: jest.Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
logger = createLogger();
|
||||
provider = new TeamsProvider(logger);
|
||||
const got = await import("got");
|
||||
gotPost = got.default.post as jest.Mock;
|
||||
gotPost.mockReset();
|
||||
gotPost.mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe("sendTestAlert", () => {
|
||||
it("returns false when address is missing", async () => {
|
||||
const notification = createNotification({ address: undefined });
|
||||
const result = await provider.sendTestAlert(notification);
|
||||
expect(result).toBe(false);
|
||||
expect(gotPost).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends an Adaptive Card test message and returns true on success", async () => {
|
||||
const notification = createNotification();
|
||||
const result = await provider.sendTestAlert(notification);
|
||||
expect(result).toBe(true);
|
||||
expect(gotPost).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [url, options] = gotPost.mock.calls[0] as [string, { json: any; headers: any }];
|
||||
expect(url).toBe(notification.address);
|
||||
expect(options.headers["Content-Type"]).toBe("application/json");
|
||||
|
||||
// Verify Teams webhook envelope
|
||||
const payload = options.json;
|
||||
expect(payload.type).toBe("message");
|
||||
expect(payload.attachments).toHaveLength(1);
|
||||
expect(payload.attachments[0].contentType).toBe("application/vnd.microsoft.card.adaptive");
|
||||
|
||||
// Verify Adaptive Card structure
|
||||
const card = payload.attachments[0].content;
|
||||
expect(card.type).toBe("AdaptiveCard");
|
||||
expect(card.version).toBe("1.4");
|
||||
expect(card.body[0].type).toBe("TextBlock");
|
||||
expect(card.body[0].text).toContain("test notification");
|
||||
});
|
||||
|
||||
it("returns false and logs warning on HTTP error", async () => {
|
||||
gotPost.mockRejectedValue(new Error("Network error"));
|
||||
const notification = createNotification();
|
||||
const result = await provider.sendTestAlert(notification);
|
||||
expect(result).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: "Teams test alert failed",
|
||||
service: "TeamsProvider",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMessage", () => {
|
||||
it("returns false when address is missing", async () => {
|
||||
const notification = createNotification({ address: undefined });
|
||||
const result = await provider.sendMessage(notification, createMessage());
|
||||
expect(result).toBe(false);
|
||||
expect(gotPost).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends a well-formed Adaptive Card and returns true", async () => {
|
||||
const notification = createNotification();
|
||||
const message = createMessage();
|
||||
const result = await provider.sendMessage(notification, message);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(gotPost).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [url, options] = gotPost.mock.calls[0] as [string, { json: any }];
|
||||
expect(url).toBe(notification.address);
|
||||
|
||||
const payload = options.json;
|
||||
expect(payload.type).toBe("message");
|
||||
|
||||
const card = payload.attachments[0].content;
|
||||
expect(card.type).toBe("AdaptiveCard");
|
||||
expect(card.version).toBe("1.4");
|
||||
|
||||
// Verify body contains expected elements
|
||||
const textBlocks = card.body.filter((b: any) => b.type === "TextBlock");
|
||||
const factSets = card.body.filter((b: any) => b.type === "FactSet");
|
||||
|
||||
// Title block
|
||||
expect(textBlocks[0].text).toBe(message.content.title);
|
||||
expect(textBlocks[0].color).toBe("attention"); // critical -> attention
|
||||
|
||||
// Summary block
|
||||
expect(textBlocks[1].text).toBe(message.content.summary);
|
||||
|
||||
// FactSet with monitor details
|
||||
expect(factSets).toHaveLength(1);
|
||||
const facts = factSets[0].facts;
|
||||
expect(facts).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ title: "Name", value: "API Server" }),
|
||||
expect.objectContaining({ title: "URL", value: "https://api.example.com" }),
|
||||
expect.objectContaining({ title: "Status", value: "down" }),
|
||||
])
|
||||
);
|
||||
|
||||
// Incident action button
|
||||
expect(card.actions).toHaveLength(1);
|
||||
expect(card.actions[0].type).toBe("Action.OpenUrl");
|
||||
expect(card.actions[0].title).toBe("View Incident");
|
||||
expect(card.actions[0].url).toContain("/infrastructure/mon-1");
|
||||
});
|
||||
|
||||
it("omits actions when no incident is present", async () => {
|
||||
const notification = createNotification();
|
||||
const message = createMessage({
|
||||
content: {
|
||||
title: "Test",
|
||||
summary: "Summary",
|
||||
timestamp: new Date(),
|
||||
},
|
||||
});
|
||||
const result = await provider.sendMessage(notification, message);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const [, options] = gotPost.mock.calls[0] as [string, { json: any }];
|
||||
const card = options.json.attachments[0].content;
|
||||
expect(card.actions).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includes threshold breaches when present", async () => {
|
||||
const notification = createNotification();
|
||||
const message = createMessage({
|
||||
type: "threshold_breach",
|
||||
severity: "warning",
|
||||
content: {
|
||||
title: "Threshold Breach",
|
||||
summary: "CPU usage exceeded",
|
||||
thresholds: [
|
||||
{
|
||||
metric: "cpu",
|
||||
currentValue: 95,
|
||||
threshold: 80,
|
||||
unit: "%",
|
||||
formattedValue: "95%",
|
||||
},
|
||||
],
|
||||
timestamp: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await provider.sendMessage(notification, message);
|
||||
|
||||
const [, options] = gotPost.mock.calls[0] as [string, { json: any }];
|
||||
const card = options.json.attachments[0].content;
|
||||
|
||||
// Title should use "warning" color
|
||||
expect(card.body[0].color).toBe("warning");
|
||||
|
||||
// Find threshold text blocks
|
||||
const thresholdHeader = card.body.find((b: any) => b.type === "TextBlock" && b.text === "**Threshold Breaches**");
|
||||
expect(thresholdHeader).toBeDefined();
|
||||
|
||||
const cpuBlock = card.body.find((b: any) => b.type === "TextBlock" && b.text?.includes("CPU"));
|
||||
expect(cpuBlock).toBeDefined();
|
||||
expect(cpuBlock.text).toContain("95%");
|
||||
expect(cpuBlock.text).toContain("threshold: 80%");
|
||||
});
|
||||
|
||||
it("maps severity to correct Adaptive Card colors", async () => {
|
||||
const notification = createNotification();
|
||||
|
||||
const severityMap = [
|
||||
{ severity: "critical", expected: "attention" },
|
||||
{ severity: "warning", expected: "warning" },
|
||||
{ severity: "success", expected: "good" },
|
||||
{ severity: "info", expected: "accent" },
|
||||
] as const;
|
||||
|
||||
for (const { severity, expected } of severityMap) {
|
||||
gotPost.mockReset();
|
||||
gotPost.mockResolvedValue({});
|
||||
|
||||
const message = createMessage({ severity });
|
||||
await provider.sendMessage(notification, message);
|
||||
|
||||
const [, options] = gotPost.mock.calls[0] as [string, { json: any }];
|
||||
const card = options.json.attachments[0].content;
|
||||
expect(card.body[0].color).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns false and logs warning on HTTP error", async () => {
|
||||
gotPost.mockRejectedValue(new Error("502 Bad Gateway"));
|
||||
const notification = createNotification();
|
||||
const result = await provider.sendMessage(notification, createMessage());
|
||||
expect(result).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: "Teams alert failed via sendMessage",
|
||||
service: "TeamsProvider",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,8 +14,7 @@
|
||||
"moduleResolution": "nodenext",
|
||||
"skipLibCheck": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["tests", "dist", "node_modules"]
|
||||
|
||||
Reference in new issue
Block a user