diff --git a/server/src/controllers/geoCheckController.ts b/server/src/controllers/geoCheckController.ts index cfa7fe056..aef0d7e7c 100644 --- a/server/src/controllers/geoCheckController.ts +++ b/server/src/controllers/geoCheckController.ts @@ -1,6 +1,8 @@ import { Request, Response, NextFunction } from "express"; import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js"; import type { IGeoChecksService } from "@/service/business/geoChecksService.js"; +import { requireTeamId } from "./controllerUtils.js"; +import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "geoCheckController"; @@ -18,13 +20,29 @@ class GeoCheckController { getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - getChecksParamValidation.parse(req.params); - getChecksQueryValidation.parse(req.query); + const validatedParams = getChecksParamValidation.parse(req.params); + const validatedQuery = getChecksQueryValidation.parse(req.query); + + if (!validatedQuery.continent || validatedQuery.continent.length === 0) { + throw new AppError({ + message: "At least one continent must be specified", + service: SERVICE_NAME, + method: "getGeoChecksByMonitor", + details: { monitorId: validatedParams.monitorId }, + status: 400, + }); + } + + const teamId = requireTeamId(req.user?.teamId); const result = await this.geoChecksService.getGeoChecksByMonitor({ - monitorId: req?.params?.monitorId as string, - query: req?.query, - teamId: req?.user?.teamId as string, + monitorId: validatedParams.monitorId, + teamId: teamId, + sortOrder: validatedQuery.sortOrder, + dateRange: validatedQuery.dateRange, + page: validatedQuery.page, + rowsPerPage: validatedQuery.rowsPerPage, + continent: validatedQuery.continent, }); return res.status(200).json({ diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index 2e0294db1..edc236fa1 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -21,7 +21,17 @@ class MaintenanceWindowController { try { const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body); const teamId = requireTeamId(req?.user?.teamId); - await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: validatedBody }); + + const monitorIDs = validatedBody.monitors; + const name = validatedBody.name; + const active = validatedBody.active ? validatedBody.active : true; + const duration = validatedBody.duration; + const durationUnit = validatedBody.durationUnit; + const repeat = validatedBody.repeat; + const start = validatedBody.start; + const end = validatedBody.end; + + await this.maintenanceWindowService.createMaintenanceWindow({ teamId, monitorIDs, name, active, duration, durationUnit, repeat, start, end }); return res.status(200).json({ success: true, diff --git a/server/src/service/business/diagnosticService.ts b/server/src/service/business/diagnosticService.ts index f9c53f633..45a5592f5 100644 --- a/server/src/service/business/diagnosticService.ts +++ b/server/src/service/business/diagnosticService.ts @@ -24,8 +24,7 @@ export class DiagnosticService implements IDiagnosticService { * Clears performance marks after each measurement to prevent memory leaks. */ const obs = new PerformanceObserver((items) => { - // Get the first entry but we don't need to store it - items.getEntries()[0]; + items.getEntries(); performance.clearMarks(); }); obs.observe({ entryTypes: ["measure"] }); diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index f35826a8f..315c57858 100644 --- a/server/src/service/business/geoChecksService.ts +++ b/server/src/service/business/geoChecksService.ts @@ -1,7 +1,7 @@ import type { Monitor, GeoCheck } from "@/types/index.js"; -import type { GeoCheckResult } from "@/types/geoCheck.js"; +import type { GeoCheckResult, GeoContinent } from "@/types/geoCheck.js"; import { Types } from "mongoose"; -import type { IGeoChecksRepository } from "@/repositories/index.js"; +import type { FlatGeoChecksQueryResult, IGeoChecksRepository } from "@/repositories/index.js"; import type { IMonitorsRepository } from "@/repositories/index.js"; import type { IGlobalPingService } from "@/service/infrastructure/globalPingService.js"; import type { ILogger } from "@/utils/logger.js"; @@ -13,7 +13,15 @@ export interface IGeoChecksService { readonly serviceName: string; buildGeoCheck(monitor: Monitor): Promise; createGeoChecks(geoChecks: GeoCheck[]): Promise; - getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise; + getGeoChecksByMonitor(args: { + monitorId: string; + teamId: string; + sortOrder: string; + dateRange: string; + page?: number; + rowsPerPage?: number; + continent: GeoContinent | GeoContinent[]; + }): Promise; } export class GeoChecksService implements IGeoChecksService { @@ -139,7 +147,23 @@ export class GeoChecksService implements IGeoChecksService { return this.geoChecksRepository.createGeoChecks(geoChecks); }; - getGeoChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => { + getGeoChecksByMonitor = async ({ + monitorId, + teamId, + sortOrder, + dateRange, + page, + rowsPerPage, + continent, + }: { + monitorId: string; + teamId: string; + sortOrder: string; + dateRange: string; + page?: number; + rowsPerPage?: number; + continent: GeoContinent | GeoContinent[]; + }) => { if (!monitorId) { throw new AppError({ message: "No monitor ID in request", @@ -167,10 +191,9 @@ export class GeoChecksService implements IGeoChecksService { }); } - const { sortOrder, dateRange, page, rowsPerPage, continent } = query; const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined; - const parsedPage = page ? parseInt(page) : page; - const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage; + const parsedPage = page || 0; + const parsedRowsPerPage = rowsPerPage || 5; const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents); return result; }; diff --git a/server/src/service/business/inviteService.ts b/server/src/service/business/inviteService.ts index 9d1cbf59d..7e2b7de52 100644 --- a/server/src/service/business/inviteService.ts +++ b/server/src/service/business/inviteService.ts @@ -2,6 +2,8 @@ import type { Invite, UserRole } from "@/types/index.js"; import { canManageRole } from "@/types/user.js"; import type { IInvitesRepository } from "@/repositories/index.js"; import { AppError } from "@/utils/AppError.js"; +import { ISettingsService } from "../system/settingsService.js"; +import { IEmailService } from "../infrastructure/emailService.js"; const SERVICE_NAME = "inviteService"; @@ -14,8 +16,8 @@ export interface IInviteService { export class InviteService implements IInviteService { static SERVICE_NAME = SERVICE_NAME; - private settingsService: any; - private emailService: any; + private settingsService: ISettingsService; + private emailService: IEmailService; private invitesRepository: IInvitesRepository; constructor({ @@ -24,8 +26,8 @@ export class InviteService implements IInviteService { emailService, }: { invitesRepository: IInvitesRepository; - settingsService: any; - emailService: any; + settingsService: ISettingsService; + emailService: IEmailService; }) { this.invitesRepository = invitesRepository; this.settingsService = settingsService; @@ -57,8 +59,16 @@ export class InviteService implements IInviteService { return inviteToken; }; - sendInviteEmail = async ({ invite, firstName, userRoles }: { invite: Partial; firstName: any; userRoles: UserRole[] }) => { + sendInviteEmail = async ({ invite, firstName, userRoles }: { invite: Partial; firstName: string; userRoles: UserRole[] }) => { const inviteRoles = invite.role ?? []; + if (!invite.email) { + throw new AppError({ + message: "Invite email is required to send an invite", + service: SERVICE_NAME, + method: "sendInviteEmail", + status: 400, + }); + } for (const targetRole of inviteRoles) { const canManage = userRoles.some((actorRole) => canManageRole(actorRole, targetRole)); @@ -66,7 +76,7 @@ export class InviteService implements IInviteService { throw new AppError({ message: "You do not have permission to create this invite", service: SERVICE_NAME, - method: "getInviteToken", + method: "sendInviteEmail", status: 403, }); } @@ -79,6 +89,16 @@ export class InviteService implements IInviteService { name: firstName, link: `${clientHost}/register/${inviteToken.token}`, }); + + if (!html) { + throw new AppError({ + message: "Failed to build invite e-mail... Please verify your settings.", + service: SERVICE_NAME, + method: "sendInviteEmail", + status: 500, + }); + } + const result = await this.emailService.sendEmail(invite.email, "Welcome to Uptime Monitor", html); if (!result) { throw new AppError({ diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index 98b803bd1..50278d215 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -1,11 +1,21 @@ import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js"; -import type { MaintenanceWindow } from "@/types/index.js"; +import type { DurationUnit, MaintenanceWindow } from "@/types/index.js"; import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "maintenanceWindowService"; export interface IMaintenanceWindowService { - createMaintenanceWindow(params: { teamId: string; body: Partial & { monitors: string[] } }): Promise; + createMaintenanceWindow(params: { + teamId: string; + monitorIDs: string[]; + name: string; + active: boolean; + duration: number; + durationUnit: DurationUnit; + repeat: number; + start: string; + end: string; + }): Promise; getMaintenanceWindowById(params: { id: string; teamId: string }): Promise; getMaintenanceWindowsByTeamId(params: { teamId: string; @@ -40,9 +50,28 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { return MaintenanceWindowService.SERVICE_NAME; } - createMaintenanceWindow = async ({ teamId, body }: { teamId: string; body: any }) => { - const monitorIds = body.monitors; - const monitors = await this.monitorsRepository.findByIds(monitorIds); + createMaintenanceWindow = async ({ + teamId, + monitorIDs, + name, + active, + duration, + durationUnit, + repeat, + start, + end, + }: { + teamId: string; + monitorIDs: string[]; + name: string; + active: boolean; + duration: number; + durationUnit: DurationUnit; + repeat: number; + start: string; + end: string; + }) => { + const monitors = await this.monitorsRepository.findByIds(monitorIDs); const unauthorizedMonitors = monitors.filter((monitor) => monitor.teamId !== teamId); @@ -55,17 +84,17 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { }); } - const dbTransactions = monitorIds.map((monitorId: string) => { + const dbTransactions = monitorIDs.map((monitorId: string) => { return this.maintenanceWindowsRepository.create({ teamId, monitorId, - name: body.name, - active: body.active ? body.active : true, - duration: body.duration, - durationUnit: body.durationUnit, - repeat: body.repeat, - start: body.start, - end: body.end, + name: name, + active: active, + duration: duration, + durationUnit: durationUnit, + repeat: repeat, + start: start, + end: end, }); }); await Promise.all(dbTransactions); @@ -106,7 +135,7 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { return await this.maintenanceWindowsRepository.deleteById(id, teamId); }; - editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: any }) => { + editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: Partial }) => { return await this.maintenanceWindowsRepository.updateById(id, teamId, body); }; }