Remove business V2 logic

This commit is contained in:
Sajanpreet Singh
2025-12-04 10:31:45 -06:00
parent 1e3ee23120
commit 72bcc0d020
117 changed files with 1 additions and 8794 deletions
-15
View File
@@ -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;
};
-23
View File
@@ -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());
};
-68
View File
@@ -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) => {
-150
View File
@@ -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;
-25
View File
@@ -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;
}
};
-50
View File
@@ -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);
-102
View File
@@ -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);
-297
View File
@@ -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);
-33
View File
@@ -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";
-43
View File
@@ -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 };
-21
View File
@@ -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 };
-33
View File
@@ -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;
-30
View File
@@ -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;
-34
View File
@@ -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;
-36
View File
@@ -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;
-24
View File
@@ -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
View File
@@ -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;
}