Merge pull request #2978 from bluewave-labs/feat/v2/routes

add v2 routes, controllers
This commit is contained in:
Alexander Holliday
2025-09-25 09:43:54 -07:00
committed by GitHub
38 changed files with 1532 additions and 3 deletions

View File

@@ -2,7 +2,7 @@ import { createCommonDependencies } from "../controllers/v1/baseController.js";
// Services
// Controllers
// V1 Controllers
import MonitorController from "../controllers/v1/monitorController.js";
import AuthController from "../controllers/v1/authController.js";
import SettingsController from "../controllers/v1/settingsController.js";
@@ -15,10 +15,18 @@ import StatusPageController from "../controllers/v1/statusPageController.js";
import NotificationController from "../controllers/v1/notificationController.js";
import DiagnosticController from "../controllers/v1/diagnosticController.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);
// V1
controllers.authController = new AuthController(commonDependencies, {
settingsService: services.settingsService,
emailService: services.emailService,
@@ -62,5 +70,13 @@ export const initializeControllers = (services) => {
diagnosticService: services.diagnosticService,
});
//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);
controllers.notificationChannelControllerV2 = new NotificationChannelControllerV2(services.notificationChannelServiceV2);
controllers.queueControllerV2 = new QueueControllerV2(services.jobQueueV2);
return controllers;
};

View File

@@ -13,7 +13,16 @@ import LogRoutes from "../routes/v1/logRoutes.js";
import DiagnosticRoutes from "../routes/v1//diagnosticRoute.js";
import NotificationRoutes from "../routes/v1/notificationRoute.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);
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
const settingsRoutes = new SettingsRoutes(controllers.settingsController);
@@ -37,4 +46,19 @@ export const setupRoutes = (app, controllers) => {
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.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());
};

View File

@@ -61,13 +61,35 @@ import AppSettings from "../db/v1/models/AppSettings.js";
import InviteModule from "../db/v1/modules/inviteModule.js";
import CheckModule from "../db/v1/modules/checkModule.js";
import StatusPageModule from "../db/v1/modules/statusPageModule.js";
import UserModule from "../db/v1/modules//userModule.js";
import UserModule from "../db/v1/modules/userModule.js";
import MaintenanceWindowModule from "../db/v1/modules/maintenanceWindowModule.js";
import MonitorModule from "../db/v1/modules/monitorModule.js";
import NotificationModule from "../db/v1/modules/notificationModule.js";
import RecoveryModule from "../db/v1/modules/recoveryModule.js";
import SettingsModule from "../db/v1/modules/settingsModule.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;
@@ -208,7 +230,37 @@ export const initializeServices = async ({ logger, envSettings, settingsService
games,
});
// V2 Services
const jobQueueV2 = new JobQueueV2();
const authServiceV2 = new AuthServiceV2(jobQueueV2);
const checkServiceV2 = new CheckServiceV2();
const inviteServiceV2 = new InviteServiceV2();
const maintenanceServiceV2 = new MaintenanceServiceV2();
const monitorServiceV2 = new MonitorServiceV2(jobQueueV2);
const monitorStatsServiceV2 = new MonitorStatsServiceV2();
const notificationChannelServiceV2 = new NotificationChannelServiceV2();
const queueServiceV2 = new QueueServiceV2(jobQueueV2);
const userServiceV2 = new UserServiceV2();
// V2 Infra
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 services = {
//v1
settingsService,
translationService,
stringService,
@@ -227,6 +279,25 @@ export const initializeServices = async ({ logger, envSettings, settingsService
monitorService,
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) => {

View File

@@ -0,0 +1,150 @@
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;

View File

@@ -0,0 +1,62 @@
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;

View File

@@ -0,0 +1,96 @@
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;

View File

@@ -0,0 +1,157 @@
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";
class MonitorController {
private monitorService: MonitorService;
constructor(monitorService: MonitorService) {
this.monitorService = monitorService;
}
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);
}
};
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);
}
};
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);
}
};
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);
}
};
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;

View File

@@ -0,0 +1,96 @@
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;

View File

@@ -0,0 +1,29 @@
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;

View File

@@ -0,0 +1,77 @@
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 };

View File

@@ -0,0 +1,21 @@
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 };

View File

@@ -0,0 +1,33 @@
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;

View File

@@ -0,0 +1,30 @@
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;

View File

@@ -0,0 +1,34 @@
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;

View File

