mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-04-26 02:48:54 -05:00
Remove business V2 logic
This commit is contained in:
@@ -16,13 +16,6 @@ import NotificationController from "../controllers/v1/notificationController.js"
|
||||
import DiagnosticController from "../controllers/v1/diagnosticController.js";
|
||||
import IncidentController from "../controllers/v1/incidentController.js";
|
||||
|
||||
// V2 Controllers
|
||||
import AuthControllerV2 from "../controllers/v2/AuthController.js";
|
||||
import InviteControllerV2 from "../controllers/v2/InviteController.js";
|
||||
import MaintenanceControllerV2 from "../controllers/v2/MaintenanceController.js";
|
||||
import MonitorControllerV2 from "../controllers/v2/MonitorController.js";
|
||||
import NotificationChannelControllerV2 from "../controllers/v2/NotificationChannelController.js";
|
||||
import QueueControllerV2 from "../controllers/v2/QueueController.js";
|
||||
export const initializeControllers = (services) => {
|
||||
const controllers = {};
|
||||
const commonDependencies = createCommonDependencies(services.db, services.errorService, services.logger, services.stringService);
|
||||
@@ -75,13 +68,5 @@ export const initializeControllers = (services) => {
|
||||
incidentService: services.incidentService,
|
||||
});
|
||||
|
||||
//V2
|
||||
controllers.authControllerV2 = new AuthControllerV2(services.authServiceV2, services.inviteServiceV2);
|
||||
controllers.inviteControllerV2 = new InviteControllerV2(services.inviteServiceV2);
|
||||
controllers.maintenanceControllerV2 = new MaintenanceControllerV2(services.maintenanceServiceV2);
|
||||
controllers.monitorControllerV2 = new MonitorControllerV2(services.monitorServiceV2, services.checkServiceV2);
|
||||
controllers.notificationChannelControllerV2 = new NotificationChannelControllerV2(services.notificationChannelServiceV2);
|
||||
controllers.queueControllerV2 = new QueueControllerV2(services.jobQueueV2);
|
||||
|
||||
return controllers;
|
||||
};
|
||||
|
||||
@@ -15,14 +15,6 @@ import NotificationRoutes from "../routes/v1/notificationRoute.js";
|
||||
|
||||
import IncidentRoutes from "../routes/v1/incidentRoute.js";
|
||||
|
||||
//V2
|
||||
import AuthRoutesV2 from "../routes/v2/auth.js";
|
||||
import InviteRoutesV2 from "../routes/v2/invite.js";
|
||||
import MaintenanceRoutesV2 from "../routes/v2/maintenance.js";
|
||||
import MonitorRoutesV2 from "../routes/v2/monitors.js";
|
||||
import NotificationChannelRoutesV2 from "../routes/v2/notificationChannels.js";
|
||||
import QueueRoutesV2 from "../routes/v2/queue.js";
|
||||
|
||||
export const setupRoutes = (app, controllers) => {
|
||||
// V1
|
||||
const authRoutes = new AuthRoutes(controllers.authController);
|
||||
@@ -50,19 +42,4 @@ export const setupRoutes = (app, controllers) => {
|
||||
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
|
||||
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());
|
||||
app.use("/api/v1/incidents", verifyJWT, incidentRoutes.getRouter());
|
||||
|
||||
// V2
|
||||
const authRoutesV2 = new AuthRoutesV2(controllers.authControllerV2);
|
||||
const inviteRoutesV2 = new InviteRoutesV2(controllers.inviteControllerV2);
|
||||
const maintenanceRoutesV2 = new MaintenanceRoutesV2(controllers.maintenanceControllerV2);
|
||||
const monitorRoutesV2 = new MonitorRoutesV2(controllers.monitorControllerV2);
|
||||
const notificationChannelRoutesV2 = new NotificationChannelRoutesV2(controllers.notificationChannelControllerV2);
|
||||
const queueRoutesV2 = new QueueRoutesV2(controllers.queueControllerV2);
|
||||
|
||||
app.use("/api/v2/auth", authApiLimiter, authRoutesV2.getRouter());
|
||||
app.use("/api/v2/invite", inviteRoutesV2.getRouter());
|
||||
app.use("/api/v2/maintenance", maintenanceRoutesV2.getRouter());
|
||||
app.use("/api/v2/monitors", monitorRoutesV2.getRouter());
|
||||
app.use("/api/v2/notification-channels", notificationChannelRoutesV2.getRouter());
|
||||
app.use("/api/v2/queue", queueRoutesV2.getRouter());
|
||||
};
|
||||
|
||||
@@ -71,28 +71,6 @@ import RecoveryModule from "../db/v1/modules/recoveryModule.js";
|
||||
import SettingsModule from "../db/v1/modules/settingsModule.js";
|
||||
import IncidentModule from "../db/v1/modules/incidentModule.js";
|
||||
|
||||
// V2 Business
|
||||
import AuthServiceV2 from "../service/v2/business/AuthService.js";
|
||||
import CheckServiceV2 from "../service/v2/business/CheckService.js";
|
||||
import InviteServiceV2 from "../service/v2/business/InviteService.js";
|
||||
import MaintenanceServiceV2 from "../service/v2/business/MaintenanceService.js";
|
||||
import MonitorServiceV2 from "../service/v2/business/MonitorService.js";
|
||||
import MonitorStatsServiceV2 from "../service/v2/business/MonitorStatsService.js";
|
||||
import NotificationChannelServiceV2 from "../service/v2/business/NotificationChannelService.js";
|
||||
import QueueServiceV2 from "../service/v2/business/QueueService.js";
|
||||
import UserServiceV2 from "../service/v2/business/UserService.js";
|
||||
|
||||
// V2 Infra
|
||||
import DiscordServiceV2 from "../service/v2/infrastructure/NotificationServices/Discord.js";
|
||||
import EmailServiceV2 from "../service/v2/infrastructure/NotificationServices/Email.js";
|
||||
import SlackServiceV2 from "../service/v2/infrastructure/NotificationServices/Slack.js";
|
||||
import WebhookServiceV2 from "../service/v2/infrastructure/NotificationServices/Webhook.js";
|
||||
import JobGeneratorV2 from "../service/v2/infrastructure/JobGenerator.js";
|
||||
import JobQueueV2 from "../service/v2/infrastructure/JobQueue.js";
|
||||
import NetworkServiceV2 from "../service/v2/infrastructure/NetworkService.js";
|
||||
import NotificationServiceV2 from "../service/v2/infrastructure/NotificationService.js";
|
||||
import StatusServiceV2 from "../service/v2/infrastructure/StatusService.js";
|
||||
|
||||
export const initializeServices = async ({ logger, envSettings, settingsService }) => {
|
||||
const serviceRegistry = new ServiceRegistry({ logger });
|
||||
ServiceRegistry.instance = serviceRegistry;
|
||||
@@ -244,33 +222,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
games,
|
||||
});
|
||||
|
||||
// V2 Services
|
||||
const checkServiceV2 = new CheckServiceV2();
|
||||
const inviteServiceV2 = new InviteServiceV2();
|
||||
const maintenanceServiceV2 = new MaintenanceServiceV2();
|
||||
const monitorStatsServiceV2 = new MonitorStatsServiceV2();
|
||||
const notificationChannelServiceV2 = new NotificationChannelServiceV2();
|
||||
const userServiceV2 = new UserServiceV2();
|
||||
const discordServiceV2 = new DiscordServiceV2();
|
||||
const emailServiceV2 = new EmailServiceV2(userServiceV2);
|
||||
const slackServiceV2 = new SlackServiceV2();
|
||||
const webhookServiceV2 = new WebhookServiceV2();
|
||||
const networkServiceV2 = new NetworkServiceV2();
|
||||
const statusServiceV2 = new StatusServiceV2();
|
||||
const notificationServiceV2 = new NotificationServiceV2(userServiceV2);
|
||||
const jobGeneratorV2 = new JobGeneratorV2(
|
||||
networkServiceV2,
|
||||
checkServiceV2,
|
||||
monitorStatsServiceV2,
|
||||
statusServiceV2,
|
||||
notificationServiceV2,
|
||||
maintenanceServiceV2
|
||||
);
|
||||
const jobQueueV2 = await JobQueueV2.create(jobGeneratorV2);
|
||||
const authServiceV2 = new AuthServiceV2(jobQueueV2);
|
||||
const monitorServiceV2 = new MonitorServiceV2(jobQueueV2);
|
||||
const queueServiceV2 = new QueueServiceV2(jobQueueV2);
|
||||
|
||||
const services = {
|
||||
//v1
|
||||
settingsService,
|
||||
@@ -292,25 +243,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
incidentService,
|
||||
errorService,
|
||||
logger,
|
||||
//v2
|
||||
jobQueueV2,
|
||||
authServiceV2,
|
||||
checkServiceV2,
|
||||
inviteServiceV2,
|
||||
maintenanceServiceV2,
|
||||
monitorServiceV2,
|
||||
monitorStatsServiceV2,
|
||||
notificationChannelServiceV2,
|
||||
queueServiceV2,
|
||||
userServiceV2,
|
||||
discordServiceV2,
|
||||
emailServiceV2,
|
||||
slackServiceV2,
|
||||
webhookServiceV2,
|
||||
networkServiceV2,
|
||||
statusServiceV2,
|
||||
notificationServiceV2,
|
||||
jobGeneratorV2,
|
||||
};
|
||||
|
||||
Object.values(services).forEach((service) => {
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { encode, decode } from "../../utils/JWTUtils.js";
|
||||
import AuthService from "../../service/v2/business/AuthService.js";
|
||||
import ApiError from "../../utils/ApiError.js";
|
||||
import InviteService from "../../service/v2/business/InviteService.js";
|
||||
import { IInvite } from "../../db/v2/models/index.js";
|
||||
|
||||
class AuthController {
|
||||
private authService: AuthService;
|
||||
private inviteService: InviteService;
|
||||
constructor(authService: AuthService, inviteService: InviteService) {
|
||||
this.authService = authService;
|
||||
this.inviteService = inviteService;
|
||||
}
|
||||
|
||||
register = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { email, firstName, lastName, password } = req.body;
|
||||
|
||||
if (!email || !firstName || !lastName || !password) {
|
||||
throw new Error("Email, firstName, lastName, and password are required");
|
||||
}
|
||||
|
||||
const result = await this.authService.register({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
});
|
||||
|
||||
const token = encode(result);
|
||||
|
||||
res.cookie("token", token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "strict",
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
message: "User created successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
registerWithInvite = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const token = req.params.token;
|
||||
if (!token) {
|
||||
throw new ApiError("Invite token is required", 400);
|
||||
}
|
||||
|
||||
const invite: IInvite = await this.inviteService.get(token);
|
||||
|
||||
const { firstName, lastName, password } = req.body;
|
||||
const email = invite?.email;
|
||||
const roles = invite?.roles;
|
||||
|
||||
if (!email || !firstName || !lastName || !password || !roles || roles.length === 0) {
|
||||
throw new Error("Email, firstName, lastName, password, and roles are required");
|
||||
}
|
||||
|
||||
const result = await this.authService.registerWithInvite({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
roles,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new Error("Registration failed");
|
||||
}
|
||||
|
||||
await this.inviteService.delete(invite._id.toString());
|
||||
|
||||
const jwt = encode(result);
|
||||
|
||||
res.cookie("token", jwt, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "strict",
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
|
||||
});
|
||||
|
||||
res.status(201).json({ message: "User created successfully" });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
login = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
// Validation
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ message: "Email and password are required" });
|
||||
}
|
||||
const result = await this.authService.login({ email, password });
|
||||
|
||||
const token = encode(result);
|
||||
|
||||
res.cookie("token", token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "strict",
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
message: "Login successful",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
logout = (req: Request, res: Response) => {
|
||||
res.clearCookie("token", {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "strict",
|
||||
});
|
||||
res.status(200).json({ message: "Logout successful" });
|
||||
};
|
||||
|
||||
me = (req: Request, res: Response, next: NextFunction) => {
|
||||
return res.status(200).json({ message: "OK" });
|
||||
};
|
||||
|
||||
cleanup = async (req: Request, res: Response) => {
|
||||
try {
|
||||
await this.authService.cleanup();
|
||||
res.status(200).json({ message: "Cleanup successful" });
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
cleanMonitors = async (req: Request, res: Response) => {
|
||||
try {
|
||||
await this.authService.cleanMonitors();
|
||||
res.status(200).json({ message: "Monitors cleanup successful" });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default AuthController;
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import InviteService from "../../service/v2/business/InviteService.js";
|
||||
|
||||
class InviteController {
|
||||
private inviteService: InviteService;
|
||||
constructor(inviteService: InviteService) {
|
||||
this.inviteService = inviteService;
|
||||
}
|
||||
|
||||
create = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
const invite = await this.inviteService.create(tokenizedUser, req.body);
|
||||
res.status(201).json({ message: "OK", data: invite });
|
||||
} catch (error: any) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getAll = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const invites = await this.inviteService.getAll();
|
||||
res.status(200).json({
|
||||
message: "OK",
|
||||
data: invites,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
get = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const token = req.params.token;
|
||||
if (!token) {
|
||||
return res.status(400).json({ message: "Token parameter is required" });
|
||||
}
|
||||
const invite = await this.inviteService.get(token);
|
||||
res.status(200).json({ message: "OK", data: invite });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
delete = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
return res.status(400).json({ message: "ID parameter is required" });
|
||||
}
|
||||
await this.inviteService.delete(id);
|
||||
res.status(204).json({ message: "OK" });
|
||||
} catch (error: any) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default InviteController;
|
||||
@@ -1,96 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import MaintenanceService from "../../service/v2/business/MaintenanceService.js";
|
||||
|
||||
class MaintenanceController {
|
||||
private maintenanceService: MaintenanceService;
|
||||
constructor(maintenanceService: MaintenanceService) {
|
||||
this.maintenanceService = maintenanceService;
|
||||
}
|
||||
|
||||
create = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
const maintenance = await this.maintenanceService.create(tokenizedUser, req.body);
|
||||
res.status(201).json({ message: "OK", data: maintenance });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getAll = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const maintenances = await this.maintenanceService.getAll();
|
||||
res.status(200).json({
|
||||
message: "OK",
|
||||
data: maintenances,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
toggleActive = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
return res.status(400).json({ message: "ID parameter is required" });
|
||||
}
|
||||
const maintenance = await this.maintenanceService.toggleActive(tokenizedUser, id);
|
||||
res.status(200).json({ message: "OK", data: maintenance });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
update = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
return res.status(400).json({ message: "ID parameter is required" });
|
||||
}
|
||||
const updatedMaintenance = await this.maintenanceService.update(tokenizedUser, id, req.body);
|
||||
res.status(200).json({ message: "OK", data: updatedMaintenance });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
get = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
return res.status(400).json({ message: "ID parameter is required" });
|
||||
}
|
||||
const maintenance = await this.maintenanceService.get(id);
|
||||
res.status(200).json({ message: "OK", data: maintenance });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
delete = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
return res.status(400).json({ message: "ID parameter is required" });
|
||||
}
|
||||
await this.maintenanceService.delete(id);
|
||||
res.status(204).json({ message: "OK" });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default MaintenanceController;
|
||||
@@ -1,191 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import ApiError from "../../utils/ApiError.js";
|
||||
import MonitorService from "../../service/v2/business/MonitorService.js";
|
||||
import { MonitorType } from "../../db/v2/models/monitors/Monitor.js";
|
||||
import CheckService from "../../service/v2/business/CheckService.js";
|
||||
class MonitorController {
|
||||
private monitorService: MonitorService;
|
||||
private checkService: CheckService;
|
||||
constructor(monitorService: MonitorService, checkService: CheckService) {
|
||||
this.monitorService = monitorService;
|
||||
this.checkService = checkService;
|
||||
}
|
||||
|
||||
create = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const monitor = await this.monitorService.create(tokenizedUser, req.body);
|
||||
res.status(201).json({
|
||||
message: "Monitor created successfully",
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getAll = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
let monitors;
|
||||
if (req.query.embedChecks === "true") {
|
||||
const page = Math.max(1, Number(req.query.page) || 1);
|
||||
const limit = Math.max(1, Number(req.query.limit) || 10);
|
||||
const type: MonitorType[] = req.query.type as MonitorType[];
|
||||
|
||||
monitors = await this.monitorService.getAllEmbedChecks(page, limit, type);
|
||||
} else {
|
||||
monitors = await this.monitorService.getAll();
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
message: "Monitors retrieved successfully",
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getChecks = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
throw new ApiError("Monitor ID is required", 400);
|
||||
}
|
||||
|
||||
const page = Number(req.query.page);
|
||||
const rowsPerPage = Number(req.query.rowsPerPage);
|
||||
|
||||
if (isNaN(page)) throw new ApiError("Page query parameter must be a number", 400);
|
||||
if (isNaN(rowsPerPage)) throw new ApiError("rowsPerPage query parameter must be a number", 400);
|
||||
|
||||
if (page < 0) throw new ApiError("Page must be greater than 0", 400);
|
||||
if (rowsPerPage < 0) throw new ApiError("rowsPerPage must be greater than 0", 400);
|
||||
|
||||
const { count, checks } = await this.checkService.getChecks(id, page, rowsPerPage);
|
||||
res.status(200).json({
|
||||
message: "Checks retrieved successfully",
|
||||
data: { count, checks },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
toggleActive = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
throw new ApiError("Monitor ID is required", 400);
|
||||
}
|
||||
|
||||
const monitor = await this.monitorService.toggleActive(id, tokenizedUser);
|
||||
res.status(200).json({
|
||||
message: "Monitor paused/unpaused successfully",
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
get = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
throw new ApiError("Monitor ID is required", 400);
|
||||
}
|
||||
|
||||
const range = req.query.range;
|
||||
if (!range || typeof range !== "string") throw new ApiError("Range query parameter is required", 400);
|
||||
|
||||
let monitor;
|
||||
|
||||
const status = req.query.status;
|
||||
if (status && typeof status !== "string") {
|
||||
throw new ApiError("Status query parameter must be a string", 400);
|
||||
}
|
||||
|
||||
if (req.query.embedChecks === "true") {
|
||||
monitor = await this.monitorService.getEmbedChecks(id, range, status);
|
||||
} else {
|
||||
monitor = await this.monitorService.get(id);
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
message: "Monitor retrieved successfully",
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
update = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
throw new ApiError("Monitor ID is required", 400);
|
||||
}
|
||||
|
||||
const monitor = await this.monitorService.update(tokenizedUser, id, req.body);
|
||||
res.status(200).json({
|
||||
message: "Monitor updated successfully",
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
delete = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
throw new ApiError("Monitor ID is required", 400);
|
||||
}
|
||||
await this.monitorService.delete(id);
|
||||
|
||||
res.status(200).json({
|
||||
message: "Monitor deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default MonitorController;
|
||||
@@ -1,96 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import NotificationService from "../../service/v2/business/NotificationChannelService.js";
|
||||
|
||||
class NotificationChannelController {
|
||||
private notificationService: NotificationService;
|
||||
constructor(notificationService: NotificationService) {
|
||||
this.notificationService = notificationService;
|
||||
}
|
||||
|
||||
create = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
const channel = await this.notificationService.create(tokenizedUser, req.body);
|
||||
res.status(201).json({ message: "OK", data: channel });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getAll = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const notificationChannels = await this.notificationService.getAll();
|
||||
res.status(200).json({
|
||||
message: "OK",
|
||||
data: notificationChannels,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
toggleActive = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
return res.status(400).json({ message: "ID parameter is required" });
|
||||
}
|
||||
const notificationChannel = await this.notificationService.toggleActive(tokenizedUser, id);
|
||||
res.status(200).json({ message: "OK", data: notificationChannel });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
update = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
return res.status(400).json({ message: "ID parameter is required" });
|
||||
}
|
||||
const updatedChannel = await this.notificationService.update(tokenizedUser, id, req.body);
|
||||
res.status(200).json({ message: "OK", data: updatedChannel });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
get = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
return res.status(400).json({ message: "ID parameter is required" });
|
||||
}
|
||||
const notificationChannel = await this.notificationService.get(id);
|
||||
res.status(200).json({ message: "OK", data: notificationChannel });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
delete = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
return res.status(400).json({ message: "ID parameter is required" });
|
||||
}
|
||||
await this.notificationService.delete(id);
|
||||
res.status(204).json({ message: "OK" });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default NotificationChannelController;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import QueueService from "../../service/v2/business/QueueService.js";
|
||||
class QueueController {
|
||||
private queueService: QueueService;
|
||||
constructor(queueService: QueueService) {
|
||||
this.queueService = queueService;
|
||||
}
|
||||
|
||||
getJobs = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const jobs = await this.queueService.getJobs();
|
||||
res.status(200).json({ message: "ok", data: jobs });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getMetrics = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const metrics = await this.queueService.getMetrics();
|
||||
res.status(200).json({ message: "ok", data: metrics });
|
||||
};
|
||||
|
||||
flush = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const result = await this.queueService.flush();
|
||||
res.status(200).json({ message: "ok", flushed: result });
|
||||
};
|
||||
}
|
||||
|
||||
export default QueueController;
|
||||
@@ -1,25 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/checkmate";
|
||||
|
||||
export const connectDatabase = async (): Promise<boolean> => {
|
||||
try {
|
||||
await mongoose.connect(MONGODB_URI);
|
||||
console.log("Connected to MongoDB");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("MongoDB connection error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export const disconnectDatabase = async (): Promise<boolean> => {
|
||||
try {
|
||||
await mongoose.disconnect();
|
||||
console.log("Disconnected from MongoDB");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("MongoDB disconnection error:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
|
||||
export interface IRole extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions: string[];
|
||||
isSystem: boolean;
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const roleSchema = new Schema<IRole>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 50,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 200,
|
||||
},
|
||||
|
||||
permissions: [
|
||||
{
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
isSystem: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
roleSchema.index({ name: 1 }, { unique: true });
|
||||
|
||||
export const Role = mongoose.model<IRole>("Role_v2", roleSchema);
|
||||
@@ -1,102 +0,0 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
|
||||
export interface ITokenizedUser {
|
||||
sub: string;
|
||||
roles: string[];
|
||||
}
|
||||
export interface IUser extends Document {
|
||||
_id: Types.ObjectId;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
passwordHash: string;
|
||||
roles: Types.ObjectId[];
|
||||
profile: {
|
||||
avatar?: string;
|
||||
bio?: string;
|
||||
timezone: string;
|
||||
locale: string;
|
||||
};
|
||||
preferences: {
|
||||
theme: "light" | "dark" | "system";
|
||||
};
|
||||
lastLoginAt?: Date;
|
||||
isActive: boolean;
|
||||
isVerified: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const userSchema = new Schema<IUser>(
|
||||
{
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
},
|
||||
firstName: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 50,
|
||||
},
|
||||
lastName: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 50,
|
||||
},
|
||||
passwordHash: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Role_v2",
|
||||
},
|
||||
],
|
||||
profile: {
|
||||
avatar: {
|
||||
type: String,
|
||||
},
|
||||
bio: {
|
||||
type: String,
|
||||
maxlength: 200,
|
||||
},
|
||||
timezone: {
|
||||
type: String,
|
||||
default: "UTC",
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
default: "en",
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
theme: {
|
||||
type: String,
|
||||
enum: ["light", "dark", "system"],
|
||||
default: "system",
|
||||
},
|
||||
},
|
||||
lastLoginAt: {
|
||||
type: Date,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
export const User = mongoose.model<IUser>("User_v2", userSchema);
|
||||
@@ -1,297 +0,0 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
import { MonitorType, MonitorTypes, MonitorStatus, MonitorStatuses } from "../monitors/Monitor.js";
|
||||
import type { Response } from "got";
|
||||
export type GotTimings = Response["timings"];
|
||||
export interface ITimingPhases {
|
||||
wait: number;
|
||||
dns: number;
|
||||
tcp: number;
|
||||
tls: number;
|
||||
request: number;
|
||||
firstByte: number;
|
||||
download: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ICpuInfo {
|
||||
physical_core: number;
|
||||
logical_core: number;
|
||||
frequency: number;
|
||||
current_frequency: number;
|
||||
temperature: number[]; // per-core temps
|
||||
free_percent: number;
|
||||
usage_percent: number;
|
||||
}
|
||||
|
||||
export interface IMemoryInfo {
|
||||
total_bytes: number;
|
||||
available_bytes: number;
|
||||
used_bytes: number;
|
||||
usage_percent: number;
|
||||
}
|
||||
|
||||
export interface IHostInfo {
|
||||
os?: string;
|
||||
platform?: string;
|
||||
kernel_version?: string;
|
||||
pretty_name?: string;
|
||||
}
|
||||
|
||||
export interface IDiskInfo {
|
||||
device: string;
|
||||
total_bytes: number;
|
||||
free_bytes: number;
|
||||
used_bytes: number;
|
||||
usage_percent: number;
|
||||
total_inodes?: number;
|
||||
free_inodes?: number;
|
||||
used_inodes?: number;
|
||||
inodes_usage_percent?: number;
|
||||
read_bytes?: number;
|
||||
write_bytes?: number;
|
||||
read_time?: number;
|
||||
write_time?: number;
|
||||
}
|
||||
|
||||
export interface INetInfo {
|
||||
name: string;
|
||||
bytes_sent: number;
|
||||
bytes_recv: number;
|
||||
packets_sent: number;
|
||||
packets_recv: number;
|
||||
err_in: number;
|
||||
err_out: number;
|
||||
drop_in: number;
|
||||
drop_out: number;
|
||||
fifo_in: number;
|
||||
fifo_out: number;
|
||||
}
|
||||
|
||||
export interface ICaptureInfo {
|
||||
version?: string;
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
export interface ISystemInfo {
|
||||
cpu: ICpuInfo;
|
||||
memory: IMemoryInfo;
|
||||
disk: IDiskInfo[];
|
||||
host: IHostInfo;
|
||||
net: INetInfo[];
|
||||
}
|
||||
|
||||
export interface ILighthouseAudit {
|
||||
id?: string;
|
||||
title?: string;
|
||||
score?: number | null;
|
||||
displayValue?: string;
|
||||
numericValue?: number;
|
||||
numericUnit?: string;
|
||||
}
|
||||
export interface ILighthouseCategories {
|
||||
accessibility?: { score?: number | null };
|
||||
"best-practices"?: { score?: number | null };
|
||||
seo?: { score?: number | null };
|
||||
performance?: { score?: number | null };
|
||||
}
|
||||
|
||||
export interface ILighthouseResult {
|
||||
categories?: ILighthouseCategories;
|
||||
audits?: Record<string, ILighthouseAudit>;
|
||||
}
|
||||
|
||||
export interface ICheckLighthouseFields {
|
||||
accessibility: number;
|
||||
bestPractices: number;
|
||||
seo: number;
|
||||
performance: number;
|
||||
audits: {
|
||||
cls: ILighthouseAudit;
|
||||
si: ILighthouseAudit;
|
||||
fcp: ILighthouseAudit;
|
||||
lcp: ILighthouseAudit;
|
||||
tbt: ILighthouseAudit;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ICheck extends Document {
|
||||
_id: Types.ObjectId;
|
||||
monitorId: Types.ObjectId;
|
||||
type: MonitorType;
|
||||
status: MonitorStatus;
|
||||
message: string;
|
||||
responseTime?: number; // in ms
|
||||
timings?: GotTimings;
|
||||
httpStatusCode?: number;
|
||||
errorMessage?: string;
|
||||
ack: boolean;
|
||||
ackAt?: Date;
|
||||
ackBy?: Types.ObjectId;
|
||||
expiry: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
system?: ISystemInfo;
|
||||
capture?: ICaptureInfo;
|
||||
lighthouse?: ICheckLighthouseFields;
|
||||
}
|
||||
|
||||
const CheckSchema = new Schema<ICheck>(
|
||||
{
|
||||
monitorId: { type: Schema.Types.ObjectId, ref: "Monitor_v2", required: true },
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: MonitorTypes,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: MonitorStatuses,
|
||||
},
|
||||
message: { type: String, trim: true },
|
||||
responseTime: { type: Number },
|
||||
timings: {
|
||||
start: { type: Date },
|
||||
socket: { type: Date },
|
||||
lookup: { type: Date },
|
||||
connect: { type: Date },
|
||||
secureConnect: { type: Date },
|
||||
response: { type: Date },
|
||||
end: { type: Date },
|
||||
phases: {
|
||||
wait: { type: Number },
|
||||
dns: { type: Number },
|
||||
tcp: { type: Number },
|
||||
tls: { type: Number },
|
||||
request: { type: Number },
|
||||
firstByte: { type: Number },
|
||||
download: { type: Number },
|
||||
total: { type: Number },
|
||||
},
|
||||
},
|
||||
system: {
|
||||
type: {
|
||||
cpu: {
|
||||
physical_core: { type: Number },
|
||||
logical_core: { type: Number },
|
||||
frequency: { type: Number },
|
||||
current_frequency: { type: Number },
|
||||
temperature: [{ type: Number }],
|
||||
free_percent: { type: Number },
|
||||
usage_percent: { type: Number },
|
||||
},
|
||||
memory: {
|
||||
total_bytes: { type: Number },
|
||||
available_bytes: { type: Number },
|
||||
used_bytes: { type: Number },
|
||||
usage_percent: { type: Number },
|
||||
},
|
||||
disk: [
|
||||
{
|
||||
device: { type: String },
|
||||
total_bytes: { type: Number },
|
||||
free_bytes: { type: Number },
|
||||
used_bytes: { type: Number },
|
||||
usage_percent: { type: Number },
|
||||
total_inodes: { type: Number },
|
||||
free_inodes: { type: Number },
|
||||
used_inodes: { type: Number },
|
||||
inodes_usage_percent: { type: Number },
|
||||
read_bytes: { type: Number },
|
||||
write_bytes: { type: Number },
|
||||
read_time: { type: Number },
|
||||
write_time: { type: Number },
|
||||
},
|
||||
],
|
||||
host: {
|
||||
os: { type: String },
|
||||
platform: { type: String },
|
||||
kernel_version: { type: String },
|
||||
pretty_name: { type: String },
|
||||
},
|
||||
net: [
|
||||
{
|
||||
name: { type: String },
|
||||
bytes_sent: { type: Number },
|
||||
bytes_recv: { type: Number },
|
||||
packets_sent: { type: Number },
|
||||
packets_recv: { type: Number },
|
||||
err_in: { type: Number },
|
||||
err_out: { type: Number },
|
||||
drop_in: { type: Number },
|
||||
drop_out: { type: Number },
|
||||
fifo_in: { type: Number },
|
||||
fifo_out: { type: Number },
|
||||
},
|
||||
],
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
|
||||
capture: {
|
||||
type: {
|
||||
version: { type: String },
|
||||
mode: { type: String },
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
lighthouse: {
|
||||
accessibility: { type: Number, required: false },
|
||||
bestPractices: { type: Number, required: false },
|
||||
seo: { type: Number, required: false },
|
||||
performance: { type: Number, required: false },
|
||||
audits: {
|
||||
cls: {
|
||||
type: Object,
|
||||
},
|
||||
si: {
|
||||
type: Object,
|
||||
},
|
||||
fcp: {
|
||||
type: Object,
|
||||
},
|
||||
lcp: {
|
||||
type: Object,
|
||||
},
|
||||
tbt: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
type: {
|
||||
accessibility: { type: Number },
|
||||
bestPractices: { type: Number },
|
||||
seo: { type: Number },
|
||||
performance: { type: Number },
|
||||
audits: {
|
||||
cls: { type: Object },
|
||||
si: { type: Object },
|
||||
fcp: { type: Object },
|
||||
lcp: { type: Object },
|
||||
tbt: { type: Object },
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
|
||||
httpStatusCode: { type: Number },
|
||||
errorMessage: { type: String, trim: true },
|
||||
ack: { type: Boolean, required: true, default: false },
|
||||
ackAt: { type: Date },
|
||||
ackBy: { type: Schema.Types.ObjectId, ref: "User_v2" },
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
expires: 60 * 60 * 24 * 30,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
CheckSchema.index({ monitorId: 1, createdAt: -1 });
|
||||
CheckSchema.index({ status: 1 });
|
||||
CheckSchema.index({ status: 1, ack: 1 });
|
||||
CheckSchema.index({ ack: 1, ackAt: 1 });
|
||||
CheckSchema.index({ createdAt: -1 });
|
||||
|
||||
export const Check = mongoose.model<ICheck>("Check_v2", CheckSchema);
|
||||
@@ -1,33 +0,0 @@
|
||||
export { User } from "./auth/User.js";
|
||||
export type { IUser } from "./auth/User.js";
|
||||
export type { ITokenizedUser } from "./auth/User.js";
|
||||
export { Role } from "./auth/Role.js";
|
||||
export type { IRole } from "./auth/Role.js";
|
||||
export { connectDatabase, disconnectDatabase } from "../index.js";
|
||||
export { Monitor } from "./monitors/Monitor.js";
|
||||
export { MonitorStatuses } from "./monitors/Monitor.js";
|
||||
export type { IMonitor } from "./monitors/Monitor.js";
|
||||
export { Check } from "./checks/Check.js";
|
||||
export type {
|
||||
ICheck,
|
||||
ISystemInfo,
|
||||
ICaptureInfo,
|
||||
INetInfo,
|
||||
IDiskInfo,
|
||||
IHostInfo,
|
||||
IMemoryInfo,
|
||||
ICpuInfo,
|
||||
ILighthouseAudit,
|
||||
ITimingPhases,
|
||||
ILighthouseCategories,
|
||||
ILighthouseResult,
|
||||
ICheckLighthouseFields,
|
||||
} from "./checks/Check.js";
|
||||
export type { IMonitorStats } from "./monitors/MonitorStats.js";
|
||||
export { MonitorStats } from "./monitors/MonitorStats.js";
|
||||
export type { INotificationChannel } from "./notification-channel/NotificationChannel.js";
|
||||
export { NotificationChannel } from "./notification-channel/NotificationChannel.js";
|
||||
export type { IMaintenance } from "./maintenance/Maintenance.js";
|
||||
export { Maintenance } from "./maintenance/Maintenance.js";
|
||||
export type { IInvite } from "./invite/Invite.js";
|
||||
export { Invite } from "./invite/Invite.js";
|
||||
@@ -1,43 +0,0 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
|
||||
export interface IInvite extends Document {
|
||||
_id: Types.ObjectId;
|
||||
email: string;
|
||||
tokenHash: string;
|
||||
roles: Types.ObjectId[];
|
||||
createdBy: Types.ObjectId;
|
||||
updatedBy: Types.ObjectId;
|
||||
expiry: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const InviteSchema = new Schema<IInvite>(
|
||||
{
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Role_v2",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
tokenHash: { type: String, required: true, unique: true },
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
expires: 60 * 60 * 24,
|
||||
},
|
||||
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
export const Invite = mongoose.model<IInvite>("Invite_v2", InviteSchema);
|
||||
@@ -1,39 +0,0 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
|
||||
export interface IMaintenance extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
monitors: Types.ObjectId[];
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
createdBy: Types.ObjectId;
|
||||
updatedBy: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const MaintenanceSchema = new Schema<IMaintenance>(
|
||||
{
|
||||
name: { type: String, required: true, trim: true },
|
||||
isActive: { type: Boolean, required: true, default: true },
|
||||
monitors: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Monitor_v2",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
startTime: { type: Date, required: true },
|
||||
endTime: { type: Date, required: true },
|
||||
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
MaintenanceSchema.index({ isActive: 1 });
|
||||
MaintenanceSchema.index({ startTime: 1 });
|
||||
MaintenanceSchema.index({ endTime: 1 });
|
||||
|
||||
export const Maintenance = mongoose.model<IMaintenance>("Maintenance_v2", MaintenanceSchema);
|
||||
@@ -1,96 +0,0 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
import { Check, MonitorStats } from "../index.js";
|
||||
|
||||
export const MonitorTypes = ["http", "https", "ping", "infrastructure", "pagespeed"] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const MonitorStatuses = ["up", "down", "paused", "initializing"] as const;
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
export interface IMonitor extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
url: string;
|
||||
secret?: string;
|
||||
type: MonitorType;
|
||||
interval: number; // in ms
|
||||
isActive: boolean;
|
||||
status: MonitorStatus;
|
||||
n: number; // Number of consecutive successes required to change status
|
||||
lastCheckedAt?: Date;
|
||||
latestChecks: {
|
||||
status: MonitorStatus;
|
||||
responseTime: number;
|
||||
checkedAt: Date;
|
||||
}[];
|
||||
notificationChannels?: Types.ObjectId[];
|
||||
createdBy: Types.ObjectId;
|
||||
updatedBy: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const MonitorSchema = new Schema<IMonitor>(
|
||||
{
|
||||
name: { type: String, required: true, trim: true, maxlength: 100 },
|
||||
url: { type: String, required: true, trim: true },
|
||||
secret: { type: String, required: false },
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: MonitorTypes,
|
||||
},
|
||||
interval: { type: Number, required: true, default: 60000 },
|
||||
isActive: { type: Boolean, required: true, default: true },
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "initializing",
|
||||
enum: MonitorStatuses,
|
||||
},
|
||||
n: { type: Number, required: true, default: 1 },
|
||||
lastCheckedAt: { type: Date },
|
||||
latestChecks: {
|
||||
type: [
|
||||
{
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: MonitorStatuses,
|
||||
},
|
||||
responseTime: { type: Number, required: true },
|
||||
checkedAt: { type: Date, required: true },
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
},
|
||||
notificationChannels: {
|
||||
type: [{ type: Schema.Types.ObjectId, ref: "NotificationChannel_v2" }],
|
||||
default: [],
|
||||
},
|
||||
|
||||
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
MonitorSchema.pre("deleteOne", { document: true, query: false }, async function (next) {
|
||||
try {
|
||||
const monitorId = this._id;
|
||||
await Check.deleteMany({ monitorId });
|
||||
await MonitorStats.deleteMany({ monitorId });
|
||||
next();
|
||||
} catch (error: any) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
MonitorSchema.index({ isActive: 1 });
|
||||
MonitorSchema.index({ status: 1 });
|
||||
MonitorSchema.index({ type: 1 });
|
||||
MonitorSchema.index({ lastCheckedAt: 1 });
|
||||
MonitorSchema.index({ isActive: 1, status: 1 });
|
||||
MonitorSchema.index({ createdBy: 1 });
|
||||
MonitorSchema.index({ updatedBy: 1 });
|
||||
|
||||
export const Monitor = mongoose.model<IMonitor>("Monitor_v2", MonitorSchema);
|
||||
@@ -1,77 +0,0 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
import { MonitorStatus, MonitorStatuses } from "./Monitor.js";
|
||||
|
||||
export interface IMonitorStats extends mongoose.Document {
|
||||
monitorId: mongoose.Types.ObjectId;
|
||||
avgResponseTime: number;
|
||||
maxResponseTime: number;
|
||||
totalChecks: number;
|
||||
totalUpChecks: number;
|
||||
totalDownChecks: number;
|
||||
uptimePercentage: number;
|
||||
lastCheckTimestamp: number;
|
||||
lastResponseTime: number;
|
||||
timeOfLastFailure: number;
|
||||
currentStreak: number;
|
||||
currentStreakStatus: MonitorStatus;
|
||||
currentStreakStartedAt: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const MonitorStatsSchema = new Schema<IMonitorStats>(
|
||||
{
|
||||
monitorId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor_v2",
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
avgResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
maxResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalUpChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalDownChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
uptimePercentage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastCheckTimestamp: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
timeOfLastFailure: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
currentStreak: { type: Number, required: false, default: 0 },
|
||||
currentStreakStatus: {
|
||||
type: String,
|
||||
required: false,
|
||||
enum: MonitorStatuses,
|
||||
},
|
||||
currentStreakStartedAt: { type: Number, required: false },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
export const MonitorStats = mongoose.model<IMonitorStats>("MonitorStats_v2", MonitorStatsSchema);
|
||||
@@ -1,50 +0,0 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const;
|
||||
export type ChannelType = (typeof ChannelTypes)[number];
|
||||
|
||||
export interface INotificationChannelConfig {
|
||||
url?: string; // For webhook, slack, discord
|
||||
emailAddress?: string; // For email
|
||||
}
|
||||
|
||||
export interface INotificationChannel {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
type: ChannelType;
|
||||
config: INotificationChannelConfig;
|
||||
isActive: boolean;
|
||||
createdBy: Types.ObjectId;
|
||||
updatedBy: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const NotificationChannelConfigSchema = new Schema<INotificationChannelConfig>(
|
||||
{
|
||||
url: { type: String, required: false },
|
||||
emailAddress: { type: String, required: false },
|
||||
},
|
||||
{ _id: false, strict: "throw" }
|
||||
);
|
||||
|
||||
const NotificationChannelSchema = new Schema<INotificationChannel>(
|
||||
{
|
||||
name: { type: String, required: true, trim: true },
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ChannelTypes,
|
||||
},
|
||||
config: { type: NotificationChannelConfigSchema, required: true },
|
||||
isActive: { type: Boolean, required: true, default: true },
|
||||
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
NotificationChannelSchema.index({ isActive: 1 });
|
||||
NotificationChannelSchema.index({ type: 1 });
|
||||
NotificationChannelSchema.index({ type: 1, isActive: 1 });
|
||||
|
||||
export const NotificationChannel = mongoose.model<INotificationChannel>("NotificationChannel_v2", NotificationChannelSchema);
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import ApiError from "../../utils/ApiError.js";
|
||||
import { User, IUser, Role, IRole } from "../../db/v2/models/index.js";
|
||||
|
||||
const rolesCache = new Map<string, { roles: IRole[]; timestamp: number }>();
|
||||
// const CACHE_TTL = 30 * 60 * 1000; // 30 minutes
|
||||
const CACHE_TTL = 1; // 30 minutes
|
||||
const MAX_CACHE_SIZE = 1000;
|
||||
|
||||
const getCachedRoles = async (userId: string) => {
|
||||
if (rolesCache.size >= MAX_CACHE_SIZE) {
|
||||
const oldestKey = rolesCache.keys().next().value;
|
||||
if (!oldestKey) return null;
|
||||
rolesCache.delete(oldestKey);
|
||||
}
|
||||
|
||||
const cached = rolesCache.get(userId);
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.roles;
|
||||
}
|
||||
|
||||
const user: IUser | null = await User.findById(userId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const roles = await Role.find({ _id: { $in: user.roles } });
|
||||
rolesCache.set(userId, { roles, timestamp: Date.now() });
|
||||
return roles;
|
||||
};
|
||||
|
||||
const hasPermission = (roles: IRole[], requiredPermissions: string[]) => {
|
||||
const userPermissions = [...new Set(roles.flatMap((role) => role.permissions))];
|
||||
|
||||
if (userPermissions.includes("*")) return true;
|
||||
|
||||
const matches = (requiredPermission: string, userPermission: string) => {
|
||||
if (userPermission === requiredPermission) return true;
|
||||
if (userPermission.endsWith(".*")) {
|
||||
const prefix = userPermission.slice(0, -2);
|
||||
return requiredPermission.startsWith(prefix + ".");
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return requiredPermissions.every((requiredPermission) => {
|
||||
return userPermissions.some((userPermission) => matches(requiredPermission, userPermission));
|
||||
});
|
||||
};
|
||||
|
||||
const verifyPermission = (resourceActions: string[]) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const tokenizedUser = req.user;
|
||||
|
||||
if (!tokenizedUser) {
|
||||
throw new ApiError("No user", 400);
|
||||
}
|
||||
|
||||
const userId = tokenizedUser.sub;
|
||||
if (!userId) {
|
||||
throw new ApiError("No user ID", 400);
|
||||
}
|
||||
|
||||
const userRoles = await getCachedRoles(userId);
|
||||
if (!userRoles) {
|
||||
throw new ApiError("User roles not found", 400);
|
||||
}
|
||||
const allowed = hasPermission(userRoles, resourceActions);
|
||||
if (!allowed) {
|
||||
throw new ApiError("Insufficient permissions", 403);
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
export { verifyPermission };
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { decode } from "../../utils/JWTUtils.js";
|
||||
import ApiError from "../../utils/ApiError.js";
|
||||
const verifyToken = (req: Request, res: Response, next: NextFunction) => {
|
||||
const token = req.cookies.token;
|
||||
|
||||
if (!token) {
|
||||
const error = new ApiError("No token provided", 401);
|
||||
return next(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = decode(token);
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
export { verifyToken };
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import express from "express";
|
||||
import AuthController from "../../controllers/v2/AuthController.js";
|
||||
import { verifyToken } from "../../middleware/v2/VerifyToken.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
class AuthRoutes {
|
||||
private controller: AuthController;
|
||||
private router: Router;
|
||||
constructor(authController: AuthController) {
|
||||
this.controller = authController;
|
||||
this.router = Router();
|
||||
this.initRoutes();
|
||||
}
|
||||
|
||||
initRoutes = () => {
|
||||
this.router.post("/register", this.controller.register);
|
||||
this.router.post("/register/invite/:token", this.controller.registerWithInvite);
|
||||
this.router.post("/login", this.controller.login);
|
||||
this.router.post("/logout", this.controller.logout);
|
||||
this.router.get("/me", verifyToken, this.controller.me);
|
||||
this.router.post("/cleanup", this.controller.cleanup);
|
||||
this.router.post("/cleanup-monitors", this.controller.cleanMonitors);
|
||||
};
|
||||
|
||||
getRouter() {
|
||||
return this.router;
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthRoutes;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Router } from "express";
|
||||
import InviteController from "../../controllers/v2/InviteController.js";
|
||||
import { verifyToken } from "../../middleware/v2/VerifyToken.js";
|
||||
import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js";
|
||||
|
||||
class InviteRoutes {
|
||||
private router;
|
||||
private controller;
|
||||
constructor(inviteController: InviteController) {
|
||||
this.router = Router();
|
||||
this.controller = inviteController;
|
||||
this.initRoutes();
|
||||
}
|
||||
|
||||
initRoutes = () => {
|
||||
this.router.post("/", verifyToken, verifyPermission(["invite.create"]), this.controller.create);
|
||||
|
||||
this.router.get("/", verifyToken, verifyPermission(["invite.view"]), this.controller.getAll);
|
||||
|
||||
this.router.get("/:token", verifyToken, verifyPermission(["invite.view"]), this.controller.get);
|
||||
|
||||
this.router.delete("/:id", verifyToken, verifyPermission(["invite.delete"]), this.controller.delete);
|
||||
};
|
||||
|
||||
getRouter() {
|
||||
return this.router;
|
||||
}
|
||||
}
|
||||
|
||||
export default InviteRoutes;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Router } from "express";
|
||||
import MaintenanceController from "../../controllers/v2/MaintenanceController.js";
|
||||
import { verifyToken } from "../../middleware/v2/VerifyToken.js";
|
||||
import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js";
|
||||
|
||||
class MaintenanceRoutes {
|
||||
private router;
|
||||
private controller;
|
||||
constructor(maintenanceController: MaintenanceController) {
|
||||
this.router = Router();
|
||||
this.controller = maintenanceController;
|
||||
this.initRoutes();
|
||||
}
|
||||
|
||||
initRoutes = () => {
|
||||
this.router.post("/", verifyToken, verifyPermission(["maintenance.create"]), this.controller.create);
|
||||
|
||||
this.router.get("/", verifyToken, verifyPermission(["maintenance.view"]), this.controller.getAll);
|
||||
|
||||
this.router.patch("/:id/active", verifyToken, verifyPermission(["maintenance.update"]), this.controller.toggleActive);
|
||||
|
||||
this.router.patch("/:id", verifyToken, verifyPermission(["maintenance.update"]), this.controller.update);
|
||||
|
||||
this.router.get("/:id", verifyToken, verifyPermission(["maintenance.view"]), this.controller.get);
|
||||
|
||||
this.router.delete("/:id", verifyToken, verifyPermission(["maintenance.delete"]), this.controller.delete);
|
||||
};
|
||||
|
||||
getRouter() {
|
||||
return this.router;
|
||||
}
|
||||
}
|
||||
|
||||
export default MaintenanceRoutes;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Router } from "express";
|
||||
import MonitorController from "../../controllers/v2/MonitorController.js";
|
||||
import { verifyToken } from "../../middleware/v2/VerifyToken.js";
|
||||
import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js";
|
||||
|
||||
class MonitorRoutes {
|
||||
private router;
|
||||
private controller;
|
||||
constructor(monitorController: MonitorController) {
|
||||
this.router = Router();
|
||||
this.controller = monitorController;
|
||||
this.initRoutes();
|
||||
}
|
||||
|
||||
initRoutes = () => {
|
||||
this.router.post("/", verifyToken, verifyPermission(["monitors.create"]), this.controller.create);
|
||||
|
||||
this.router.get("/", verifyToken, verifyPermission(["monitors.view"]), this.controller.getAll);
|
||||
|
||||
this.router.get("/:id/checks", verifyToken, verifyPermission(["monitors.view"]), this.controller.getChecks);
|
||||
|
||||
this.router.patch("/:id/active", verifyToken, verifyPermission(["monitors.update"]), this.controller.toggleActive);
|
||||
|
||||
this.router.get("/:id", verifyToken, verifyPermission(["monitors.view"]), this.controller.get);
|
||||
|
||||
this.router.patch("/:id", verifyToken, verifyPermission(["monitors.update"]), this.controller.update);
|
||||
|
||||
this.router.delete("/:id", verifyToken, verifyPermission(["monitors.delete"]), this.controller.delete);
|
||||
};
|
||||
|
||||
getRouter() {
|
||||
return this.router;
|
||||
}
|
||||
}
|
||||
|
||||
export default MonitorRoutes;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Router } from "express";
|
||||
import NotificationController from "../../controllers/v2/NotificationChannelController.js";
|
||||
import { verifyToken } from "../../middleware/v2/VerifyToken.js";
|
||||
import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js";
|
||||
|
||||
class NotificationChannelRoutes {
|
||||
private router;
|
||||
private controller;
|
||||
constructor(notificationController: NotificationController) {
|
||||
this.router = Router();
|
||||
this.controller = notificationController;
|
||||
this.initRoutes();
|
||||
}
|
||||
|
||||
initRoutes = () => {
|
||||
this.router.post("/", verifyToken, verifyPermission(["notifications.create"]), this.controller.create);
|
||||
|
||||
this.router.get("/", verifyToken, verifyPermission(["notifications.view"]), this.controller.getAll);
|
||||
|
||||
this.router.patch("/:id/active", verifyToken, verifyPermission(["notifications.update"]), this.controller.toggleActive);
|
||||
|
||||
this.router.patch("/:id", verifyToken, verifyPermission(["notifications.update"]), this.controller.update);
|
||||
|
||||
this.router.get("/:id", verifyToken, verifyPermission(["notifications.view"]), this.controller.get);
|
||||
|
||||
this.router.delete("/:id", verifyToken, verifyPermission(["notifications.delete"]), this.controller.delete);
|
||||
};
|
||||
|
||||
getRouter() {
|
||||
return this.router;
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationChannelRoutes;
|
||||
@@ -1,24 +0,0 @@
|
||||
import QueueController from "../../controllers/v2/QueueController.js";
|
||||
import { Router } from "express";
|
||||
|
||||
class QueueRoutes {
|
||||
private router;
|
||||
private controller;
|
||||
constructor(queueController: QueueController) {
|
||||
this.router = Router();
|
||||
this.controller = queueController;
|
||||
this.initRoutes();
|
||||
}
|
||||
|
||||
initRoutes() {
|
||||
this.router.get("/jobs", this.controller.getJobs);
|
||||
this.router.get("/metrics", this.controller.getMetrics);
|
||||
this.router.post("/flush", this.controller.flush);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
return this.router;
|
||||
}
|
||||
}
|
||||
|
||||
export default QueueRoutes;
|
||||
@@ -1,210 +0,0 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import { User, Role, ITokenizedUser, Monitor, Check, NotificationChannel } from "../../../db/v2/models/index.js";
|
||||
import ApiError from "../../../utils/ApiError.js";
|
||||
import { Types } from "mongoose";
|
||||
import { IJobQueue } from "../infrastructure/JobQueue.js";
|
||||
|
||||
const SERVICE_NAME = "AuthServiceV2";
|
||||
|
||||
export const PERMISSIONS = {
|
||||
users: {
|
||||
all: "users.*",
|
||||
create: "users.create",
|
||||
view: "users.view",
|
||||
update: "users.update",
|
||||
delete: "users.delete",
|
||||
},
|
||||
|
||||
monitors: {
|
||||
all: "monitors.*",
|
||||
create: "monitors.create",
|
||||
view: "monitors.view",
|
||||
update: "monitors.update",
|
||||
delete: "monitors.delete",
|
||||
},
|
||||
notifications: {
|
||||
all: "notifications.*",
|
||||
create: "notifications.create",
|
||||
view: "notifications.view",
|
||||
update: "notifications.update",
|
||||
delete: "notifications.delete",
|
||||
},
|
||||
checks: {
|
||||
all: "checks.*",
|
||||
create: "checks.create",
|
||||
view: "checks.view",
|
||||
update: "checks.update",
|
||||
delete: "checks.delete",
|
||||
},
|
||||
statusPages: {
|
||||
all: "statusPages.*",
|
||||
create: "statusPages.create",
|
||||
view: "statusPages.view",
|
||||
update: "statusPages.update",
|
||||
delete: "statusPages.delete",
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_ROLES = [
|
||||
{
|
||||
name: "SuperAdmin",
|
||||
description: "Super admin with all permissions",
|
||||
permissions: ["*"],
|
||||
isSystem: true,
|
||||
},
|
||||
{
|
||||
name: "Admin",
|
||||
description: "Admin with full permissions",
|
||||
permissions: [PERMISSIONS.monitors.all, PERMISSIONS.users.all],
|
||||
isSystem: true,
|
||||
},
|
||||
{
|
||||
name: "Manager",
|
||||
description: "Can manage users",
|
||||
permissions: [PERMISSIONS.users.create, PERMISSIONS.users.update, PERMISSIONS.monitors.all],
|
||||
isSystem: true,
|
||||
},
|
||||
{
|
||||
name: "Member",
|
||||
description: "Basic team member",
|
||||
permissions: [PERMISSIONS.users.update, PERMISSIONS.monitors.create, PERMISSIONS.monitors.view, PERMISSIONS.monitors.update],
|
||||
isSystem: true,
|
||||
},
|
||||
];
|
||||
|
||||
export type RegisterData = {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
password: string;
|
||||
roles?: Types.ObjectId[]; // Optional roles for invite-based registration
|
||||
};
|
||||
|
||||
export type LoginData = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type AuthResult = ITokenizedUser;
|
||||
|
||||
export interface IAuthService {
|
||||
register(signupData: RegisterData): Promise<ITokenizedUser>;
|
||||
registerWithInvite(signupData: RegisterData): Promise<ITokenizedUser>;
|
||||
login(loginData: LoginData): Promise<ITokenizedUser>;
|
||||
cleanup(): Promise<void>;
|
||||
cleanMonitors(): Promise<void>;
|
||||
}
|
||||
|
||||
class AuthService implements IAuthService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private jobQueue: IJobQueue;
|
||||
constructor(jobQueue: IJobQueue) {
|
||||
this.jobQueue = jobQueue;
|
||||
}
|
||||
|
||||
async register(signupData: RegisterData): Promise<ITokenizedUser> {
|
||||
const userCount = await User.countDocuments();
|
||||
|
||||
if (userCount > 0) {
|
||||
throw new Error("Registration is closed. Please request an invite.");
|
||||
}
|
||||
|
||||
const { email, firstName, lastName, password } = signupData;
|
||||
|
||||
// Create all default roles
|
||||
const rolePromises = DEFAULT_ROLES.map((roleData) =>
|
||||
new Role({
|
||||
...roleData,
|
||||
}).save()
|
||||
);
|
||||
const roles = await Promise.all(rolePromises);
|
||||
|
||||
// Hash password and create user
|
||||
const saltRounds = 12;
|
||||
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
// Find admin role and assign to first user
|
||||
const superAdminRole = roles.find((role) => role.name === "SuperAdmin");
|
||||
|
||||
const user = new User({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
passwordHash,
|
||||
roles: [superAdminRole!._id],
|
||||
});
|
||||
|
||||
const savedUser = await user.save();
|
||||
return {
|
||||
sub: savedUser._id.toString(),
|
||||
roles: savedUser.roles.map((role) => role.toString()),
|
||||
};
|
||||
}
|
||||
|
||||
async registerWithInvite(signupData: RegisterData): Promise<ITokenizedUser> {
|
||||
const { email, firstName, lastName, password, roles } = signupData;
|
||||
|
||||
const saltRounds = 12;
|
||||
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||
const user = new User({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
passwordHash,
|
||||
roles: roles || [],
|
||||
});
|
||||
try {
|
||||
const savedUser = await user.save();
|
||||
return {
|
||||
sub: savedUser._id.toString(),
|
||||
roles: savedUser.roles.map((role) => role.toString()),
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error?.code === 11000) {
|
||||
const dupError = new ApiError("Email already in use", 409);
|
||||
dupError.stack = error?.stack;
|
||||
throw dupError;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async login(loginData: LoginData): Promise<ITokenizedUser> {
|
||||
const { email, password } = loginData;
|
||||
|
||||
// Find user by email
|
||||
const user = await User.findOne({ email });
|
||||
|
||||
if (!user) {
|
||||
throw new Error("Invalid email or password");
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!isPasswordValid) {
|
||||
throw new Error("Invalid email or password");
|
||||
}
|
||||
|
||||
return {
|
||||
sub: user._id.toString(),
|
||||
roles: user.roles.map((role) => role.toString()),
|
||||
};
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
await User.deleteMany({});
|
||||
await Role.deleteMany({});
|
||||
await Monitor.deleteMany({});
|
||||
await Check.deleteMany({});
|
||||
await NotificationChannel.deleteMany({});
|
||||
await this.jobQueue.flush();
|
||||
}
|
||||
|
||||
async cleanMonitors() {
|
||||
await Monitor.deleteMany({});
|
||||
await Check.deleteMany({});
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthService;
|
||||
@@ -1,147 +0,0 @@
|
||||
import { json } from "stream/consumers";
|
||||
import { ICheck, Check, Monitor } from "../../../db/v2/models/index.js";
|
||||
import type { ISystemInfo, ICaptureInfo } from "../../../db/v2/models/index.js";
|
||||
import { MonitorType } from "../../../db/v2/models/monitors/Monitor.js";
|
||||
import { StatusResponse } from "../infrastructure/NetworkService.js";
|
||||
import type { ICapturePayload, ILighthousePayload } from "../infrastructure/NetworkService.js";
|
||||
import mongoose from "mongoose";
|
||||
import { stat } from "fs";
|
||||
|
||||
const SERVICE_NAME = "CheckServiceV2";
|
||||
export interface ICheckService {
|
||||
buildCheck: (statusResponse: StatusResponse, type: MonitorType) => Promise<ICheck>;
|
||||
cleanupOrphanedChecks: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
class CheckService implements ICheckService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private isCapturePayload = (payload: any): payload is ICapturePayload => {
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
|
||||
if (!("data" in payload) || typeof payload.data !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = payload.data as Partial<ISystemInfo>;
|
||||
if (!data.cpu || typeof data.cpu !== "object" || typeof data.cpu.usage_percent !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!data.memory || typeof data.memory !== "object" || typeof data.memory.usage_percent !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.disk && !Array.isArray(data.disk)) {
|
||||
return false;
|
||||
}
|
||||
if (data.net && !Array.isArray(data.net)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!("capture" in payload) || typeof payload.capture !== "object") return false;
|
||||
const capture = payload.capture as Record<string, any>;
|
||||
if (typeof capture.version !== "string" || typeof capture.mode !== "string") return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
private isPagespeedPayload = (payload: any): payload is ILighthousePayload => {
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
|
||||
if (!("lighthouseResult" in payload) || typeof payload.lighthouseResult !== "object") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private buildBaseCheck = (statusResponse: StatusResponse) => {
|
||||
const monitorId = new mongoose.Types.ObjectId(statusResponse.monitorId);
|
||||
const check = new Check({
|
||||
monitorId: monitorId,
|
||||
type: statusResponse?.type,
|
||||
status: statusResponse?.status,
|
||||
httpStatusCode: statusResponse?.code,
|
||||
message: statusResponse?.message,
|
||||
responseTime: statusResponse?.responseTime,
|
||||
timings: statusResponse?.timings,
|
||||
});
|
||||
return check;
|
||||
};
|
||||
|
||||
private buildInfrastructureCheck = (statusResponse: StatusResponse<ICapturePayload>) => {
|
||||
if (!this.isCapturePayload(statusResponse.payload)) {
|
||||
throw new Error("Invalid payload for infrastructure monitor");
|
||||
}
|
||||
const check = this.buildBaseCheck(statusResponse);
|
||||
check.system = statusResponse.payload.data;
|
||||
check.capture = statusResponse.payload.capture;
|
||||
return check;
|
||||
};
|
||||
|
||||
private buildPagespeedCheck = (statusResponse: StatusResponse<ILighthousePayload>) => {
|
||||
if (!this.isPagespeedPayload(statusResponse.payload)) {
|
||||
throw new Error("Invalid payload for pagespeed monitor");
|
||||
}
|
||||
const check = this.buildBaseCheck(statusResponse);
|
||||
const lighthouseResult = statusResponse?.payload?.lighthouseResult;
|
||||
check.lighthouse = {
|
||||
accessibility: lighthouseResult?.categories?.accessibility?.score || 0,
|
||||
bestPractices: lighthouseResult?.categories?.["best-practices"]?.score || 0,
|
||||
seo: lighthouseResult?.categories?.seo?.score || 0,
|
||||
performance: lighthouseResult?.categories?.performance?.score || 0,
|
||||
audits: {
|
||||
cls: lighthouseResult?.audits?.["cumulative-layout-shift"] || {},
|
||||
si: lighthouseResult?.audits?.["speed-index"] || {},
|
||||
fcp: lighthouseResult?.audits?.["first-contentful-paint"] || {},
|
||||
lcp: lighthouseResult?.audits?.["largest-contentful-paint"] || {},
|
||||
tbt: lighthouseResult?.audits?.["total-blocking-time"] || {},
|
||||
},
|
||||
};
|
||||
return check;
|
||||
};
|
||||
|
||||
buildCheck = async (statusResponse: StatusResponse, type: MonitorType): Promise<ICheck> => {
|
||||
switch (type) {
|
||||
case "infrastructure":
|
||||
return this.buildInfrastructureCheck(statusResponse as StatusResponse<ICapturePayload>);
|
||||
|
||||
case "pagespeed":
|
||||
return this.buildPagespeedCheck(statusResponse as StatusResponse<ILighthousePayload>);
|
||||
case "http":
|
||||
case "https":
|
||||
return this.buildBaseCheck(statusResponse);
|
||||
|
||||
case "ping":
|
||||
return this.buildBaseCheck(statusResponse);
|
||||
default:
|
||||
throw new Error(`Unsupported monitor type: ${type}`);
|
||||
}
|
||||
};
|
||||
|
||||
cleanupOrphanedChecks = async () => {
|
||||
try {
|
||||
const monitorIds = await Monitor.find().distinct("_id");
|
||||
const result = await Check.deleteMany({
|
||||
monitorId: { $nin: monitorIds },
|
||||
});
|
||||
console.log(`Deleted ${result.deletedCount} orphaned Checks.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up orphaned Checks:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
getChecks = async (monitorId: string, page: number, rowsPerPage: number) => {
|
||||
const count = await Check.countDocuments({ monitorId: new mongoose.Types.ObjectId(monitorId) });
|
||||
const checks = await Check.find({ monitorId: new mongoose.Types.ObjectId(monitorId) })
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(page * rowsPerPage)
|
||||
.limit(rowsPerPage)
|
||||
.exec();
|
||||
return { checks, count };
|
||||
};
|
||||
}
|
||||
|
||||
export default CheckService;
|
||||
@@ -1,63 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
import { ITokenizedUser, IInvite, Invite } from "../../../db/v2/models/index.js";
|
||||
import ApiError from "../../../utils/ApiError.js";
|
||||
|
||||
const SERVICE_NAME = "InviteServiceV2";
|
||||
export interface IInviteService {
|
||||
create: (tokenizedUser: ITokenizedUser, invite: IInvite) => Promise<{ token: string }>;
|
||||
getAll: () => Promise<IInvite[]>;
|
||||
get: (tokenHash: string) => Promise<IInvite>;
|
||||
delete: (id: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
class InviteService implements IInviteService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor() {}
|
||||
|
||||
create = async (tokenizedUser: ITokenizedUser, inviteData: IInvite) => {
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
|
||||
try {
|
||||
const invite = await Invite.create({
|
||||
...inviteData,
|
||||
tokenHash,
|
||||
createdBy: tokenizedUser.sub,
|
||||
updatedBy: tokenizedUser.sub,
|
||||
});
|
||||
if (!invite) {
|
||||
throw new ApiError("Failed to create invite", 500);
|
||||
}
|
||||
return { token };
|
||||
} catch (error: any) {
|
||||
if (error?.code === 11000) {
|
||||
const dupError = new ApiError("Invite with this email already exists", 409);
|
||||
dupError.stack = error?.stack;
|
||||
throw dupError;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
get = async (token: string) => {
|
||||
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
|
||||
const invite = await Invite.findOne({ tokenHash });
|
||||
if (!invite) {
|
||||
throw new ApiError("Invite not found", 404);
|
||||
}
|
||||
return invite;
|
||||
};
|
||||
|
||||
getAll = async () => {
|
||||
return Invite.find();
|
||||
};
|
||||
|
||||
delete = async (id: string) => {
|
||||
const result = await Invite.deleteOne({ _id: id });
|
||||
if (!result.deletedCount) {
|
||||
throw new ApiError("Invite not found", 404);
|
||||
}
|
||||
return result.deletedCount === 1;
|
||||
};
|
||||
}
|
||||
|
||||
export default InviteService;
|
||||
@@ -1,144 +0,0 @@
|
||||
import { ITokenizedUser, IMaintenance, Maintenance } from "../../../db/v2/models/index.js";
|
||||
import ApiError from "../../../utils/ApiError.js";
|
||||
|
||||
const SERVICE_NAME = "MaintenanceServiceV2";
|
||||
|
||||
export interface IMaintenanceService {
|
||||
create: (
|
||||
tokenizedUser: ITokenizedUser,
|
||||
|
||||
maintenance: IMaintenance
|
||||
) => Promise<IMaintenance>;
|
||||
getAll: () => Promise<IMaintenance[]>;
|
||||
get: (id: string) => Promise<IMaintenance>;
|
||||
toggleActive: (tokenizedUser: ITokenizedUser, id: string) => Promise<IMaintenance>;
|
||||
update: (tokenizedUser: ITokenizedUser, id: string, updateData: Partial<IMaintenance>) => Promise<IMaintenance>;
|
||||
delete: (id: string) => Promise<boolean>;
|
||||
isInMaintenance: (monitorId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
type MaintenanceCache = Map<string, IMaintenance[]>;
|
||||
|
||||
class MaintenanceService implements IMaintenanceService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private maintenanceCache: MaintenanceCache;
|
||||
private lastRefresh: number;
|
||||
private CACHE_TTL_MS = 60 * 1000;
|
||||
|
||||
constructor() {
|
||||
this.maintenanceCache = new Map();
|
||||
this.lastRefresh = 0;
|
||||
}
|
||||
|
||||
create = async (tokenizedUser: ITokenizedUser, maintenanceData: IMaintenance) => {
|
||||
const maintenance = await Maintenance.create({
|
||||
...maintenanceData,
|
||||
createdBy: tokenizedUser.sub,
|
||||
updatedBy: tokenizedUser.sub,
|
||||
});
|
||||
return maintenance;
|
||||
};
|
||||
|
||||
get = async (id: string) => {
|
||||
const maintenance = await Maintenance.findById(id);
|
||||
if (!maintenance) {
|
||||
throw new ApiError("Maintenance not found", 404);
|
||||
}
|
||||
return maintenance;
|
||||
};
|
||||
|
||||
getAll = async () => {
|
||||
return Maintenance.find();
|
||||
};
|
||||
|
||||
toggleActive = async (tokenizedUser: ITokenizedUser, id: string) => {
|
||||
const updatedMaintenance = await Maintenance.findOneAndUpdate(
|
||||
{ _id: id },
|
||||
[
|
||||
{
|
||||
$set: {
|
||||
isActive: { $not: "$isActive" },
|
||||
updatedBy: tokenizedUser.sub,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
],
|
||||
{ new: true }
|
||||
);
|
||||
if (!updatedMaintenance) {
|
||||
throw new ApiError("Maintenance not found", 404);
|
||||
}
|
||||
return updatedMaintenance;
|
||||
};
|
||||
|
||||
update = async (tokenizedUser: ITokenizedUser, id: string, updateData: Partial<IMaintenance>) => {
|
||||
const allowedFields: (keyof IMaintenance)[] = ["name", "monitors", "startTime", "endTime", "isActive"];
|
||||
const safeUpdate: Partial<IMaintenance> = {};
|
||||
for (const field of allowedFields) {
|
||||
if (updateData[field] !== undefined) {
|
||||
(safeUpdate as any)[field] = updateData[field];
|
||||
}
|
||||
}
|
||||
|
||||
const updatedMaintenance = await Maintenance.findByIdAndUpdate(
|
||||
id,
|
||||
{
|
||||
$set: {
|
||||
...safeUpdate,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: tokenizedUser.sub,
|
||||
},
|
||||
},
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!updatedMaintenance) {
|
||||
throw new ApiError("Failed to update maintenance", 500);
|
||||
}
|
||||
|
||||
return updatedMaintenance;
|
||||
};
|
||||
|
||||
delete = async (id: string) => {
|
||||
const result = await Maintenance.deleteOne({ _id: id });
|
||||
if (!result.deletedCount) {
|
||||
throw new ApiError("Maintenance not found", 404);
|
||||
}
|
||||
return result.deletedCount === 1;
|
||||
};
|
||||
|
||||
private refreshCache = async () => {
|
||||
const now = new Date();
|
||||
|
||||
const activeMaintenances = await Maintenance.find({
|
||||
isActive: true,
|
||||
startTime: { $lte: now },
|
||||
endTime: { $gte: now },
|
||||
}).lean();
|
||||
|
||||
// Reset cache
|
||||
const newCache = new Map();
|
||||
|
||||
for (const m of activeMaintenances) {
|
||||
for (const monitorId of m.monitors) {
|
||||
const key = monitorId.toString();
|
||||
if (!newCache.has(key)) newCache.set(key, []);
|
||||
newCache.get(key)!.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
this.maintenanceCache = newCache;
|
||||
this.lastRefresh = Date.now();
|
||||
};
|
||||
|
||||
isInMaintenance = async (monitorId: string) => {
|
||||
const now = Date.now();
|
||||
if (now - this.lastRefresh > this.CACHE_TTL_MS) {
|
||||
await this.refreshCache();
|
||||
}
|
||||
const maintenances = this.maintenanceCache.get(monitorId) || [];
|
||||
return maintenances.length > 0;
|
||||
};
|
||||
}
|
||||
|
||||
export default MaintenanceService;
|
||||
@@ -1,468 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
import { IMonitor, Monitor, ITokenizedUser, MonitorStats, Check } from "../../../db/v2/models/index.js";
|
||||
import ApiError from "../../../utils/ApiError.js";
|
||||
import { IJobQueue } from "../infrastructure/JobQueue.js";
|
||||
import { MonitorWithChecksResponse } from "../../../types/index.js";
|
||||
import { MonitorStatus, MonitorType } from "../../../db/v2/models/monitors/Monitor.js";
|
||||
|
||||
const SERVICE_NAME = "MonitorServiceV2";
|
||||
|
||||
export interface IMonitorService {
|
||||
create: (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => Promise<IMonitor>;
|
||||
getAll: () => Promise<IMonitor[]>;
|
||||
getAllEmbedChecks: (page: number, limit: number, type: MonitorType[]) => Promise<any[]>;
|
||||
get: (monitorId: string) => Promise<IMonitor>;
|
||||
getEmbedChecks: (monitorId: string, range: string, status?: string) => Promise<MonitorWithChecksResponse>;
|
||||
toggleActive: (monitorId: string, tokenizedUser: ITokenizedUser) => Promise<IMonitor>;
|
||||
update: (tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial<IMonitor>) => Promise<IMonitor>;
|
||||
delete: (monitorId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
class MonitorService implements IMonitorService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private jobQueue: IJobQueue;
|
||||
constructor(jobQueue: IJobQueue) {
|
||||
this.jobQueue = jobQueue;
|
||||
}
|
||||
|
||||
create = async (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => {
|
||||
const monitor = await Monitor.create({
|
||||
...monitorData,
|
||||
createdBy: tokenizedUser.sub,
|
||||
updatedBy: tokenizedUser.sub,
|
||||
});
|
||||
await MonitorStats.create({
|
||||
monitorId: monitor._id,
|
||||
currentStreakStartedAt: Date.now(),
|
||||
});
|
||||
await this.jobQueue.addJob(monitor);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
getAll = async () => {
|
||||
return Monitor.find();
|
||||
};
|
||||
|
||||
getAllEmbedChecks = async (page: number, limit: number, type: MonitorType[] = []) => {
|
||||
const skip = (page - 1) * limit;
|
||||
let find = {};
|
||||
if (type.length > 0) find = { type: { $in: type } };
|
||||
const monitors = await Monitor.find(find).skip(skip).limit(limit);
|
||||
return monitors;
|
||||
};
|
||||
|
||||
get = async (monitorId: string) => {
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (!monitor) {
|
||||
throw new ApiError("Monitor not found", 404);
|
||||
}
|
||||
return monitor;
|
||||
};
|
||||
|
||||
private getStartDate(range: string): Date {
|
||||
const now = new Date();
|
||||
switch (range) {
|
||||
case "2h":
|
||||
return new Date(now.getTime() - 2 * 60 * 60 * 1000);
|
||||
case "24h":
|
||||
return new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
case "7d":
|
||||
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
case "30d":
|
||||
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
default:
|
||||
throw new ApiError("Invalid range parameter", 400);
|
||||
}
|
||||
}
|
||||
|
||||
private getDateFormat(range: string): string {
|
||||
switch (range) {
|
||||
case "2h":
|
||||
return "%Y-%m-%dT%H:%M:00Z";
|
||||
case "24h":
|
||||
case "7d":
|
||||
return "%Y-%m-%dT%H:00:00Z";
|
||||
case "30d":
|
||||
return "%Y-%m-%d";
|
||||
default:
|
||||
throw new ApiError("Invalid range parameter", 400);
|
||||
}
|
||||
}
|
||||
|
||||
private getBaseGroup = (dateFormat: string): Record<string, any> => {
|
||||
return {
|
||||
_id: { $dateToString: { format: dateFormat, date: "$createdAt" } },
|
||||
count: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
};
|
||||
};
|
||||
|
||||
private getBaseProjection = (): object => {
|
||||
return { status: 1, responseTime: 1, createdAt: 1 };
|
||||
};
|
||||
|
||||
private getPageSpeedGroup = (dateFormat: string): Record<string, any> => {
|
||||
return {
|
||||
_id: { $dateToString: { format: dateFormat, date: "$createdAt" } },
|
||||
count: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
accessibility: { $avg: "$lighthouse.accessibility" },
|
||||
bestPractices: { $avg: "$lighthouse.bestPractices" },
|
||||
seo: { $avg: "$lighthouse.seo" },
|
||||
performance: { $avg: "$lighthouse.performance" },
|
||||
cls: { $avg: "$lighthouse.audits.cls.score" },
|
||||
si: { $avg: "$lighthouse.audits.si.score" },
|
||||
fcp: { $avg: "$lighthouse.audits.fcp.score" },
|
||||
lcp: { $avg: "$lighthouse.audits.lcp.score" },
|
||||
tbt: { $avg: "$lighthouse.audits.tbt.score" },
|
||||
};
|
||||
};
|
||||
|
||||
private getPageSpeedProjection = (): object => {
|
||||
const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 };
|
||||
projectStage["lighthouse.accessibility"] = 1;
|
||||
projectStage["lighthouse.seo"] = 1;
|
||||
projectStage["lighthouse.bestPractices"] = 1;
|
||||
projectStage["lighthouse.performance"] = 1;
|
||||
projectStage["lighthouse.audits.cls.score"] = 1;
|
||||
projectStage["lighthouse.audits.si.score"] = 1;
|
||||
projectStage["lighthouse.audits.fcp.score"] = 1;
|
||||
projectStage["lighthouse.audits.lcp.score"] = 1;
|
||||
projectStage["lighthouse.audits.tbt.score"] = 1;
|
||||
return projectStage;
|
||||
};
|
||||
|
||||
private getInfraGroup = (dateFormat: string): Record<string, any> => {
|
||||
return {
|
||||
_id: { $dateToString: { format: dateFormat, date: "$createdAt" } },
|
||||
count: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
physicalCores: { $last: "$system.cpu.physical_core" },
|
||||
logicalCores: { $last: "$system.cpu.logical_core" },
|
||||
frequency: { $avg: "$system.cpu.frequency" },
|
||||
currentFrequency: { $last: "$system.cpu.current_frequency" },
|
||||
tempsArrays: { $push: "$system.cpu.temperature" },
|
||||
freePercent: { $avg: "$system.cpu.free_percent" },
|
||||
usedPercent: { $avg: "$system.cpu.usage_percent" },
|
||||
total_bytes: { $last: "$system.memory.total_bytes" },
|
||||
available_bytes: { $last: "$system.memory.available_bytes" },
|
||||
used_bytes: { $last: "$system.memory.used_bytes" },
|
||||
memory_usage_percent: { $avg: "$system.memory.usage_percent" },
|
||||
disksArray: { $push: "$system.disk" },
|
||||
os: { $last: "$system.host.os" },
|
||||
platform: { $last: "$system.host.platform" },
|
||||
kernel_version: { $last: "$system.host.kernel_version" },
|
||||
pretty_name: { $last: "$system.host.pretty_name" },
|
||||
netsArray: { $push: "$system.net" },
|
||||
};
|
||||
};
|
||||
|
||||
private getInfraProjection = (): object => {
|
||||
const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 };
|
||||
projectStage["system.cpu.physical_core"] = 1;
|
||||
projectStage["system.cpu.logical_core"] = 1;
|
||||
projectStage["system.cpu.frequency"] = 1;
|
||||
projectStage["system.cpu.current_frequency"] = 1;
|
||||
projectStage["system.cpu.temperature"] = 1;
|
||||
projectStage["system.cpu.free_percent"] = 1;
|
||||
projectStage["system.cpu.usage_percent"] = 1;
|
||||
projectStage["system.memory.total_bytes"] = 1;
|
||||
projectStage["system.memory.available_bytes"] = 1;
|
||||
projectStage["system.memory.used_bytes"] = 1;
|
||||
projectStage["system.memory.usage_percent"] = 1;
|
||||
projectStage["system.disk"] = 1;
|
||||
projectStage["system.host.os"] = 1;
|
||||
projectStage["system.host.platform"] = 1;
|
||||
projectStage["system.host.kernel_version"] = 1;
|
||||
projectStage["system.host.pretty_name"] = 1;
|
||||
projectStage["system.net"] = 1;
|
||||
return projectStage;
|
||||
};
|
||||
|
||||
private getFinalProjection = (type: string): object => {
|
||||
if (type === "pagespeed") {
|
||||
return {
|
||||
_id: 1,
|
||||
count: 1,
|
||||
avgResponseTime: 1,
|
||||
accessibility: "$accessibility",
|
||||
seo: "$seo",
|
||||
bestPractices: "$bestPractices",
|
||||
performance: "$performance",
|
||||
cls: "$cls",
|
||||
si: "$si",
|
||||
fcp: "$fcp",
|
||||
lcp: "$lcp",
|
||||
tbt: "$tbt",
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "infrastructure") {
|
||||
return {
|
||||
_id: 1,
|
||||
count: 1,
|
||||
avgResponseTime: 1,
|
||||
cpu: {
|
||||
physicalCores: "$physicalCores",
|
||||
logicalCores: "$logicalCores",
|
||||
frequency: "$frequency",
|
||||
currentFrequency: "$currentFrequency",
|
||||
temperatures: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [0, { $size: { $arrayElemAt: ["$tempsArrays", 0] } }],
|
||||
},
|
||||
as: "idx",
|
||||
in: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$tempsArrays",
|
||||
as: "arr",
|
||||
in: { $arrayElemAt: ["$$arr", "$$idx"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
freePercent: "$freePercent",
|
||||
usedPercent: "$usedPercent",
|
||||
},
|
||||
memory: {
|
||||
total_bytes: "$total_bytes",
|
||||
available_bytes: "$available_bytes",
|
||||
used_bytes: "$used_bytes",
|
||||
usage_percent: "$memory_usage_percent",
|
||||
},
|
||||
disks: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [0, { $size: { $arrayElemAt: ["$disksArray", 0] } }],
|
||||
},
|
||||
as: "idx",
|
||||
in: {
|
||||
$let: {
|
||||
vars: {
|
||||
diskGroup: {
|
||||
$map: {
|
||||
input: "$disksArray",
|
||||
as: "diskArr",
|
||||
in: { $arrayElemAt: ["$$diskArr", "$$idx"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
in: {
|
||||
device: { $arrayElemAt: ["$$diskGroup.device", 0] },
|
||||
total_bytes: { $avg: "$$diskGroup.total_bytes" },
|
||||
free_bytes: { $avg: "$$diskGroup.free_bytes" },
|
||||
used_bytes: { $avg: "$$diskGroup.used_bytes" },
|
||||
usage_percent: { $avg: "$$diskGroup.usage_percent" },
|
||||
total_inodes: { $avg: "$$diskGroup.total_inodes" },
|
||||
free_inodes: { $avg: "$$diskGroup.free_inodes" },
|
||||
used_inodes: { $avg: "$$diskGroup.used_inodes" },
|
||||
inodes_usage_percent: {
|
||||
$avg: "$$diskGroup.inodes_usage_percent",
|
||||
},
|
||||
read_bytes: { $avg: "$$diskGroup.read_bytes" },
|
||||
write_bytes: { $avg: "$$diskGroup.write_bytes" },
|
||||
read_time: { $avg: "$$diskGroup.read_time" },
|
||||
write_time: { $avg: "$$diskGroup.write_time" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
host: {
|
||||
os: "$os",
|
||||
platform: "$platform",
|
||||
kernel_version: "$kernel_version",
|
||||
pretty_name: "$pretty_name",
|
||||
},
|
||||
net: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [0, { $size: { $arrayElemAt: ["$netsArray", 0] } }],
|
||||
},
|
||||
as: "idx",
|
||||
in: {
|
||||
$let: {
|
||||
vars: {
|
||||
netGroup: {
|
||||
$map: {
|
||||
input: "$netsArray",
|
||||
as: "netArr",
|
||||
in: { $arrayElemAt: ["$$netArr", "$$idx"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
in: {
|
||||
name: { $arrayElemAt: ["$$netGroup.name", 0] },
|
||||
bytes_sent: { $avg: "$$netGroup.bytes_sent" },
|
||||
bytes_recv: { $avg: "$$netGroup.bytes_recv" },
|
||||
packets_sent: { $avg: "$$netGroup.packets_sent" },
|
||||
packets_recv: { $avg: "$$netGroup.packets_recv" },
|
||||
err_in: { $avg: "$$netGroup.err_in" },
|
||||
err_out: { $avg: "$$netGroup.err_out" },
|
||||
drop_in: { $avg: "$$netGroup.drop_in" },
|
||||
drop_out: { $avg: "$$netGroup.drop_out" },
|
||||
fifo_in: { $avg: "$$netGroup.fifo_in" },
|
||||
fifo_out: { $avg: "$$netGroup.fifo_out" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
getEmbedChecks = async (monitorId: string, range: string, status: string | undefined): Promise<MonitorWithChecksResponse> => {
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (!monitor) {
|
||||
throw new ApiError("Monitor not found", 404);
|
||||
}
|
||||
const startDate = this.getStartDate(range);
|
||||
const dateFormat = this.getDateFormat(range);
|
||||
|
||||
// Build match stage
|
||||
const matchStage: {
|
||||
monitorId: mongoose.Types.ObjectId;
|
||||
createdAt: { $gte: Date };
|
||||
status?: string;
|
||||
} = {
|
||||
monitorId: monitor._id,
|
||||
createdAt: { $gte: startDate },
|
||||
};
|
||||
|
||||
if (status) {
|
||||
matchStage.status = status;
|
||||
}
|
||||
|
||||
let groupClause;
|
||||
|
||||
if (monitor.type === "pagespeed") {
|
||||
groupClause = this.getPageSpeedGroup(dateFormat);
|
||||
} else if (monitor.type === "infrastructure") {
|
||||
groupClause = this.getInfraGroup(dateFormat);
|
||||
} else {
|
||||
groupClause = this.getBaseGroup(dateFormat);
|
||||
}
|
||||
|
||||
let projectStage;
|
||||
if (monitor.type === "pagespeed") {
|
||||
projectStage = this.getPageSpeedProjection();
|
||||
} else if (monitor.type === "infrastructure") {
|
||||
projectStage = this.getInfraProjection();
|
||||
} else {
|
||||
projectStage = this.getBaseProjection();
|
||||
}
|
||||
|
||||
let finalProjection = {};
|
||||
if (monitor.type === "pagespeed" || monitor.type === "infrastructure") {
|
||||
finalProjection = this.getFinalProjection(monitor.type);
|
||||
} else {
|
||||
finalProjection = { _id: 1, count: 1, avgResponseTime: 1 };
|
||||
}
|
||||
|
||||
const checks = await Check.aggregate([
|
||||
{
|
||||
$match: matchStage,
|
||||
},
|
||||
{ $sort: { createdAt: 1 } },
|
||||
{ $project: projectStage },
|
||||
{ $group: groupClause },
|
||||
{ $sort: { _id: -1 } },
|
||||
{
|
||||
$project: finalProjection,
|
||||
},
|
||||
]);
|
||||
|
||||
// Get monitor stats
|
||||
const monitorStats = await MonitorStats.findOne({
|
||||
monitorId: monitor._id,
|
||||
});
|
||||
|
||||
if (!monitorStats) {
|
||||
throw new ApiError("Monitor stats not found", 404);
|
||||
}
|
||||
|
||||
return {
|
||||
monitor: monitor.toObject(),
|
||||
checks,
|
||||
stats: monitorStats,
|
||||
};
|
||||
};
|
||||
|
||||
async toggleActive(id: string, tokenizedUser: ITokenizedUser) {
|
||||
const pendingStatus: MonitorStatus = "initializing";
|
||||
const updatedMonitor = await Monitor.findOneAndUpdate(
|
||||
{ _id: id },
|
||||
[
|
||||
{
|
||||
$set: {
|
||||
isActive: { $not: "$isActive" },
|
||||
status: pendingStatus,
|
||||
updatedBy: tokenizedUser.sub,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
],
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!updatedMonitor) {
|
||||
throw new ApiError("Monitor not found", 404);
|
||||
}
|
||||
|
||||
await this.jobQueue.updateJob(updatedMonitor);
|
||||
|
||||
if (updatedMonitor?.isActive) {
|
||||
await this.jobQueue.resumeJob(updatedMonitor);
|
||||
} else {
|
||||
await this.jobQueue.pauseJob(updatedMonitor);
|
||||
}
|
||||
return updatedMonitor;
|
||||
}
|
||||
|
||||
async update(tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial<IMonitor>) {
|
||||
const allowedFields: (keyof IMonitor)[] = ["name", "interval", "isActive", "n", "notificationChannels"];
|
||||
const safeUpdate: Partial<IMonitor> = {};
|
||||
|
||||
for (const field of allowedFields) {
|
||||
if (updateData[field] !== undefined) {
|
||||
(safeUpdate as any)[field] = updateData[field];
|
||||
}
|
||||
}
|
||||
|
||||
const updatedMonitor = await Monitor.findByIdAndUpdate(
|
||||
monitorId,
|
||||
{
|
||||
$set: {
|
||||
...safeUpdate,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: tokenizedUser.sub,
|
||||
},
|
||||
},
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!updatedMonitor) {
|
||||
throw new ApiError("Monitor not found", 404);
|
||||
}
|
||||
await this.jobQueue.updateJob(updatedMonitor);
|
||||
return updatedMonitor;
|
||||
}
|
||||
|
||||
async delete(monitorId: string) {
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (!monitor) {
|
||||
throw new ApiError("Monitor not found", 404);
|
||||
}
|
||||
await monitor.deleteOne();
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default MonitorService;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { MonitorStats } from "../../../db/v2/models/index.js";
|
||||
import { Monitor } from "../../../db/v2/models/index.js";
|
||||
|
||||
const SERVICE_NAME = "MonitorStatsServiceV2";
|
||||
export interface IMonitorStatsService {
|
||||
cleanupOrphanedMonitorStats: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
class MonitorStatsService implements IMonitorStatsService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor() {}
|
||||
|
||||
async cleanupOrphanedMonitorStats() {
|
||||
try {
|
||||
const monitorIds = await Monitor.find().distinct("_id");
|
||||
const result = await MonitorStats.deleteMany({
|
||||
monitorId: { $nin: monitorIds },
|
||||
});
|
||||
console.log(`Deleted ${result.deletedCount} orphaned MonitorStats.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up orphaned MonitorStats:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MonitorStatsService;
|
||||
@@ -1,104 +0,0 @@
|
||||
import { ITokenizedUser, INotificationChannel, NotificationChannel, Monitor } from "../../../db/v2/models/index.js";
|
||||
import ApiError from "../../../utils/ApiError.js";
|
||||
|
||||
const SERVICE_NAME = "NotificationChannelServiceV2";
|
||||
|
||||
export interface INotificationChannelService {
|
||||
create: (
|
||||
tokenizedUser: ITokenizedUser,
|
||||
|
||||
notificationChannel: INotificationChannel
|
||||
) => Promise<INotificationChannel>;
|
||||
getAll: () => Promise<INotificationChannel[]>;
|
||||
get: (id: string) => Promise<INotificationChannel>;
|
||||
toggleActive: (tokenizedUser: ITokenizedUser, id: string) => Promise<INotificationChannel>;
|
||||
update: (tokenizedUser: ITokenizedUser, id: string, updateData: Partial<INotificationChannel>) => Promise<INotificationChannel>;
|
||||
delete: (id: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
class NotificationChannelService implements INotificationChannelService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor() {}
|
||||
|
||||
create = async (tokenizedUser: ITokenizedUser, notificationChannelData: INotificationChannel) => {
|
||||
const notificationChannel = await NotificationChannel.create({
|
||||
...notificationChannelData,
|
||||
createdBy: tokenizedUser.sub,
|
||||
updatedBy: tokenizedUser.sub,
|
||||
});
|
||||
return notificationChannel;
|
||||
};
|
||||
|
||||
get = async (id: string) => {
|
||||
const channel = await NotificationChannel.findById(id);
|
||||
if (!channel) {
|
||||
throw new ApiError("Notification channel not found", 404);
|
||||
}
|
||||
return channel;
|
||||
};
|
||||
|
||||
getAll = async () => {
|
||||
return NotificationChannel.find();
|
||||
};
|
||||
|
||||
toggleActive = async (tokenizedUser: ITokenizedUser, id: string) => {
|
||||
const updatedChannel = await NotificationChannel.findOneAndUpdate(
|
||||
{ _id: id },
|
||||
[
|
||||
{
|
||||
$set: {
|
||||
isActive: { $not: "$isActive" },
|
||||
updatedBy: tokenizedUser.sub,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
],
|
||||
{ new: true }
|
||||
);
|
||||
if (!updatedChannel) {
|
||||
throw new ApiError("Notification channel not found", 404);
|
||||
}
|
||||
return updatedChannel;
|
||||
};
|
||||
|
||||
update = async (tokenizedUser: ITokenizedUser, id: string, updateData: Partial<INotificationChannel>) => {
|
||||
const allowedFields: (keyof INotificationChannel)[] = ["name", "config", "isActive"];
|
||||
const safeUpdate: Partial<INotificationChannel> = {};
|
||||
for (const field of allowedFields) {
|
||||
if (updateData[field] !== undefined) {
|
||||
(safeUpdate as any)[field] = updateData[field];
|
||||
}
|
||||
}
|
||||
|
||||
const updatedChannel = await NotificationChannel.findByIdAndUpdate(
|
||||
id,
|
||||
{
|
||||
$set: {
|
||||
...safeUpdate,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: tokenizedUser.sub,
|
||||
},
|
||||
},
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!updatedChannel) {
|
||||
throw new ApiError("Failed to update notification channel", 500);
|
||||
}
|
||||
|
||||
return updatedChannel;
|
||||
};
|
||||
|
||||
delete = async (id: string) => {
|
||||
const result = await NotificationChannel.deleteOne({ _id: id });
|
||||
if (!result.deletedCount) {
|
||||
throw new ApiError("Notification channel not found", 404);
|
||||
}
|
||||
|
||||
await Monitor.updateMany({ notificationChannels: id }, { $pull: { notificationChannels: id } });
|
||||
return result.deletedCount === 1;
|
||||
};
|
||||
}
|
||||
|
||||
export default NotificationChannelService;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { IJobQueue } from "../infrastructure/JobQueue.js";
|
||||
|
||||
const SERVICE_NAME = "QueueServiceV2";
|
||||
|
||||
class QueueService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private jobQueue: IJobQueue;
|
||||
|
||||
constructor(jobQueue: IJobQueue) {
|
||||
this.jobQueue = jobQueue;
|
||||
}
|
||||
|
||||
async getMetrics() {
|
||||
return await this.jobQueue.getMetrics();
|
||||
}
|
||||
|
||||
async getJobs() {
|
||||
return await this.jobQueue.getJobs();
|
||||
}
|
||||
|
||||
async flush() {
|
||||
return await this.jobQueue.flush();
|
||||
}
|
||||
}
|
||||
|
||||
export default QueueService;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { IUser, User } from "../../../db/v2/models/index.js";
|
||||
|
||||
const SERVICE_NAME = "UserServiceV2";
|
||||
export interface IUserService {
|
||||
getAllUsers(): Promise<IUser[]>;
|
||||
}
|
||||
|
||||
class UserService implements IUserService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
async getAllUsers(): Promise<IUser[]> {
|
||||
return await User.find();
|
||||
}
|
||||
}
|
||||
|
||||
export default UserService;
|
||||
@@ -1,82 +0,0 @@
|
||||
import { IMonitor } from "../../../db/v2/models/index.js";
|
||||
import { INetworkService } from "./NetworkService.js";
|
||||
import { ICheckService } from "../business/CheckService.js";
|
||||
import { IMonitorStatsService } from "../business/MonitorStatsService.js";
|
||||
import { IStatusService } from "./StatusService.js";
|
||||
import { INotificationService } from "./NotificationService.js";
|
||||
import { IMaintenanceService } from "../business/MaintenanceService.js";
|
||||
import ApiError from "../../../utils/ApiError.js";
|
||||
|
||||
const SERVICE_NAME = "JobGeneratorV2";
|
||||
export interface IJobGenerator {
|
||||
generateJob: () => (Monitor: IMonitor) => Promise<void>;
|
||||
generateCleanupJob: () => () => Promise<void>;
|
||||
}
|
||||
|
||||
class JobGenerator implements IJobGenerator {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private networkService: INetworkService;
|
||||
private checkService: ICheckService;
|
||||
private monitorStatsService: IMonitorStatsService;
|
||||
private statusService: IStatusService;
|
||||
private notificationService: INotificationService;
|
||||
private maintenanceService: IMaintenanceService;
|
||||
|
||||
constructor(
|
||||
networkService: INetworkService,
|
||||
checkService: ICheckService,
|
||||
monitorStatsService: IMonitorStatsService,
|
||||
statusService: IStatusService,
|
||||
notificationService: INotificationService,
|
||||
maintenanceService: IMaintenanceService
|
||||
) {
|
||||
this.networkService = networkService;
|
||||
this.checkService = checkService;
|
||||
this.monitorStatsService = monitorStatsService;
|
||||
this.statusService = statusService;
|
||||
this.notificationService = notificationService;
|
||||
this.maintenanceService = maintenanceService;
|
||||
}
|
||||
|
||||
generateJob = () => {
|
||||
return async (monitor: IMonitor) => {
|
||||
try {
|
||||
const monitorId = monitor._id.toString();
|
||||
if (!monitorId) {
|
||||
throw new ApiError("No monitorID for creating job", 400);
|
||||
}
|
||||
|
||||
// Check for active maintenance window, if found, skip the check
|
||||
const isInMaintenance = await this.maintenanceService.isInMaintenance(monitorId);
|
||||
if (isInMaintenance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await this.networkService.requestStatus(monitor);
|
||||
const check = await this.checkService.buildCheck(status, monitor.type);
|
||||
await check.save();
|
||||
const [updatedMonitor, statusChanged] = await this.statusService.updateMonitorStatus(monitor, status);
|
||||
|
||||
if (statusChanged) {
|
||||
await this.notificationService.handleNotifications(updatedMonitor);
|
||||
}
|
||||
await this.statusService.updateMonitorStats(updatedMonitor, status, statusChanged);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
generateCleanupJob = () => {
|
||||
return async () => {
|
||||
try {
|
||||
await this.checkService.cleanupOrphanedChecks();
|
||||
await this.monitorStatsService.cleanupOrphanedMonitorStats();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default JobGenerator;
|
||||
@@ -1,231 +0,0 @@
|
||||
import { IJob } from "super-simple-scheduler/dist/job/job.js";
|
||||
import { Monitor, IMonitor } from "../../../db/v2/models/index.js";
|
||||
import Scheduler from "super-simple-scheduler";
|
||||
import { IJobGenerator } from "./JobGenerator.js";
|
||||
|
||||
const SERVICE_NAME = "JobQueueV2";
|
||||
export interface IJobMetrics {
|
||||
jobs: number;
|
||||
activeJobs: number;
|
||||
failingJobs: number;
|
||||
jobsWithFailures: Array<{
|
||||
monitorId: string | number;
|
||||
monitorUrl: string | null;
|
||||
monitorType: string | null;
|
||||
failedAt: number | null;
|
||||
failCount: number | null;
|
||||
failReason: string | null;
|
||||
}>;
|
||||
totalRuns: number;
|
||||
totalFailures: number;
|
||||
}
|
||||
|
||||
export interface IJobData extends IJob {
|
||||
lastRunTook: number | null;
|
||||
}
|
||||
|
||||
export interface IJobQueue {
|
||||
init: () => Promise<boolean>;
|
||||
addJob: (monitor: IMonitor) => Promise<boolean>;
|
||||
pauseJob: (monitor: IMonitor) => Promise<boolean>;
|
||||
resumeJob: (monitor: IMonitor) => Promise<boolean>;
|
||||
updateJob: (monitor: IMonitor) => Promise<boolean>;
|
||||
deleteJob: (monitor: IMonitor) => Promise<boolean>;
|
||||
getMetrics: () => Promise<IJobMetrics | null>;
|
||||
getJobs: () => Promise<IJobData[] | null>;
|
||||
flush: () => Promise<boolean>;
|
||||
shutdown: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export default class JobQueue implements IJobQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private scheduler: Scheduler;
|
||||
private static instance: JobQueue | null = null;
|
||||
private jobGenerator: any;
|
||||
constructor() {
|
||||
this.scheduler = new Scheduler({
|
||||
logLevel: "debug",
|
||||
});
|
||||
}
|
||||
|
||||
static async create(jobGenerator: IJobGenerator) {
|
||||
if (!JobQueue.instance) {
|
||||
const instance = new JobQueue();
|
||||
instance.jobGenerator = jobGenerator;
|
||||
await instance.init();
|
||||
JobQueue.instance = instance;
|
||||
}
|
||||
return JobQueue.instance;
|
||||
}
|
||||
|
||||
static getInstance(): JobQueue | null {
|
||||
return JobQueue.instance;
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
try {
|
||||
this.scheduler.start();
|
||||
// Add template and jobs
|
||||
this.scheduler.addTemplate("monitor-job", this.jobGenerator.generateJob());
|
||||
|
||||
// Add a cleanup job
|
||||
this.scheduler.addTemplate("cleanup-job", this.jobGenerator.generateCleanupJob());
|
||||
await this.scheduler.addJob({
|
||||
id: "cleanup-orphaned-checks",
|
||||
template: "cleanup-job",
|
||||
repeat: 24 * 60 * 60 * 1000, // 24 hours
|
||||
active: true,
|
||||
});
|
||||
|
||||
const monitors = await Monitor.find();
|
||||
for (const monitor of monitors) {
|
||||
this.addJob(monitor);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
addJob = async (monitor: IMonitor) => {
|
||||
try {
|
||||
return await this.scheduler?.addJob({
|
||||
id: monitor._id.toString(),
|
||||
template: "monitor-job",
|
||||
repeat: monitor.interval,
|
||||
active: monitor.isActive,
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
pauseJob = async (monitor: IMonitor) => {
|
||||
try {
|
||||
return await this.scheduler?.pauseJob(monitor._id.toString());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
resumeJob = async (monitor: IMonitor) => {
|
||||
try {
|
||||
return await this.scheduler.resumeJob(monitor._id.toString());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
updateJob = async (monitor: IMonitor) => {
|
||||
try {
|
||||
return await this.scheduler.updateJob(monitor._id.toString(), {
|
||||
repeat: monitor.interval,
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
deleteJob = async (monitor: IMonitor) => {
|
||||
try {
|
||||
this.scheduler?.removeJob(monitor._id.toString());
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
getMetrics = async (): Promise<IJobMetrics | null> => {
|
||||
try {
|
||||
const jobs = await this.scheduler.getJobs();
|
||||
const metrics: IJobMetrics = jobs.reduce<IJobMetrics>(
|
||||
(acc, job) => {
|
||||
if (!job.data) return acc;
|
||||
|
||||
acc.totalRuns += job.runCount || 0;
|
||||
acc.totalFailures += job.failCount || 0;
|
||||
acc.jobs++;
|
||||
|
||||
// Check if job is currently failing (has recent failures)
|
||||
const hasFailures = job.failCount && job.failCount > 0;
|
||||
const isCurrentlyFailing = hasFailures && job.lastFailedAt && (!job.lastRunAt || job.lastFailedAt > job.lastRunAt);
|
||||
|
||||
if (isCurrentlyFailing) {
|
||||
acc.failingJobs++;
|
||||
}
|
||||
|
||||
if (job.lockedAt) {
|
||||
acc.activeJobs++;
|
||||
}
|
||||
|
||||
if (hasFailures) {
|
||||
acc.jobsWithFailures.push({
|
||||
monitorId: job.id,
|
||||
monitorUrl: job.data?.url || null,
|
||||
monitorType: job.data?.type || null,
|
||||
failedAt: job.lastFailedAt || null,
|
||||
failCount: job.failCount || null,
|
||||
failReason: job.lastFailReason || null,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
jobs: 0,
|
||||
activeJobs: 0,
|
||||
failingJobs: 0,
|
||||
jobsWithFailures: [],
|
||||
totalRuns: 0,
|
||||
totalFailures: 0,
|
||||
}
|
||||
);
|
||||
return metrics;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
getJobs = async (): Promise<IJobData[] | null> => {
|
||||
try {
|
||||
const jobs = await this.scheduler.getJobs();
|
||||
return jobs.map((job) => {
|
||||
return {
|
||||
...job,
|
||||
lastRunTook: job.lockedAt || !job.lastFinishedAt || !job.lastRunAt ? null : job.lastFinishedAt - job.lastRunAt,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
flush = async () => {
|
||||
try {
|
||||
return await this.scheduler.flushJobs();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
shutdown = async () => {
|
||||
try {
|
||||
return await this.scheduler.stop();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
import { Got, HTTPError } from "got";
|
||||
import got from "got";
|
||||
import ping from "ping";
|
||||
import { IMonitor } from "../../../db/v2/models/index.js";
|
||||
import { GotTimings } from "../../../db/v2/models/checks/Check.js";
|
||||
import type { Response } from "got";
|
||||
import type { ISystemInfo, ICaptureInfo, ILighthouseResult } from "../../../db/v2/models/index.js";
|
||||
import { MonitorType, MonitorStatus } from "../../../db/v2/models/monitors/Monitor.js";
|
||||
import ApiError from "../../../utils/ApiError.js";
|
||||
import { config } from "../../../config/index.js";
|
||||
|
||||
const SERVICE_NAME = "NetworkServiceV2";
|
||||
export interface INetworkService {
|
||||
requestHttp: (monitor: IMonitor) => Promise<StatusResponse>;
|
||||
requestInfrastructure: (monitor: IMonitor) => Promise<StatusResponse>;
|
||||
requestStatus: (monitor: IMonitor) => Promise<StatusResponse>;
|
||||
requestPagespeed: (monitor: IMonitor) => Promise<StatusResponse>;
|
||||
requestPing: (monitor: IMonitor) => Promise<StatusResponse>;
|
||||
}
|
||||
|
||||
export interface ICapturePayload {
|
||||
data: ISystemInfo;
|
||||
capture: ICaptureInfo;
|
||||
}
|
||||
|
||||
export interface ILighthousePayload {
|
||||
lighthouseResult: ILighthouseResult;
|
||||
}
|
||||
|
||||
export interface StatusResponse<TPayload = unknown> {
|
||||
monitorId: string;
|
||||
type: MonitorType;
|
||||
code?: number;
|
||||
status: MonitorStatus;
|
||||
message: string;
|
||||
responseTime: number;
|
||||
timings?: GotTimings;
|
||||
payload?: TPayload;
|
||||
}
|
||||
|
||||
class NetworkService implements INetworkService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private got: Got;
|
||||
private NETWORK_ERROR: number;
|
||||
constructor() {
|
||||
this.got = got;
|
||||
this.NETWORK_ERROR = 5000;
|
||||
}
|
||||
|
||||
private buildStatusResponse = <T>(monitor: IMonitor, response: Response<T> | null, error: any | null): StatusResponse<T> => {
|
||||
if (error) {
|
||||
const statusResponse: StatusResponse<T> = {
|
||||
monitorId: monitor._id.toString(),
|
||||
type: monitor.type,
|
||||
status: "down" as MonitorStatus,
|
||||
code: this.NETWORK_ERROR,
|
||||
message: error.message || "Network error",
|
||||
responseTime: 0,
|
||||
timings: { phases: {} } as GotTimings,
|
||||
};
|
||||
if (error instanceof HTTPError) {
|
||||
statusResponse.code = error?.response?.statusCode || this.NETWORK_ERROR;
|
||||
statusResponse.message = error.message || "HTTP error";
|
||||
statusResponse.responseTime = error.timings?.phases?.total || 0;
|
||||
statusResponse.timings = error.timings;
|
||||
}
|
||||
return statusResponse;
|
||||
}
|
||||
|
||||
const statusResponse: StatusResponse<T> = {
|
||||
monitorId: monitor._id.toString(),
|
||||
type: monitor.type,
|
||||
code: response?.statusCode || this.NETWORK_ERROR,
|
||||
status: response?.ok === true ? "up" : "down",
|
||||
message: response?.statusMessage || "",
|
||||
responseTime: response?.timings?.phases?.total || 0,
|
||||
timings: response?.timings || ({ phases: {} } as GotTimings),
|
||||
};
|
||||
|
||||
return statusResponse;
|
||||
};
|
||||
|
||||
requestHttp = async (monitor: IMonitor) => {
|
||||
try {
|
||||
const url = monitor.url;
|
||||
if (!url) {
|
||||
throw new Error("No URL provided");
|
||||
}
|
||||
|
||||
try {
|
||||
const response: Response = await this.got(url);
|
||||
return this.buildStatusResponse(monitor, response, null);
|
||||
} catch (error) {
|
||||
return this.buildStatusResponse(monitor, null, error);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
requestInfrastructure = async (monitor: IMonitor) => {
|
||||
const url = monitor.url;
|
||||
if (!url) {
|
||||
throw new Error("No URL provided");
|
||||
}
|
||||
const secret = monitor.secret;
|
||||
if (!secret) {
|
||||
throw new Error("No secret provided for infrastructure monitor");
|
||||
}
|
||||
|
||||
let statusResponse: StatusResponse<ICapturePayload>;
|
||||
try {
|
||||
const response: Response<ICapturePayload> | null = await this.got(url, {
|
||||
headers: { Authorization: `Bearer ${secret}` },
|
||||
responseType: "json",
|
||||
});
|
||||
|
||||
statusResponse = this.buildStatusResponse(monitor, response, null);
|
||||
if (!response?.body) {
|
||||
throw new ApiError("No payload received from infrastructure monitor", 500);
|
||||
}
|
||||
statusResponse.payload = response?.body;
|
||||
return statusResponse;
|
||||
} catch (error) {
|
||||
statusResponse = this.buildStatusResponse(monitor, null, error);
|
||||
}
|
||||
return statusResponse;
|
||||
};
|
||||
|
||||
requestPagespeed = async (monitor: IMonitor) => {
|
||||
const apiKey = config.PAGESPEED_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("No API key provided for pagespeed monitor");
|
||||
}
|
||||
const url = monitor.url;
|
||||
if (!url) {
|
||||
throw new Error("No URL provided");
|
||||
}
|
||||
|
||||
let statusResponse: StatusResponse<ILighthousePayload>;
|
||||
|
||||
try {
|
||||
const response: Response = await this.got(url);
|
||||
statusResponse = this.buildStatusResponse(monitor, response, null) as StatusResponse<ILighthousePayload>;
|
||||
} catch (error) {
|
||||
statusResponse = this.buildStatusResponse(monitor, null, error);
|
||||
}
|
||||
|
||||
const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance&key=${apiKey}`;
|
||||
const pagespeedResponse = await this.got<ILighthousePayload>(pagespeedUrl, {
|
||||
responseType: "json",
|
||||
});
|
||||
const payload = pagespeedResponse.body;
|
||||
if (payload) {
|
||||
statusResponse.payload = payload;
|
||||
return statusResponse;
|
||||
} else {
|
||||
throw new ApiError("No payload received from pagespeed monitor", 500);
|
||||
}
|
||||
};
|
||||
|
||||
requestPing = async (monitor: IMonitor) => {
|
||||
const response = await ping.promise.probe(monitor.url);
|
||||
const status = response?.alive === true ? "up" : "down";
|
||||
|
||||
const rawTime = typeof response?.time === "string" ? parseFloat(response.time) : Number(response?.time);
|
||||
const responseTime = Number.isFinite(rawTime) ? rawTime : 0;
|
||||
|
||||
return {
|
||||
monitorId: monitor._id.toString(),
|
||||
type: monitor.type,
|
||||
status: status as MonitorStatus,
|
||||
message: "Ping successful",
|
||||
responseTime,
|
||||
timings: { phases: {} } as GotTimings,
|
||||
};
|
||||
};
|
||||
|
||||
requestStatus = async (monitor: IMonitor) => {
|
||||
switch (monitor?.type) {
|
||||
case "http":
|
||||
return await this.requestHttp(monitor); // uses GOT
|
||||
case "https":
|
||||
return await this.requestHttp(monitor); // uses GOT
|
||||
case "infrastructure":
|
||||
return await this.requestInfrastructure(monitor); // uses GOT
|
||||
case "pagespeed":
|
||||
return await this.requestPagespeed(monitor); // uses GOT
|
||||
case "ping":
|
||||
return await this.requestPing(monitor); // uses PING
|
||||
default:
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default NetworkService;
|
||||
@@ -1,61 +0,0 @@
|
||||
import UserService from "../business/UserService.js";
|
||||
import { IMonitor, NotificationChannel } from "../../../db/v2/models/index.js";
|
||||
import { EmailService, SlackService, DiscordService, WebhookService } from "./NotificationServices/index.js";
|
||||
|
||||
const SERVICE_NAME = "NotificationServiceV2";
|
||||
export interface INotificationService {
|
||||
handleNotifications: (monitor: IMonitor) => Promise<void>;
|
||||
}
|
||||
|
||||
class NotificationService implements INotificationService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private emailService: EmailService;
|
||||
private slackService: SlackService;
|
||||
private discordService: DiscordService;
|
||||
private webhookService: WebhookService;
|
||||
private userService: UserService;
|
||||
|
||||
constructor(userService: UserService) {
|
||||
this.userService = userService;
|
||||
this.emailService = new EmailService(userService);
|
||||
this.slackService = new SlackService();
|
||||
this.discordService = new DiscordService();
|
||||
this.webhookService = new WebhookService();
|
||||
}
|
||||
|
||||
handleNotifications = async (monitor: IMonitor) => {
|
||||
const notificationIds = monitor.notificationChannels || [];
|
||||
|
||||
if (notificationIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationChannels = await NotificationChannel.find({
|
||||
_id: { $in: notificationIds },
|
||||
});
|
||||
|
||||
for (const channel of notificationChannels) {
|
||||
// Implement sending logic based on channel.type and channel.config
|
||||
let service;
|
||||
switch (channel.type) {
|
||||
case "email":
|
||||
await this.emailService.sendMessage(this.emailService.buildAlert(monitor), channel);
|
||||
break;
|
||||
case "slack":
|
||||
await this.slackService.sendMessage(this.slackService.buildAlert(monitor), channel);
|
||||
break;
|
||||
case "discord":
|
||||
await this.discordService.sendMessage(this.discordService.buildAlert(monitor), channel);
|
||||
break;
|
||||
case "webhook":
|
||||
await this.webhookService.sendMessage(this.webhookService.buildAlert(monitor), channel);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown notification channel type: ${channel.type}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
export default NotificationService;
|
||||
@@ -1,76 +0,0 @@
|
||||
import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js";
|
||||
import { IAlert, IMessageService } from "./IMessageService.js";
|
||||
import got from "got";
|
||||
import ApiError from "../../../../utils/ApiError.js";
|
||||
|
||||
const SERVICE_NAME = "DiscordServiceV2";
|
||||
class DiscordService implements IMessageService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor() {}
|
||||
|
||||
private toDiscordEmbeds = (alert: IAlert) => {
|
||||
return {
|
||||
color: alert.status === "up" ? 65280 : 16711680,
|
||||
title: `Monitor name: ${alert.name}`,
|
||||
description: `Status: **${alert.status}**`,
|
||||
fields: [
|
||||
{
|
||||
name: "Url",
|
||||
value: alert.url,
|
||||
},
|
||||
{
|
||||
name: "Checked at",
|
||||
value: alert.checkTime ? alert.checkTime.toISOString() : "N/A",
|
||||
},
|
||||
{ name: "Alert time", value: alert.alertTime.toISOString() },
|
||||
...(alert.details
|
||||
? Object.entries(alert.details).map(([key, value]) => ({
|
||||
name: key,
|
||||
value,
|
||||
}))
|
||||
: []),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
buildAlert = (monitor: IMonitor) => {
|
||||
const name = monitor?.name || "Unnamed monitor";
|
||||
const monitorStatus = monitor?.status || "unknown status";
|
||||
const url = monitor?.url || "no URL";
|
||||
const checkTime = monitor?.lastCheckedAt || null;
|
||||
const alertTime = new Date();
|
||||
return {
|
||||
name,
|
||||
url,
|
||||
status: monitorStatus,
|
||||
checkTime,
|
||||
alertTime,
|
||||
};
|
||||
};
|
||||
|
||||
sendMessage = async (alert: IAlert, channel: INotificationChannel) => {
|
||||
const notificationUrl = channel?.config?.url;
|
||||
if (!notificationUrl) {
|
||||
throw new ApiError("Webhook URL not configured", 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
content: "Status Alert",
|
||||
embeds: [this.toDiscordEmbeds(alert)],
|
||||
};
|
||||
await got.post(notificationUrl, { json: payload });
|
||||
} catch (error) {
|
||||
console.warn("Failed to send Discord message", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
testMessage = async () => {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export default DiscordService;
|
||||
@@ -1,68 +0,0 @@
|
||||
import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js";
|
||||
import { IMessageService, IAlert } from "./IMessageService.js";
|
||||
import nodemailer, { Transporter } from "nodemailer";
|
||||
import { config } from "../../../../config/index.js";
|
||||
import UserService from "../../business/UserService.js";
|
||||
import ApiError from "../../../../utils/ApiError.js";
|
||||
|
||||
const SERVICE_NAME = "EmailServiceV2";
|
||||
class EmailService implements IMessageService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private transporter: Transporter;
|
||||
private userService: UserService;
|
||||
|
||||
constructor(userService: UserService) {
|
||||
this.userService = userService;
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: config.SMTP_HOST,
|
||||
port: config.SMTP_PORT,
|
||||
secure: config.SMTP_PORT === 465,
|
||||
auth: {
|
||||
user: config.SMTP_USER,
|
||||
pass: config.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
buildAlert = (monitor: IMonitor) => {
|
||||
const name = monitor?.name || "Unnamed monitor";
|
||||
const monitorStatus = monitor?.status || "unknown status";
|
||||
const url = monitor?.url || "no URL";
|
||||
const checkTime = monitor?.lastCheckedAt || null;
|
||||
const alertTime = new Date();
|
||||
return {
|
||||
name,
|
||||
url,
|
||||
status: monitorStatus,
|
||||
checkTime,
|
||||
alertTime,
|
||||
};
|
||||
};
|
||||
|
||||
sendMessage = async (alert: string | IAlert, channel: INotificationChannel) => {
|
||||
try {
|
||||
const users = await this.userService.getAllUsers();
|
||||
const emails = users.map((u) => u.email).join(",");
|
||||
|
||||
if (!emails || emails.length === 0) {
|
||||
throw new ApiError("No user emails found", 500);
|
||||
}
|
||||
|
||||
await this.transporter.sendMail({
|
||||
from: `"Checkmate" <${config.SMTP_USER}>`,
|
||||
to: emails,
|
||||
subject: "Monitor Alert",
|
||||
text: JSON.stringify(alert, null, 2),
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
testMessage = async () => {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export default EmailService;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js";
|
||||
|
||||
export interface IAlert {
|
||||
name: string;
|
||||
url: string;
|
||||
status: string;
|
||||
details?: Record<string, string>;
|
||||
checkTime: Date | null;
|
||||
alertTime: Date;
|
||||
}
|
||||
export interface IMessageService {
|
||||
buildAlert: (monitor: IMonitor) => IAlert;
|
||||
sendMessage: (alert: IAlert, channel: INotificationChannel) => Promise<boolean>;
|
||||
testMessage: (message: string, channel: INotificationChannel) => Promise<boolean>;
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js";
|
||||
import { IAlert, IMessageService } from "./IMessageService.js";
|
||||
import got from "got";
|
||||
|
||||
const SERVICE_NAME = "SlackServiceV2";
|
||||
class SlackService implements IMessageService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor() {}
|
||||
|
||||
private toSlackBlocks = (alert: IAlert) => {
|
||||
return [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "Status Alert",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Monitor name:* ${alert.name}`,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Status:* ${alert.status}`,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*URL:* ${alert.url}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Checked at:* ${alert?.checkTime?.toISOString() || "N/A"}`,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
|
||||
...(alert.details
|
||||
? Object.entries(alert.details).map(([key, value]) => ({
|
||||
type: "section",
|
||||
fields: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*${key}:* ${value}`,
|
||||
},
|
||||
],
|
||||
}))
|
||||
: []),
|
||||
|
||||
{
|
||||
type: "context",
|
||||
elements: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Alert generated at:* ${alert?.alertTime?.toISOString() || "N/A"}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
buildAlert = (monitor: IMonitor) => {
|
||||
const name = monitor?.name || "Unnamed monitor";
|
||||
const monitorStatus = monitor?.status || "unknown status";
|
||||
const url = monitor?.url || "no URL";
|
||||
const checkTime = monitor?.lastCheckedAt || null;
|
||||
const alertTime = new Date();
|
||||
return {
|
||||
name,
|
||||
url,
|
||||
status: monitorStatus,
|
||||
checkTime,
|
||||
alertTime,
|
||||
};
|
||||
};
|
||||
|
||||
sendMessage = async (alert: IAlert, channel: INotificationChannel) => {
|
||||
const notificationUrl = channel?.config?.url;
|
||||
if (!notificationUrl) {
|
||||
throw new Error("Webhook URL not configured");
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
text: "Status Alert",
|
||||
blocks: this.toSlackBlocks(alert),
|
||||
};
|
||||
await got.post(notificationUrl, { json: payload });
|
||||
} catch (error) {
|
||||
console.warn("Error sending Slack message:", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
testMessage = async () => {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export default SlackService;
|
||||
@@ -1,47 +0,0 @@
|
||||
import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js";
|
||||
import { IAlert, IMessageService } from "./IMessageService.js";
|
||||
import ApiError from "../../../../utils/ApiError.js";
|
||||
import got from "got";
|
||||
|
||||
const SERVICE_NAME = "WebhookServiceV2";
|
||||
class WebhookService implements IMessageService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor() {}
|
||||
|
||||
buildAlert = (monitor: IMonitor) => {
|
||||
const name = monitor?.name || "Unnamed monitor";
|
||||
const monitorStatus = monitor?.status || "unknown status";
|
||||
const url = monitor?.url || "no URL";
|
||||
const checkTime = monitor?.lastCheckedAt || null;
|
||||
const alertTime = new Date();
|
||||
return {
|
||||
name,
|
||||
url,
|
||||
status: monitorStatus,
|
||||
checkTime,
|
||||
alertTime,
|
||||
};
|
||||
};
|
||||
|
||||
sendMessage = async (alert: IAlert, channel: INotificationChannel) => {
|
||||
const notificationUrl = channel?.config?.url;
|
||||
if (!notificationUrl) {
|
||||
throw new ApiError("Webhook URL not configured", 400);
|
||||
}
|
||||
try {
|
||||
await got.post(notificationUrl, { json: { ...alert } });
|
||||
} catch (error) {
|
||||
console.warn("Failed to send webhook notification:", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
testMessage = async () => {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export default WebhookService;
|
||||
@@ -1,5 +0,0 @@
|
||||
export { default as DiscordService } from "./Discord.js";
|
||||
export { default as EmailService } from "./Email.js";
|
||||
export { default as SlackService } from "./Slack.js";
|
||||
export { default as WebhookService } from "./Webhook.js";
|
||||
export * from "./IMessageService.js";
|
||||
@@ -1,102 +0,0 @@
|
||||
import { IMonitor, IMonitorStats, MonitorStats } from "../../../db/v2/models/index.js";
|
||||
import { StatusResponse } from "./NetworkService.js";
|
||||
import ApiError from "../../../utils/ApiError.js";
|
||||
|
||||
const SERVICE_NAME = "StatusServiceV2";
|
||||
const MAX_LATEST_CHECKS = 25;
|
||||
export interface IStatusService {
|
||||
updateMonitorStatus: (monitor: IMonitor, status: StatusResponse) => Promise<StatusChangeResult>;
|
||||
|
||||
calculateAvgResponseTime: (stats: IMonitorStats, statusResponse: StatusResponse) => number;
|
||||
|
||||
updateMonitorStats: (monitor: IMonitor, status: StatusResponse, statusChanged: boolean) => Promise<IMonitorStats | null>;
|
||||
}
|
||||
|
||||
export type StatusChangeResult = [updatedMonitor: IMonitor, statusChanged: boolean];
|
||||
|
||||
class StatusService implements IStatusService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
updateMonitorStatus = async (monitor: IMonitor, statusResponse: StatusResponse): Promise<StatusChangeResult> => {
|
||||
const newStatus = statusResponse.status;
|
||||
monitor.lastCheckedAt = new Date();
|
||||
|
||||
// Store latest checks for display
|
||||
monitor.latestChecks = monitor.latestChecks || [];
|
||||
monitor.latestChecks.push({
|
||||
status: newStatus,
|
||||
responseTime: statusResponse.responseTime,
|
||||
checkedAt: monitor.lastCheckedAt,
|
||||
});
|
||||
while (monitor.latestChecks.length > MAX_LATEST_CHECKS) {
|
||||
monitor.latestChecks.shift();
|
||||
}
|
||||
|
||||
// Update monitor status
|
||||
if (monitor.status === "initializing") {
|
||||
monitor.status = newStatus;
|
||||
return [await monitor.save(), true];
|
||||
} else {
|
||||
const { n } = monitor;
|
||||
const latestChecks = monitor.latestChecks.slice(-n);
|
||||
// Return early if not enough statuses to evaluate
|
||||
if (latestChecks.length < n) {
|
||||
return [await monitor.save(), false];
|
||||
}
|
||||
|
||||
// If all different than current status, update status
|
||||
const allDifferent = latestChecks.every((check) => check.status !== monitor.status);
|
||||
if (allDifferent && monitor.status !== newStatus) {
|
||||
monitor.status = newStatus;
|
||||
}
|
||||
return [await monitor.save(), allDifferent];
|
||||
}
|
||||
};
|
||||
|
||||
calculateAvgResponseTime = (stats: IMonitorStats, statusResponse: StatusResponse): number => {
|
||||
let avgResponseTime = stats.avgResponseTime;
|
||||
// Set initial
|
||||
if (avgResponseTime === 0) {
|
||||
avgResponseTime = statusResponse.responseTime;
|
||||
} else {
|
||||
avgResponseTime = (avgResponseTime * (stats.totalChecks - 1) + statusResponse.responseTime) / stats.totalChecks;
|
||||
}
|
||||
return avgResponseTime;
|
||||
};
|
||||
|
||||
updateMonitorStats = async (monitor: IMonitor, statusResponse: StatusResponse, statusChanged: boolean) => {
|
||||
const stats = await MonitorStats.findOne({ monitorId: monitor._id });
|
||||
if (!stats) {
|
||||
throw new ApiError("MonitorStats not found", 500);
|
||||
}
|
||||
|
||||
// Update check counts
|
||||
stats.totalChecks += 1;
|
||||
stats.totalUpChecks += statusResponse.status === "up" ? 1 : 0;
|
||||
stats.totalDownChecks += statusResponse.status === "down" ? 1 : 0;
|
||||
|
||||
// Update streak
|
||||
if (!statusChanged) {
|
||||
stats.currentStreak += 1;
|
||||
} else {
|
||||
stats.currentStreak = 1;
|
||||
stats.currentStreakStatus = statusResponse.status;
|
||||
stats.currentStreakStartedAt = Date.now();
|
||||
}
|
||||
|
||||
// Update time stamps
|
||||
stats.lastCheckTimestamp = Date.now();
|
||||
stats.timeOfLastFailure = statusResponse.status === "down" ? Date.now() : stats.timeOfLastFailure;
|
||||
|
||||
// Update stats that need updated check counts
|
||||
stats.avgResponseTime = this.calculateAvgResponseTime(stats, statusResponse);
|
||||
stats.uptimePercentage = stats.totalUpChecks / stats.totalChecks;
|
||||
|
||||
// Other
|
||||
stats.lastResponseTime = statusResponse.responseTime;
|
||||
stats.maxResponseTime = Math.max(stats.maxResponseTime, statusResponse.responseTime);
|
||||
|
||||
return await stats.save();
|
||||
};
|
||||
}
|
||||
|
||||
export default StatusService;
|
||||
@@ -1 +0,0 @@
|
||||
export type { MonitorWithChecksResponse } from "./monitor-response-with-checks.js";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { IMonitor, IMonitorStats } from "../db/v2/models/index.js";
|
||||
|
||||
export interface MonitorWithChecksResponse {
|
||||
monitor: IMonitor;
|
||||
checks: Array<{
|
||||
_id: string;
|
||||
count: number;
|
||||
avgResponseTime: number;
|
||||
}>;
|
||||
stats: IMonitorStats;
|
||||
}
|
||||
Reference in New Issue
Block a user