@@ -0,0 +1,34 @@
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.patch("/:id/active", verifyToken, verifyPermission(["monitors.update"]), this.controller.toggleActive);
this.router.patch("/:id", verifyToken, verifyPermission(["monitors.update"]), this.controller.update);
this.router.get("/:id", verifyToken, verifyPermission(["monitors.view"]), this.controller.get);
this.router.delete("/:id", verifyToken, verifyPermission(["monitors.delete"]), this.controller.delete);
};
getRouter() {
return this.router;
}
}
export default MonitorRoutes;

View File

@@ -0,0 +1,34 @@
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;

View File

@@ -0,0 +1,24 @@
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;

View File

@@ -4,6 +4,8 @@ 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.*",
@@ -94,6 +96,8 @@ export interface IAuthService {
}
class AuthService implements IAuthService {
static SERVICE_NAME = SERVICE_NAME;
private jobQueue: IJobQueue;
constructor(jobQueue: IJobQueue) {
this.jobQueue = jobQueue;

View File

@@ -6,12 +6,15 @@ import { StatusResponse } from "../infrastructure/NetworkService.js";
import type { ICapturePayload, ILighthousePayload } from "../infrastructure/NetworkService.js";
import mongoose from "mongoose";
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;

View File

@@ -2,6 +2,7 @@ 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[]>;
@@ -10,6 +11,7 @@ export interface IInviteService {
}
class InviteService implements IInviteService {
static SERVICE_NAME = SERVICE_NAME;
constructor() {}
create = async (tokenizedUser: ITokenizedUser, inviteData: IInvite) => {

View File

@@ -1,6 +1,8 @@
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,
@@ -18,6 +20,7 @@ export interface IMaintenanceService {
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;

View File

@@ -0,0 +1,468 @@
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 "30m":
return new Date(now.getTime() - 30 * 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 "30m":
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,
}).lean();
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;

View File

@@ -1,10 +1,13 @@
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() {

View File

@@ -1,6 +1,8 @@
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,
@@ -15,6 +17,8 @@ export interface INotificationChannelService {
}
class NotificationChannelService implements INotificationChannelService {
static SERVICE_NAME = SERVICE_NAME;
constructor() {}
create = async (tokenizedUser: ITokenizedUser, notificationChannelData: INotificationChannel) => {

View File

@@ -1,6 +1,9 @@
import { IJobQueue } from "../infrastructure/JobQueue.js";
const SERVICE_NAME = "QueueServiceV2";
class QueueService {
static SERVICE_NAME = SERVICE_NAME;
private jobQueue: IJobQueue;
constructor(jobQueue: IJobQueue) {

View File

@@ -1,10 +1,12 @@
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();
}

View File

@@ -7,12 +7,14 @@ 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;

View File

@@ -2,6 +2,8 @@ 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;
@@ -36,6 +38,8 @@ export interface IJobQueue {
}
export default class JobQueue implements IJobQueue {
static SERVICE_NAME = SERVICE_NAME;
private scheduler: Scheduler;
private static instance: JobQueue | null = null;
private jobGenerator: any;

View File

@@ -1,4 +1,5 @@
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";
@@ -7,6 +8,8 @@ import type { ISystemInfo, ICaptureInfo, ILighthouseResult } from "../../../db/v
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>;
@@ -36,9 +39,10 @@ export interface StatusResponse<TPayload = unknown> {
}
class NetworkService implements INetworkService {
static SERVICE_NAME = SERVICE_NAME;
private got: Got;
private NETWORK_ERROR: number;
constructor(got: Got) {
constructor() {
this.got = got;
this.NETWORK_ERROR = 5000;
}

View File

@@ -1,11 +1,14 @@
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;

View File

@@ -2,7 +2,10 @@ import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.j
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) => {

View File

@@ -4,7 +4,10 @@ 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;

View File

@@ -2,7 +2,9 @@ import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.j
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) => {

View File

@@ -2,7 +2,11 @@ import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.j
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) => {

View File

@@ -2,6 +2,7 @@ import { IMonitor, IMonitorStats, MonitorStats } from "../../../db/v2/models/ind
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>;
@@ -14,6 +15,7 @@ export interface IStatusService {
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();

10
server/src/types/express.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
import { ITokenizedUser } from "../db/models/index.ts";
declare global {
namespace Express {
interface Request {
user?: ITokenizedUser;
resource?: any;
}
}
}

View File

@@ -0,0 +1 @@
export type { MonitorWithChecksResponse } from "./monitor-response-with-checks.js";

View File

@@ -0,0 +1,11 @@
import { IMonitor, IMonitorStats } from "../db/v2/models/index.js";
export interface MonitorWithChecksResponse {
monitor: IMonitor;
checks: Array<{
_id: string;
count: number;
avgResponseTime: number;
}>;
stats: IMonitorStats;
}