diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx index c5b62280f..0793e7149 100644 --- a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx +++ b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx @@ -124,7 +124,7 @@ const MonitorDetailsControlHeader = ({ }); }} > - {monitor?.isActive ? "Pause" : "Resume"} + {monitor?.isActive ? t("pause") : t("resume")} )} {isAdmin && ( diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index b1460dba6..01fc9eb7a 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -80,13 +80,13 @@ const UptimeCreate = ({ isClone = false }) => { const [isFetchingMonitor] = useFetchMonitorById({ monitorId, setMonitor, - updateTrigger: true, + updateTrigger, }); // Fetch games const [isFetchingGames] = useFetchMonitorGames({ setGames, - triggerUpdate: true, + updateTrigger, }); // Combine the loading states @@ -283,10 +283,6 @@ const UptimeCreate = ({ isClone = false }) => { })); }; - const handlePause = async () => { - await pauseMonitor({ monitorId, triggerUpdate }); - }; - const handleRemove = async (event) => { event.preventDefault(); const TEMP_MONITOR = { id: monitor.id }; @@ -438,7 +434,12 @@ const UptimeCreate = ({ isClone = false }) => { /> ) } - onClick={handlePause} + onClick={() => { + pauseMonitor({ + monitorId: monitor?.id, + triggerUpdate, + }); + }} > {monitor?.isActive ? t("pause") : t("resume")} diff --git a/server/package-lock.json b/server/package-lock.json index a1a392b18..3f5b824b0 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -36,7 +36,6 @@ "mongoose": "^8.3.3", "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.14", - "papaparse": "^5.5.2", "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", @@ -46,6 +45,9 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/compression": "1.8.1", + "@types/cookie-parser": "1.4.10", + "@types/cors": "2.8.19", "@types/dockerode": "^4.0.0", "@types/express": "5.0.3", "@types/gamedig": "^5.0.3", @@ -58,6 +60,7 @@ "@types/nodemailer": "7.0.1", "@types/papaparse": "^5.5.2", "@types/ping": "0.4.4", + "@types/swagger-ui-express": "4.1.8", "c8": "10.1.3", "eslint": "^9.17.0", "eslint-plugin-mocha": "^10.5.0", @@ -4324,6 +4327,17 @@ "@types/node": "*" } }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -4334,6 +4348,26 @@ "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/docker-modem": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", @@ -4370,6 +4404,7 @@ "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4650,6 +4685,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -11567,12 +11613,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, - "node_modules/papaparse": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", - "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", - "license": "MIT" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/server/package.json b/server/package.json index 2bf898c94..2421aeb1c 100755 --- a/server/package.json +++ b/server/package.json @@ -48,7 +48,6 @@ "mongoose": "^8.3.3", "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.14", - "papaparse": "^5.5.2", "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", @@ -58,6 +57,9 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/compression": "1.8.1", + "@types/cookie-parser": "1.4.10", + "@types/cors": "2.8.19", "@types/dockerode": "^4.0.0", "@types/express": "5.0.3", "@types/gamedig": "^5.0.3", @@ -70,6 +72,7 @@ "@types/nodemailer": "7.0.1", "@types/papaparse": "^5.5.2", "@types/ping": "0.4.4", + "@types/swagger-ui-express": "4.1.8", "c8": "10.1.3", "eslint": "^9.17.0", "eslint-plugin-mocha": "^10.5.0", diff --git a/server/src/app.js b/server/src/app.ts similarity index 69% rename from server/src/app.js rename to server/src/app.ts index c83726a49..04699964d 100644 --- a/server/src/app.js +++ b/server/src/app.ts @@ -5,12 +5,27 @@ import helmet from "helmet"; import compression from "compression"; import cookieParser from "cookie-parser"; import swaggerUi from "swagger-ui-express"; -import { handleErrors } from "./middleware/handleErrors.js"; -import { setupRoutes } from "./config/routes.js"; -import { generalApiLimiter } from "./middleware/rateLimiter.js"; -import { sanitizeBody, sanitizeQuery } from "./middleware/sanitization.js"; +import { handleErrors } from "@/middleware/handleErrors.js"; +import { generalApiLimiter } from "@/middleware/rateLimiter.js"; +import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js"; +import { setupRoutes } from "@/config/routes.js"; +import { InitializedServices } from "@/config/services.js"; +import { InitializedControllers } from "@/config/controllers.js"; +import { EnvConfig } from "@/service/system/settingsService.js"; -export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => { +export const createApp = ({ + services, + controllers, + envSettings, + frontendPath, + openApiSpec, +}: { + services: InitializedServices; + controllers: InitializedControllers; + envSettings: EnvConfig; + frontendPath: string; + openApiSpec: any; +}) => { const allowedOrigin = envSettings.clientHost; const app = express(); app.use(generalApiLimiter); diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 5ddbf2a06..d72e4f76f 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -10,26 +10,35 @@ import StatusPageController from "../controllers/statusPageController.js"; import NotificationController from "../controllers/notificationController.js"; import DiagnosticController from "../controllers/diagnosticController.js"; import IncidentController from "../controllers/incidentController.js"; -import type { InitializedSerivces } from "@/config/services.js"; -export const initializeControllers = (services: InitializedSerivces) => { - const controllers: Record = {}; +import type { InitializedServices } from "@/config/services.js"; - controllers.authController = new AuthController(services.userService); - - controllers.monitorController = new MonitorController(services.monitorService); - - controllers.settingsController = new SettingsController(services.settingsService, services.emailService, services.db); - controllers.checkController = new CheckController(services.checkService); - controllers.inviteController = new InviteController(services.inviteService); - - controllers.maintenanceWindowController = new MaintenanceWindowController(services.maintenanceWindowService); - controllers.queueController = new QueueController(services.jobQueue); - controllers.logController = new LogController(services.logger); - controllers.statusPageController = new StatusPageController(services.db); - controllers.notificationController = new NotificationController(services.notificationsService, services.db, services.monitorsRepository); - controllers.diagnosticController = new DiagnosticController(services.diagnosticService); - - controllers.incidentController = new IncidentController(services.incidentService); - - return controllers; +export interface InitializedControllers { + authController: AuthController; + monitorController: MonitorController; + settingsController: SettingsController; + checkController: CheckController; + inviteController: InviteController; + maintenanceWindowController: MaintenanceWindowController; + queueController: QueueController; + logController: LogController; + statusPageController: StatusPageController; + notificationController: NotificationController; + diagnosticController: DiagnosticController; + incidentController: IncidentController; +} +export const initializeControllers = (services: InitializedServices): InitializedControllers => { + return { + authController: new AuthController(services.userService), + monitorController: new MonitorController(services.monitorService), + settingsController: new SettingsController(services.settingsService, services.emailService, services.db), + checkController: new CheckController(services.checkService), + inviteController: new InviteController(services.inviteService), + maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService), + queueController: new QueueController(services.jobQueue), + logController: new LogController(services.logger), + statusPageController: new StatusPageController(services.db), + notificationController: new NotificationController(services.notificationsService, services.db, services.monitorsRepository), + diagnosticController: new DiagnosticController(services.diagnosticService), + incidentController: new IncidentController(services.incidentService), + }; }; diff --git a/server/src/config/database.js b/server/src/config/database.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/server/src/config/index.ts b/server/src/config/index.ts deleted file mode 100644 index 7ace6f99c..000000000 --- a/server/src/config/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import dotenv from "dotenv"; - -export interface IEevConfig { - NODE_ENV: string; - JWT_SECRET: string; - PORT: number; - PAGESPEED_API_KEY: string; - SMTP_HOST: string; - SMTP_PORT: number; - SMTP_USER: string; - SMTP_PASS: string; -} - -dotenv.config(); - -export const config: IEevConfig = { - NODE_ENV: process.env.NODE_ENV || "development", - JWT_SECRET: process.env.JWT_SECRET || "your_jwt_secret", - PORT: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, - PAGESPEED_API_KEY: process.env.PAGESPEED_API_KEY || "", - SMTP_HOST: process.env.SMTP_HOST || "smtp.example.com", - SMTP_PORT: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587, - SMTP_USER: process.env.SMTP_USER || "user@example.com", - SMTP_PASS: process.env.SMTP_PASS || "your_smtp_password", -}; diff --git a/server/src/config/routes.ts b/server/src/config/routes.ts index 024387c25..d38ac69ae 100644 --- a/server/src/config/routes.ts +++ b/server/src/config/routes.ts @@ -1,23 +1,22 @@ import { createVerifyJWT } from "../middleware/verifyJWT.js"; import { authApiLimiter } from "../middleware/rateLimiter.js"; -import AuthRoutes from "../routes/v1/authRoute.js"; -import InviteRoutes from "../routes/v1/inviteRoute.js"; -import MonitorRoutes from "../routes/v1/monitorRoute.js"; -import CheckRoutes from "../routes/v1/checkRoute.js"; -import SettingsRoutes from "../routes/v1/settingsRoute.js"; -import MaintenanceWindowRoutes from "../routes/v1/maintenanceWindowRoute.js"; -import StatusPageRoutes from "../routes/v1/statusPageRoute.js"; -import QueueRoutes from "../routes/v1/queueRoute.js"; -import LogRoutes from "../routes/v1/logRoutes.js"; -import DiagnosticRoutes from "../routes/v1/diagnosticRoute.js"; -import NotificationRoutes from "../routes/v1/notificationRoute.js"; +import AuthRoutes from "../routes/authRoute.js"; +import InviteRoutes from "../routes/inviteRoute.js"; +import MonitorRoutes from "../routes/monitorRoute.js"; +import CheckRoutes from "../routes/checkRoute.js"; +import SettingsRoutes from "../routes/settingsRoute.js"; +import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js"; +import StatusPageRoutes from "../routes/statusPageRoute.js"; +import QueueRoutes from "../routes/queueRoute.js"; +import LogRoutes from "../routes/logRoutes.js"; +import DiagnosticRoutes from "../routes/diagnosticRoute.js"; +import NotificationRoutes from "../routes/notificationRoute.js"; -import IncidentRoutes from "../routes/v1/incidentRoute.js"; +import IncidentRoutes from "../routes/incidentRoute.js"; export const setupRoutes = (app: any, controllers: Record, services: Record) => { const verifyJWT = createVerifyJWT(services.settingsService); - // V1 const authRoutes = new AuthRoutes(controllers.authController, verifyJWT); const monitorRoutes = new MonitorRoutes(controllers.monitorController); const settingsRoutes = new SettingsRoutes(controllers.settingsController); diff --git a/server/src/config/services.ts b/server/src/config/services.ts index de4d5115f..8cfcb400b 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -1,4 +1,3 @@ -import TranslationService from "../service/system/translationService.js"; import MongoDB from "../db/MongoDB.js"; import NetworkService from "../service/infrastructure/networkService.js"; import EmailService from "../service/infrastructure/emailService.js"; @@ -24,7 +23,6 @@ import InviteService from "../service/business/inviteService.js"; import MaintenanceWindowService from "../service/business/maintenanceWindowService.js"; import { MonitorService } from "@/service/index.js"; import IncidentService from "../service/business/incidentService.js"; -import papaparse from "papaparse"; import axios from "axios"; import got from "got"; import ping from "ping"; @@ -82,6 +80,7 @@ import { MongoSettingsRepository, MongoNotificationsRepository, MongoIncidentRepository, + MongoTeamsRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, @@ -92,12 +91,13 @@ import { ISettingsRepository, INotificationsRepository, IIncidentsRepository, + ITeamsRepository, } from "@/repositories/index.js"; +import { ILogger } from "@/utils/logger.js"; +import { EnvConfig } from "@/service/system/settingsService.js"; -export type InitializedSerivces = { - //v1 +export type InitializedServices = { settingsService: any; - translationService: any; db: any; networkService: any; emailService: any; @@ -126,6 +126,7 @@ export type InitializedSerivces = { settingsRepository: ISettingsRepository; notificationsRepository: INotificationsRepository; incidentsRepository: IIncidentsRepository; + teamsRepository: ITeamsRepository; }; export const initializeServices = async ({ @@ -133,13 +134,10 @@ export const initializeServices = async ({ envSettings, settingsService, }: { - logger: any; - envSettings: any; + logger: ILogger; + envSettings: EnvConfig; settingsService: any; -}): Promise => { - const translationService = new TranslationService(logger); - await translationService.initialize(); - +}): Promise => { // Create DB const inviteModule = new InviteModule({ InviteToken, crypto }); const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, AppSettings }); @@ -176,6 +174,8 @@ export const initializeServices = async ({ const settingsRepository = new MongoSettingsRepository(); const notificationsRepository = new MongoNotificationsRepository(); const incidentsRepository = new MongoIncidentRepository(); + const teamsRepository = new MongoTeamsRepository(); + const networkService = new NetworkService({ axios, got, @@ -206,7 +206,7 @@ export const initializeServices = async ({ checksRepository, }); - const bufferService = new BufferService({ logger, checkService }); + const bufferService = new BufferService({ logger, checkService, settingsService }); const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository }); @@ -259,6 +259,7 @@ export const initializeServices = async ({ invitesRepository, recoveryTokensRepository, settingsRepository, + teamsRepository, }); const diagnosticService = new DiagnosticService(); @@ -276,7 +277,6 @@ export const initializeServices = async ({ const monitorService = new MonitorService({ jobQueue: superSimpleQueue, emailService, - papaparse, logger, errorService, games, @@ -289,7 +289,6 @@ export const initializeServices = async ({ const services = { //v1 settingsService, - translationService, db, networkService, emailService, @@ -318,6 +317,7 @@ export const initializeServices = async ({ settingsRepository, notificationsRepository, incidentsRepository, + teamsRepository, }; return services; diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index 93783b20c..6231f3cf4 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -1,5 +1,6 @@ import { AppError } from "@/utils/AppError.js"; import { Request, Response, NextFunction } from "express"; +import { requireTeamId } from "./controllerUtils.js"; const SERVICE_NAME = "incidentController"; @@ -17,10 +18,19 @@ class IncidentController { getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => { try { - const result = await this.incidentService.getIncidentsByTeam({ - teamId: req?.user?.teamId, - query: req?.query, - }); + const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {}; + + const teamId = requireTeamId(req.user?.teamId); + const result = await this.incidentService.getIncidentsByTeam( + teamId, + sortOrder, + dateRange, + page, + rowsPerPage, + status, + monitorId, + resolutionType + ); return res.status(200).json({ success: true, diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 2ebb113ae..30ceabbc5 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -174,27 +174,15 @@ class MonitorController { createBulkMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - if (!req.file) { - throw new AppError({ message: "No file uploaded", status: 400 }); + const userId = req.user?.id; + if (!userId) { + throw new AppError({ message: "User ID is required", status: 400 }); } - if (!req.file.mimetype.includes("csv")) { - throw new AppError({ message: "File is not a CSV", status: 400 }); - } - - if (req.file.size === 0) { - throw new AppError({ message: "File is empty", status: 400 }); - } - - const userId = requireString(req?.user?.id, "User ID"); const teamId = requireTeamId(req?.user?.teamId); - const fileData = req?.file?.buffer?.toString("utf-8"); - if (!fileData) { - throw new AppError({ message: "Cannot get file from buffer", status: 400 }); - } - - const monitors = await this.monitorService.createBulkMonitors(fileData, userId, teamId); + const data = req.body; + const monitors = await this.monitorService.createBulkMonitors(data, userId, teamId); return res.status(200).json({ success: true, diff --git a/server/src/db/models/Team.js b/server/src/db/models/Team.js deleted file mode 100755 index 95337c201..000000000 --- a/server/src/db/models/Team.js +++ /dev/null @@ -1,14 +0,0 @@ -import mongoose from "mongoose"; -const TeamSchema = mongoose.Schema( - { - email: { - type: String, - required: true, - unique: true, - }, - }, - { - timestamps: true, - } -); -export default mongoose.model("Team", TeamSchema); diff --git a/server/src/db/models/Team.ts b/server/src/db/models/Team.ts new file mode 100755 index 000000000..608f90ad8 --- /dev/null +++ b/server/src/db/models/Team.ts @@ -0,0 +1,28 @@ +import { Schema, model, type Types } from "mongoose"; + +import type { Team } from "@/types/index.js"; + +type TeamDocumentBase = Omit & {}; +interface TeamDocument extends TeamDocumentBase { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const TeamSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + }, + }, + { + timestamps: true, + } +); +const TeamModel = model("Team", TeamSchema); + +export type { TeamDocument }; +export { TeamModel }; +export default TeamModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 4b71937bb..f762027ec 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -27,3 +27,6 @@ export { default as NotificationModel } from "@/db/models/Notification.js"; export * from "@/db/models/Incident.js"; export { default as IncidentModel } from "@/db/models/Incident.js"; + +export * from "@/db/models/Team.js"; +export { default as TeamModel } from "@/db/models/Team.js"; diff --git a/server/src/index.js b/server/src/index.ts similarity index 92% rename from server/src/index.js rename to server/src/index.ts index 28dc5e8fd..b74404228 100755 --- a/server/src/index.js +++ b/server/src/index.ts @@ -7,12 +7,12 @@ import path from "path"; import fs from "fs"; import { runMigrations } from "./db/migration/index.js"; -import Logger from "./utils/logger.js"; -import SettingsService from "./service/system/settingsService.js"; +import Logger, { ILogger } from "@/utils/logger.js"; +import SettingsService from "@/service/system/settingsService.js"; import AppSettings from "./db/models/AppSettings.js"; const SERVICE_NAME = "Server"; -let logger; +let logger: ILogger; const startApp = async () => { // FE path diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 72dfd2f40..ac5125af9 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -27,3 +27,6 @@ export { default as MongoNotificationsRepository } from "@/repositories/notifica export * from "@/repositories/incidents/IIncidentsRepository.js"; export { default as MongoIncidentRepository } from "@/repositories/incidents/MongoIncidentRepository.js"; + +export * from "@/repositories/teams/ITeamsRepository.js"; +export { default as MongoTeamsRepository } from "@/repositories/teams/MongoTeamsRepository.js"; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 29ebe37be..f29cf9d15 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -1,4 +1,4 @@ -import { type MonitorType, type Monitor } from "@/types/index.js"; +import { type MonitorType, type Monitor, type MonitorsSummary } from "@/types/index.js"; export interface TeamQueryConfig { limit?: number; @@ -37,6 +37,6 @@ export interface IMonitorsRepository { findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise; // other - findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; + findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; findGroupsByTeamId(teamId: string): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 878d6f7cf..17b9ce243 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,6 +1,6 @@ import { MonitorModel } from "@/db/models/index.js"; import type { MonitorDocument } from "@/db/models/index.js"; -import type { Monitor } from "@/types/index.js"; +import type { Monitor, MonitorsSummary } from "@/types/index.js"; import mongoose, { type FilterQuery } from "mongoose"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import { AppError } from "@/utils/AppError.js"; @@ -146,10 +146,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { return { monitors: this.mapDocuments(monitors), deletedCount }; }; - findMonitorsSummaryByTeamId = async ( - teamId: string, - config?: SummaryConfig - ): Promise<{ totalMonitors: number; upMonitors: number; downMonitors: number; pausedMonitors: number }> => { + findMonitorsSummaryByTeamId = async (teamId: string, config?: SummaryConfig): Promise => { const match: FilterQuery = { teamId: new mongoose.Types.ObjectId(teamId) }; if (config?.type !== undefined) { match.type = Array.isArray(config.type) ? { $in: config.type } : config.type; diff --git a/server/src/repositories/teams/ITeamsRepository.ts b/server/src/repositories/teams/ITeamsRepository.ts new file mode 100644 index 000000000..9ccdd97fb --- /dev/null +++ b/server/src/repositories/teams/ITeamsRepository.ts @@ -0,0 +1,9 @@ +import type { Team } from "@/types/index.js"; +export interface ITeamsRepository { + // create + create(email: string): Promise; + // fetch + // update + // delete + // other +} diff --git a/server/src/repositories/teams/MongoTeamsRepository.ts b/server/src/repositories/teams/MongoTeamsRepository.ts new file mode 100644 index 000000000..93a306787 --- /dev/null +++ b/server/src/repositories/teams/MongoTeamsRepository.ts @@ -0,0 +1,36 @@ +import { Team } from "@/types/index.js"; +import { TeamDocument, TeamModel } from "@/db/models/index.js"; +import { ITeamsRepository } from "@/repositories/index.js"; +import mongoose from "mongoose"; + +class MongoTeamsRepository implements ITeamsRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + private toEntity = (doc: TeamDocument): Team => { + return { + id: this.toStringId(doc._id), + email: doc.email, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (email: string) => { + const team = await TeamModel.create({ email }); + return this.toEntity(team); + }; +} + +export default MongoTeamsRepository; diff --git a/server/src/routes/v1/authRoute.js b/server/src/routes/authRoute.ts similarity index 83% rename from server/src/routes/v1/authRoute.js rename to server/src/routes/authRoute.ts index 0eca250e5..eec6ea11b 100755 --- a/server/src/routes/v1/authRoute.js +++ b/server/src/routes/authRoute.ts @@ -1,17 +1,20 @@ -import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { Router, RequestHandler } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; import multer from "multer"; const upload = multer(); class AuthRoutes { - constructor(authController, verifyJWT) { + private router: Router; + private authController: any; + + constructor(authController: any, verifyJWT: RequestHandler) { this.router = Router(); this.authController = authController; this.initRoutes(verifyJWT); } - initRoutes(verifyJWT) { + initRoutes(verifyJWT: RequestHandler) { this.router.post("/register", upload.single("profileImage"), this.authController.registerUser); this.router.post("/login", this.authController.loginUser); diff --git a/server/src/routes/v1/checkRoute.js b/server/src/routes/checkRoute.ts similarity index 83% rename from server/src/routes/v1/checkRoute.js rename to server/src/routes/checkRoute.ts index 2c34049bb..4df286846 100755 --- a/server/src/routes/v1/checkRoute.js +++ b/server/src/routes/checkRoute.ts @@ -1,9 +1,12 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../middleware/isAllowed.js"; class CheckRoutes { - constructor(checkController) { + private router: Router; + private checkController: any; + + constructor(checkController: any) { this.router = Router(); this.checkController = checkController; this.initRoutes(); diff --git a/server/src/routes/v1/diagnosticRoute.js b/server/src/routes/diagnosticRoute.ts similarity index 54% rename from server/src/routes/v1/diagnosticRoute.js rename to server/src/routes/diagnosticRoute.ts index 9ec470e53..b2cefe4aa 100755 --- a/server/src/routes/v1/diagnosticRoute.js +++ b/server/src/routes/diagnosticRoute.ts @@ -1,14 +1,17 @@ -import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { RequestHandler, Router } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; class DiagnosticRoutes { - constructor(diagnosticController, verifyJWT) { + private router: Router; + private diagnosticController: any; + + constructor(diagnosticController: any, verifyJWT: RequestHandler) { this.router = Router(); this.diagnosticController = diagnosticController; this.initRoutes(verifyJWT); } - initRoutes(verifyJWT) { + initRoutes(verifyJWT: RequestHandler) { this.router.get("/system", verifyJWT, isAllowed(["admin", "superadmin"]), this.diagnosticController.getSystemStats); } diff --git a/server/src/routes/v1/incidentRoute.js b/server/src/routes/incidentRoute.ts similarity index 80% rename from server/src/routes/v1/incidentRoute.js rename to server/src/routes/incidentRoute.ts index f87ef17a7..83f1ff8f9 100644 --- a/server/src/routes/v1/incidentRoute.js +++ b/server/src/routes/incidentRoute.ts @@ -1,8 +1,11 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../middleware/isAllowed.js"; class IncidentRoutes { - constructor(incidentController) { + private router: Router; + private incidentController: any; + + constructor(incidentController: any) { this.router = Router(); this.incidentController = incidentController; this.initRoutes(); diff --git a/server/src/routes/v1/inviteRoute.js b/server/src/routes/inviteRoute.ts similarity index 64% rename from server/src/routes/v1/inviteRoute.js rename to server/src/routes/inviteRoute.ts index 44ec0495f..b91f922df 100755 --- a/server/src/routes/v1/inviteRoute.js +++ b/server/src/routes/inviteRoute.ts @@ -1,14 +1,17 @@ -import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { RequestHandler, Router } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; class InviteRoutes { - constructor(inviteController, verifyJWT) { + private router: Router; + private inviteController: any; + + constructor(inviteController: any, verifyJWT: RequestHandler) { this.router = Router(); this.inviteController = inviteController; this.initRoutes(verifyJWT); } - initRoutes(verifyJWT) { + initRoutes(verifyJWT: RequestHandler) { this.router.post("/send", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.sendInviteEmail); this.router.post("/verify", this.inviteController.verifyInviteToken); this.router.post("/", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.getInviteToken); diff --git a/server/src/routes/v1/logRoutes.js b/server/src/routes/logRoutes.ts similarity index 68% rename from server/src/routes/v1/logRoutes.js rename to server/src/routes/logRoutes.ts index feabe669a..c35361fd0 100755 --- a/server/src/routes/v1/logRoutes.js +++ b/server/src/routes/logRoutes.ts @@ -1,7 +1,10 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../middleware/isAllowed.js"; class LogRoutes { - constructor(logController) { + private router: Router; + private logController: any; + + constructor(logController: any) { this.router = Router(); this.logController = logController; this.initRoutes(); diff --git a/server/src/routes/v1/maintenanceWindowRoute.js b/server/src/routes/maintenanceWindowRoute.ts similarity index 87% rename from server/src/routes/v1/maintenanceWindowRoute.js rename to server/src/routes/maintenanceWindowRoute.ts index 345f2c13c..0230ba4bd 100755 --- a/server/src/routes/v1/maintenanceWindowRoute.js +++ b/server/src/routes/maintenanceWindowRoute.ts @@ -1,7 +1,10 @@ import { Router } from "express"; class MaintenanceWindowRoutes { - constructor(maintenanceWindowController) { + private router: Router; + private mwController: any; + + constructor(maintenanceWindowController: any) { this.router = Router(); this.mwController = maintenanceWindowController; this.initRoutes(); diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/monitorRoute.ts similarity index 94% rename from server/src/routes/v1/monitorRoute.ts rename to server/src/routes/monitorRoute.ts index 4488a2c49..eb5604c24 100755 --- a/server/src/routes/v1/monitorRoute.ts +++ b/server/src/routes/monitorRoute.ts @@ -1,7 +1,7 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "@/middleware/isAllowed.js"; import multer from "multer"; -import { fetchMonitorCertificate } from "../../controllers/controllerUtils.js"; +import { fetchMonitorCertificate } from "@/controllers/controllerUtils.js"; const upload = multer({ storage: multer.memoryStorage(), // Store file in memory as Buffer }); diff --git a/server/src/routes/v1/notificationRoute.js b/server/src/routes/notificationRoute.ts similarity index 88% rename from server/src/routes/v1/notificationRoute.js rename to server/src/routes/notificationRoute.ts index f88b0b96c..7fe4bf5cb 100755 --- a/server/src/routes/v1/notificationRoute.js +++ b/server/src/routes/notificationRoute.ts @@ -1,6 +1,9 @@ import { Router } from "express"; class NotificationRoutes { - constructor(notificationController) { + private router: Router; + private notificationController: any; + + constructor(notificationController: any) { this.router = Router(); this.notificationController = notificationController; this.initializeRoutes(); diff --git a/server/src/routes/v1/queueRoute.js b/server/src/routes/queueRoute.ts similarity index 84% rename from server/src/routes/v1/queueRoute.js rename to server/src/routes/queueRoute.ts index 59e1cb744..d5b036b15 100755 --- a/server/src/routes/v1/queueRoute.js +++ b/server/src/routes/queueRoute.ts @@ -1,7 +1,10 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../middleware/isAllowed.js"; class QueueRoutes { - constructor(queueController) { + private router: Router; + private queueController: any; + + constructor(queueController: any) { this.router = Router(); this.queueController = queueController; this.initRoutes(); diff --git a/server/src/routes/v1/settingsRoute.js b/server/src/routes/settingsRoute.ts similarity index 77% rename from server/src/routes/v1/settingsRoute.js rename to server/src/routes/settingsRoute.ts index 76556e923..c7db1ed3b 100755 --- a/server/src/routes/v1/settingsRoute.js +++ b/server/src/routes/settingsRoute.ts @@ -1,8 +1,11 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../middleware/isAllowed.js"; class SettingsRoutes { - constructor(settingsController) { + private router: Router; + private settingsController: any; + + constructor(settingsController: any) { this.router = Router(); this.settingsController = settingsController; this.initRoutes(); diff --git a/server/src/routes/v1/statusPageRoute.js b/server/src/routes/statusPageRoute.ts similarity index 78% rename from server/src/routes/v1/statusPageRoute.js rename to server/src/routes/statusPageRoute.ts index 7d915cb86..9d6902aac 100755 --- a/server/src/routes/v1/statusPageRoute.js +++ b/server/src/routes/statusPageRoute.ts @@ -1,15 +1,18 @@ -import { Router } from "express"; +import { RequestHandler, Router } from "express"; import multer from "multer"; const upload = multer(); class StatusPageRoutes { - constructor(statusPageController, verifyJWT) { + private router: Router; + private statusPageController: any; + + constructor(statusPageController: any, verifyJWT: RequestHandler) { this.router = Router(); this.statusPageController = statusPageController; this.initRoutes(verifyJWT); } - initRoutes(verifyJWT) { + initRoutes(verifyJWT: RequestHandler) { this.router.get("/", this.statusPageController.getStatusPage); this.router.get("/team", verifyJWT, this.statusPageController.getStatusPagesByTeamId); diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 5ababc5b4..e3fb4cfc3 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -124,13 +124,21 @@ class IncidentService { } }; - getIncidentsByTeam = async ({ teamId, query }: { teamId: string; query?: any }) => { + getIncidentsByTeam = async ( + teamId: string, + sortOrder: string, + dateRange: string, + page: string, + rowsPerPage: string, + status: string, + monitorId: string, + resolutionType: string + ) => { try { if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); } - const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = query || {}; const startDate = dateRangeLookup[dateRange]; const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index e9e10de04..4f97bdf0f 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -1,13 +1,20 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; import { type Monitor } from "@/types/index.js"; -import type { MonitorType } from "@/types/monitor.js"; +import type { + MonitorType, + MonitorsWithChecksByTeamIdResult, + UptimeDetailsResult, + HardwareDetailsResult, + PageSpeedDetailsResult, +} from "@/types/monitor.js"; import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js"; import fs from "fs"; import { fileURLToPath } from "url"; import path from "path"; import { AppError } from "../infrastructure/errorService.js"; +import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; const SERVICE_NAME = "MonitorService"; type DateRangeKey = "recent" | "day" | "week" | "month" | "all"; @@ -17,13 +24,13 @@ export interface IMonitorService { // create createMonitor(teamId: string, userId: string, body: Monitor): Promise; - createBulkMonitors(fileData: string, userId: string, teamId: string): Promise; - addDemoMonitors(args: { userId: string; teamId: string }): Promise; + createBulkMonitors(monitors: Array, userId: string, teamId: string): Promise; + addDemoMonitors(args: { userId: string; teamId: string }): Promise; // read - getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; - getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; - getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; + getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; + getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; + getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; getMonitorById(args: { teamId: string; monitorId: string }): Promise; getMonitorsByTeamId(args: { teamId: string; @@ -34,7 +41,7 @@ export interface IMonitorService { filter?: string; field?: string; order?: "asc" | "desc"; - }): Promise; + }): Promise; getMonitorsWithChecksByTeamId(args: { teamId: string; limit?: number; @@ -45,7 +52,7 @@ export interface IMonitorService { field?: string; order?: "asc" | "desc"; explain?: boolean; - }): Promise<{ summary: any; count: number; monitors: any[] }>; + }): Promise; getAllGames(): any; getGroupsByTeamId(args: { teamId: string }): Promise; @@ -59,15 +66,14 @@ export interface IMonitorService { // other sendTestEmail(args: { to: string }): Promise; - exportMonitorsToJSON(args: { teamId: string }): Promise; + exportMonitorsToJSON(args: { teamId: string }): Promise; } export class MonitorService implements IMonitorService { static SERVICE_NAME = SERVICE_NAME; - private jobQueue: any; + private jobQueue: ISuperSimpleQueue; private emailService: any; - private papaparse: any; private logger: any; private errorService: any; private games: any; @@ -79,7 +85,6 @@ export class MonitorService implements IMonitorService { constructor({ jobQueue, emailService, - papaparse, logger, errorService, games, @@ -88,9 +93,8 @@ export class MonitorService implements IMonitorService { monitorStatsRepository, statusPagesRepository, }: { - jobQueue: any; + jobQueue: ISuperSimpleQueue; emailService: any; - papaparse: any; logger: any; errorService: any; games: any; @@ -101,7 +105,6 @@ export class MonitorService implements IMonitorService { }) { this.jobQueue = jobQueue; this.emailService = emailService; - this.papaparse = papaparse; this.logger = logger; this.errorService = errorService; this.games = games; @@ -149,63 +152,14 @@ export class MonitorService implements IMonitorService { this.jobQueue.addJob(monitor.id, monitor); }; - createBulkMonitors = async (fileData: string, userId: string, teamId: string): Promise => { - const { parse } = this.papaparse; + createBulkMonitors = async (monitors: Array, userId: string, teamId: string): Promise => { + const createdMonitors = await this.monitorsRepository.createBulkMonitors(monitors); + if (!monitors || monitors.length === 0) { + throw new AppError("Failed to create monitors", 500); + } - return new Promise((resolve, reject) => { - parse(fileData, { - header: true, - skipEmptyLines: true, - transform: (value: string, header: string): string | number | undefined => { - if (value === "") return undefined; // Empty fields become undefined - - // Handle 'port' and 'interval' fields, check if they're valid numbers - if (["port", "interval"].includes(header)) { - const num = parseInt(value, 10); - if (isNaN(num)) { - throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`); - } - return num; - } - - return value; - }, - complete: async ({ data, errors }: { data: any[]; errors: Error[] }): Promise => { - try { - if (errors.length > 0) { - throw this.errorService.createServerError("Error parsing CSV"); - } - - if (!data || data.length === 0) { - throw this.errorService.createServerError("CSV file contains no data rows"); - } - - const enrichedData = data.map((monitor: Monitor) => ({ - ...monitor, - userId, - teamId, - description: monitor.description || monitor.name || monitor.url, - name: monitor.name || monitor.url, - type: monitor.type || "http", - })); - - await createMonitorsBodyValidation.validateAsync(enrichedData); - - const monitors = await this.monitorsRepository.createBulkMonitors(enrichedData); - - await Promise.all( - monitors.map(async (monitor: Monitor) => { - this.jobQueue.addJob(monitor.id, monitor); - }) - ); - - resolve(monitors); - } catch (error) { - reject(error); - } - }, - }); - }); + await Promise.all(createdMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor))); + return createdMonitors; }; addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise => { @@ -241,7 +195,7 @@ export class MonitorService implements IMonitorService { monitorId: string; dateRange: string; normalize?: boolean; - }): Promise => { + }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -276,7 +230,15 @@ export class MonitorService implements IMonitorService { }; }; - getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { + getHardwareDetailsById = async ({ + teamId, + monitorId, + dateRange, + }: { + teamId: string; + monitorId: string; + dateRange: string; + }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -307,7 +269,15 @@ export class MonitorService implements IMonitorService { }; }; - getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { + getPageSpeedDetailsById = async ({ + teamId, + monitorId, + dateRange, + }: { + teamId: string; + monitorId: string; + dateRange: string; + }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -336,12 +306,20 @@ export class MonitorService implements IMonitorService { monitorStats, }; }; - getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); return monitor; }; - getMonitorsByTeamId = async ({ teamId, type, filter }: { teamId: string; type?: MonitorType | MonitorType[]; filter?: string }): Promise => { + getMonitorsByTeamId = async ({ + teamId, + type, + filter, + }: { + teamId: string; + type?: MonitorType | MonitorType[]; + filter?: string; + }): Promise => { const monitors = await this.monitorsRepository.findByTeamId(teamId, { type, filter, @@ -369,7 +347,7 @@ export class MonitorService implements IMonitorService { field?: string; order?: "asc" | "desc"; explain?: boolean; - }): Promise<{ summary: any; count: number; monitors: any[] }> => { + }): Promise => { const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId); const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter }); const monitors = await this.monitorsRepository.findByTeamId(teamId, { diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index f6f489b8e..b9883f594 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -1,9 +1,15 @@ -import { IInvitesRepository, IMonitorsRepository, IRecoveryTokensRepository, IUsersRepository, ISettingsRepository } from "@/repositories/index.js"; -import Team from "@/db/models/Team.js"; +import { + IInvitesRepository, + IMonitorsRepository, + IRecoveryTokensRepository, + IUsersRepository, + ISettingsRepository, + ITeamsRepository, +} from "@/repositories/index.js"; import type { User } from "@/types/index.js"; import bcrypt from "bcryptjs"; import { AppError } from "@/utils/AppError.js"; -import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; const SERVICE_NAME = "userService"; @@ -21,6 +27,7 @@ class UserService { private invitesRepository: IInvitesRepository; private recoveryTokensRepository: IRecoveryTokensRepository; private settingsRepository: ISettingsRepository; + private teamsRepository: ITeamsRepository; constructor({ crypto, @@ -34,6 +41,7 @@ class UserService { invitesRepository, recoveryTokensRepository, settingsRepository, + teamsRepository, }: { crypto: any; emailService: any; @@ -47,6 +55,7 @@ class UserService { invitesRepository: IInvitesRepository; recoveryTokensRepository: IRecoveryTokensRepository; settingsRepository: ISettingsRepository; + teamsRepository: ITeamsRepository; }) { this.emailService = emailService; this.settingsService = settingsService; @@ -59,6 +68,7 @@ class UserService { this.invitesRepository = invitesRepository; this.recoveryTokensRepository = recoveryTokensRepository; this.settingsRepository = settingsRepository; + this.teamsRepository = teamsRepository; } get serviceName() { @@ -85,10 +95,11 @@ class UserService { const jwtSecret = this.crypto.randomBytes(64).toString("hex"); await this.settingsRepository.update({ jwtSecret }); // Create a new team - const team = new Team({ - email: user.email, - }); - user.teamId = team._id; + if (!user.email) { + throw new AppError({ message: "Email is required for first user", service: SERVICE_NAME, method: "registerUser", status: 400 }); + } + const team = await this.teamsRepository.create(user.email); + user.teamId = team.id; } const newUser = await this.usersRepository.create({ ...user }, file); diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index 89cd6a55f..461001800 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -1,4 +1,3 @@ -import { config } from "@/config/index.js"; import type { Check } from "@/types/index.js"; const SERVICE_NAME = "BufferService"; @@ -12,8 +11,9 @@ class BufferService { private bufferTimer: NodeJS.Timeout | null = null; private checksService: any; - constructor({ logger, checkService }: { logger: any; checkService: any }) { - this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute + constructor({ logger, checkService, settingsService }: { logger: any; checkService: any; settingsService: any }) { + this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute + console.log(this.BUFFER_TIMEOUT); this.logger = logger; this.checksService = checkService; this.SERVICE_NAME = SERVICE_NAME; diff --git a/server/src/service/system/settingsService.js b/server/src/service/system/settingsService.ts similarity index 65% rename from server/src/service/system/settingsService.js rename to server/src/service/system/settingsService.ts index cea46f7ed..15f86995d 100755 --- a/server/src/service/system/settingsService.js +++ b/server/src/service/system/settingsService.ts @@ -1,6 +1,17 @@ const SERVICE_NAME = "SettingsService"; -const envConfig = { +export type EnvConfig = { + jwtSecret: string | undefined; + jwtTTL: string | undefined; + systemEmailHost: string | undefined; + nodeEnv: string | undefined; + logLevel: string | undefined; + clientHost: string | undefined; + dbConnectionString: string | undefined; + port: string | undefined; +}; + +const envConfig: EnvConfig = { jwtSecret: process.env.JWT_SECRET, jwtTTL: process.env.TOKEN_TTL, systemEmailHost: process.env.SYSTEM_EMAIL_HOST, @@ -10,19 +21,20 @@ const envConfig = { dbConnectionString: process.env.DB_CONNECTION_STRING, port: process.env.PORT, }; -/** - * SettingsService - * - * This service is responsible for loading and managing the application settings. - */ -class SettingsService { - static SERVICE_NAME = "SettingsService"; - /** - * Constructs a new SettingsService - * @constructor - * @throws {Error} - */ constructor(AppSettings) { +export interface ISettingsService { + readonly serviceName: string; + loadSettings(): EnvConfig; + getSettings(): EnvConfig; + getDBSettings(): Promise>; +} + +class SettingsService implements ISettingsService { + static SERVICE_NAME = "SettingsService"; + private AppSettings: any; + private settings: EnvConfig; + + constructor(AppSettings: any) { this.AppSettings = AppSettings; this.settings = { ...envConfig }; } @@ -31,19 +43,10 @@ class SettingsService { return SettingsService.SERVICE_NAME; } - /** - * Load settings from env settings - * @returns {Object>} The settings. - */ loadSettings() { return this.settings; } - /** - * Get the current settings. - * @returns {Object} The current settings. - * @throws Will throw an error if settings have not been loaded. - */ getSettings() { if (!this.settings) { throw new Error("Settings have not been loaded"); diff --git a/server/src/service/system/translationService.js b/server/src/service/system/translationService.js deleted file mode 100755 index 0d3893852..000000000 --- a/server/src/service/system/translationService.js +++ /dev/null @@ -1,92 +0,0 @@ -import fs from "fs"; -import path from "path"; - -class TranslationService { - static SERVICE_NAME = "TranslationService"; - constructor(logger) { - this.logger = logger; - this.translations = {}; - this._language = "en"; - this.localesDir = path.join(process.cwd(), "src/locales"); - } - - get serviceName() { - return TranslationService.SERVICE_NAME; - } - - setLanguage(language) { - this._language = language; - } - - get language() { - return this._language; - } - - async initialize() { - try { - await this.loadFromFiles(); - } catch (error) { - this.logger.error({ - message: error.message, - service: "TranslationService", - method: "initialize", - stack: error.stack, - }); - } - } - - async loadFromFiles() { - try { - if (!fs.existsSync(this.localesDir)) { - return false; - } - - const files = fs.readdirSync(this.localesDir).filter((file) => file.endsWith(".json")); - - if (files.length === 0) { - return false; - } - - for (const file of files) { - const language = file.replace(".json", ""); - const filePath = path.join(this.localesDir, file); - const content = fs.readFileSync(filePath, "utf8"); - this.translations[language] = JSON.parse(content); - } - - this.logger.info({ - message: "Translations loaded from files successfully", - service: "TranslationService", - method: "loadFromFiles", - }); - - return true; - } catch (error) { - this.logger.error({ - message: error.message, - service: "TranslationService", - method: "loadFromFiles", - stack: error.stack, - }); - return false; - } - } - - getTranslation(key) { - let language = this._language; - - try { - return this.translations[language]?.[key] || this.translations["en"]?.[key] || key; - } catch (error) { - this.logger.error({ - message: error.message, - service: "TranslationService", - method: "getTranslation", - stack: error.stack, - }); - return key; - } - } -} - -export default TranslationService; diff --git a/server/src/types/check.ts b/server/src/types/check.ts index f8ee4f618..3195cc06a 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -156,11 +156,17 @@ export interface HardwareChecksResult { }>; } +export interface GroupedCheck { + _id: string; + avgResponseTime: number; + totalChecks: number; +} + export interface UptimeChecksResult { monitorType: Exclude; - groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; - groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; - groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + groupedChecks: GroupedCheck[]; + groupedUpChecks: GroupedCheck[]; + groupedDownChecks: GroupedCheck[]; uptimePercentage: number; avgResponseTime: number; } @@ -171,3 +177,11 @@ export interface ChecksSummary { downChecks: number; cannotResolveChecks: number; } + +export type NormalizedCheck = T & { + originalResponseTime: number; +}; + +export type NormalizedUptimeCheck = T & { + originalAvgResponseTime: number; +}; diff --git a/server/src/types/index.ts b/server/src/types/index.ts index b0b6b07c5..9464622ac 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -11,3 +11,4 @@ export * from "@/types/notification.js"; export * from "@/types/alert.js"; export * from "@/types/incident.js"; export * from "@/types/email.js"; +export * from "@/types/team.js"; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index dc3bd8c09..1d41a7333 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -44,3 +44,78 @@ export interface Monitor { createdAt: string; updatedAt: string; } + +export interface MonitorsSummary { + totalMonitors: number; + upMonitors: number; + downMonitors: number; + pausedMonitors: number; +} + +export interface MonitorWithChecks extends Monitor { + checks: import("./check.js").Check[]; +} + +export interface MonitorsWithChecksByTeamIdResult { + summary: MonitorsSummary | null; + count: number; + monitors: MonitorWithChecks[]; +} + +export interface UptimeDetailsResult { + monitorData: { + monitor: Monitor; + groupedChecks: import("./check.js").GroupedCheck[]; + groupedUpChecks: import("./check.js").GroupedCheck[]; + groupedDownChecks: import("./check.js").GroupedCheck[]; + groupedAvgResponseTime: number; + groupedUptimePercentage: number; + }; + monitorStats: import("./monitorStats.js").MonitorStats | null; +} + +export interface HardwareDetailsResult extends Monitor { + stats: { + aggregateData: { + latestCheck: import("./check.js").Check | null; + totalChecks: number; + }; + upChecks: { + totalChecks: number; + }; + checks: Array<{ + _id: string; + avgCpuUsage: number; + avgMemoryUsage: number; + avgTemperature: number[]; + disks: Array<{ + name: string; + readSpeed: number; + writeSpeed: number; + totalBytes: number; + freeBytes: number; + usagePercent: number; + }>; + net: Array<{ + name: string; + bytesSentPerSecond: number; + deltaBytesRecv: number; + deltaPacketsSent: number; + deltaPacketsRecv: number; + deltaErrIn: number; + deltaErrOut: number; + deltaDropIn: number; + deltaDropOut: number; + deltaFifoIn: number; + deltaFifoOut: number; + }>; + }>; + }; +} + +export interface PageSpeedDetailsResult { + monitor: Monitor & { + checks: import("./check.js").Check[]; + }; + monitorStats: import("./monitorStats.js").MonitorStats | null; +} diff --git a/server/src/types/team.ts b/server/src/types/team.ts new file mode 100644 index 000000000..1bca943a0 --- /dev/null +++ b/server/src/types/team.ts @@ -0,0 +1,6 @@ +export interface Team { + id: string; + email: string; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/utils/dataUtils.js b/server/src/utils/dataUtils.ts similarity index 53% rename from server/src/utils/dataUtils.js rename to server/src/utils/dataUtils.ts index e7fd334dd..0bf3e6416 100755 --- a/server/src/utils/dataUtils.js +++ b/server/src/utils/dataUtils.ts @@ -1,35 +1,33 @@ -const calculatePercentile = (arr, percentile) => { +import type { Check, GroupedCheck, NormalizedCheck, NormalizedUptimeCheck } from "@/types/index.js"; + +const calculatePercentile = (arr: Check[], percentile: number): number => { const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime); const index = (percentile / 100) * (sorted.length - 1); const lower = Math.floor(index); const upper = lower + 1; const weight = index % 1; - if (upper >= sorted.length) return sorted[lower].responseTime; - return sorted[lower].responseTime * (1 - weight) + sorted[upper].responseTime * weight; + if (upper >= sorted.length) return sorted[lower]!.responseTime; + return sorted[lower]!.responseTime * (1 - weight) + sorted[upper]!.responseTime * weight; }; -const calculatePercentileUptimeDetails = (arr, percentile) => { +const calculatePercentileUptimeDetails = (arr: GroupedCheck[], percentile: number): number => { const sorted = arr.slice().sort((a, b) => a.avgResponseTime - b.avgResponseTime); const index = (percentile / 100) * (sorted.length - 1); const lower = Math.floor(index); const upper = lower + 1; const weight = index % 1; - if (upper >= sorted.length) return sorted[lower].avgResponseTime; - return sorted[lower].avgResponseTime * (1 - weight) + sorted[upper].avgResponseTime * weight; + if (upper >= sorted.length) return sorted[lower]!.avgResponseTime; + return sorted[lower]!.avgResponseTime * (1 - weight) + sorted[upper]!.avgResponseTime * weight; }; -const NormalizeData = (checks, rangeMin, rangeMax) => { +export const NormalizeData = (checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck[] => { if (checks.length > 1) { - // Get the 5th and 95th percentile const min = calculatePercentile(checks, 0); const max = calculatePercentile(checks, 95); const normalizedChecks = checks.map((check) => { const originalResponseTime = check.responseTime; - // Normalize the response time between 1 and 100 let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min); - // Put a floor on the response times so we don't have extreme outliers - // Better visuals normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); return { ...check, @@ -45,19 +43,16 @@ const NormalizeData = (checks, rangeMin, rangeMax) => { }); } }; -const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => { + +export const NormalizeDataUptimeDetails = (checks: T[], rangeMin: number, rangeMax: number): NormalizedUptimeCheck[] => { if (checks.length > 1) { - // Get the 5th and 95th percentile const min = calculatePercentileUptimeDetails(checks, 0); const max = calculatePercentileUptimeDetails(checks, 95); const normalizedChecks = checks.map((check) => { const originalResponseTime = check.avgResponseTime; - // Normalize the response time between 1 and 100 let normalizedResponseTime = rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min); - // Put a floor on the response times so we don't have extreme outliers - // Better visuals normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); return { ...check, @@ -69,32 +64,7 @@ const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => { return normalizedChecks; } else { return checks.map((check) => { - return { ...check, originalResponseTime: check.responseTime }; + return { ...check, originalAvgResponseTime: check.avgResponseTime }; }); } }; - -const safelyParseFloat = (value, defaultValue = 0) => { - if (value === null || typeof value === "undefined") { - return defaultValue; - } - const stringValue = String(value).trim(); - - if (typeof value === "number" && !isNaN(value)) { - return value; - } - - if (stringValue === "") { - return defaultValue; - } - - const parsedValue = parseFloat(stringValue); - - if (isNaN(parsedValue) || !isFinite(parsedValue)) { - return defaultValue; - } - - return parsedValue; -}; - -export { safelyParseFloat, calculatePercentile, NormalizeData, calculatePercentileUptimeDetails, NormalizeDataUptimeDetails }; diff --git a/server/src/utils/demoMonitorsOld.json b/server/src/utils/demoMonitorsOld.json deleted file mode 100755 index ffce7d226..000000000 --- a/server/src/utils/demoMonitorsOld.json +++ /dev/null @@ -1,1271 +0,0 @@ -[ - { - "name": "0to255", - "url": "https://0to255.com" - }, - { - "name": "10015.io", - "url": "https://10015.io" - }, - { - "name": "3DIcons", - "url": "https://3dicons.co" - }, - { - "name": "About.me", - "url": "https://about.me" - }, - { - "name": "Alias", - "url": "https://alias.co" - }, - { - "name": "All About Berlin", - "url": "https://allaboutberlin.com" - }, - { - "name": "All Acronyms", - "url": "https://allacronyms.com" - }, - { - "name": "All You Can Read ", - "url": "https://allyoucanread.com" - }, - { - "name": "AllTrails", - "url": "https://alltrails.com" - }, - { - "name": "Anotepad", - "url": "https://anotepad.com" - }, - { - "name": "AnswerSocrates", - "url": "https://answersocrates.com" - }, - { - "name": "AnswerThePublic ", - "url": "https://answerthepublic.com" - }, - { - "name": "Apollo ", - "url": "https://apollo.io" - }, - { - "name": "ArrayList", - "url": "https://arraylist.org" - }, - { - "name": "Ask Difference", - "url": "https://askdifference.com" - }, - { - "name": "Audd.io", - "url": "https://audd.io" - }, - { - "name": "Audiocheck", - "url": "https://audiocheck.net" - }, - { - "name": "Audionautix", - "url": "https://audionautix.com" - }, - { - "name": "Authentic Jobs", - "url": "https://authenticjobs.com" - }, - { - "name": "Behind the Name", - "url": "https://behindthename.com" - }, - { - "name": "Bilim Terimleri", - "url": "https://terimler.org" - }, - { - "name": "BitBof", - "url": "https://bitbof.com" - }, - { - "name": "Blank Page", - "url": "https://blank.page" - }, - { - "name": "Bonanza", - "url": "https://bonanza.com" - }, - { - "name": "BookCrossing", - "url": "https://bookcrossing.com" - }, - { - "name": "Browse AI", - "url": "https://browse.ai" - }, - { - "name": "Bubbl.us", - "url": "https://bubbl.us" - }, - { - "name": "Business Model Toolbox", - "url": "https://bmtoolbox.net" - }, - { - "name": "ByClickDownloader", - "url": "https://byclickdownloader.com" - }, - { - "name": "Calligraphr", - "url": "https://calligraphr.com" - }, - { - "name": "CertificateClaim", - "url": "https://certificateclaim.com" - }, - { - "name": "Chosic", - "url": "https://chosic.com" - }, - { - "name": "ClipDrop", - "url": "https://clipdrop.co" - }, - { - "name": "CloudConvert", - "url": "https://cloudconvert.com" - }, - { - "name": "CodingFont", - "url": "https://codingfont.com" - }, - { - "name": "Color Hunt", - "url": "https://colorhunt.co" - }, - { - "name": "ColorHexa", - "url": "https://colorhexa.com" - }, - { - "name": "Conversion-Tool", - "url": "https://conversion-tool.com" - }, - { - "name": "Cool Startup Jobs", - "url": "https://coolstartupjobs.com" - }, - { - "name": "Coroflot", - "url": "https://coroflot.com" - }, - { - "name": "Corrupt-a-File", - "url": "https://corrupt-a-file.net" - }, - { - "name": "Couchsurfing", - "url": "https://couchsurfing.com" - }, - { - "name": "Countries Been", - "url": "https://countriesbeen.com" - }, - { - "name": "Country Code", - "url": "https://countrycode.org" - }, - { - "name": "Creately", - "url": "https://creately.com" - }, - { - "name": "Creately ", - "url": "https://creately.com" - }, - { - "name": "Crossfade.io", - "url": "https://crossfade.io" - }, - { - "name": "Crunchbase", - "url": "https://crunchbase.com" - }, - { - "name": "CVmkr", - "url": "https://cvwizard.com" - }, - { - "name": "Daily Remote", - "url": "https://dailyremote.com" - }, - { - "name": "David Li", - "url": "https://david.li" - }, - { - "name": "DemandHunt", - "url": "https://demandhunt.com" - }, - { - "name": "Designify", - "url": "https://designify.com" - }, - { - "name": "Diff Checker", - "url": "https://diffchecker.com" - }, - { - "name": "DifferenceBetween.info", - "url": "https://differencebetween.info" - }, - { - "name": "Digital Glossary", - "url": "https://digital-glossary.com" - }, - { - "name": "Dimensions", - "url": "https://dimensions.com" - }, - { - "name": "Discoverify Music", - "url": "https://discoverifymusic.com" - }, - { - "name": "discu.eu", - "url": "https://discu.eu" - }, - { - "name": "Do It Yourself", - "url": "https://doityourself.com" - }, - { - "name": "draw.io", - "url": "https://drawio.com" - }, - { - "name": "Drumeo", - "url": "https://drumeo.com" - }, - { - "name": "Dummies", - "url": "https://dummies.com" - }, - { - "name": "Easel.ly", - "url": "https://easel.ly" - }, - { - "name": "Educalingo", - "url": "https://educalingo.com" - }, - { - "name": "Emoji Combos", - "url": "https://emojicombos.com" - }, - { - "name": "EquityBee", - "url": "https://equitybee.com" - }, - { - "name": "EquityZen", - "url": "https://equityzen.com" - }, - { - "name": "Escape Room Tips", - "url": "https://escaperoomtips.com" - }, - { - "name": "Every Noise", - "url": "https://everynoise.com" - }, - { - "name": "Every Time Zone", - "url": "https://everytimezone.com" - }, - { - "name": "Excalideck", - "url": "https://excalideck.com" - }, - { - "name": "Excalidraw", - "url": "https://excalidraw.com" - }, - { - "name": "Extract pics", - "url": "https://extract.pics" - }, - { - "name": "EZGIF", - "url": "https://ezgif.com" - }, - { - "name": "FactSlides", - "url": "https://factslides.com" - }, - { - "name": "FIGR ", - "url": "https://figr.app" - }, - { - "name": "Fine Dictionary", - "url": "https://finedictionary.com" - }, - { - "name": "Fiverr", - "url": "https://fiverr.com" - }, - { - "name": "Fix It Club", - "url": "https://fixitclub.com" - }, - { - "name": "Flightradar24", - "url": "https://flightradar24.com" - }, - { - "name": "FlowCV ", - "url": "https://flowcv.com" - }, - { - "name": "Font Squirrel", - "url": "https://fontsquirrel.com" - }, - { - "name": "FontAwesome", - "url": "https://fontawesome.com" - }, - { - "name": "Fontello ", - "url": "https://fontello.com" - }, - { - "name": "Form to Chatbot", - "url": "https://formtochatbot.com" - }, - { - "name": "Founder Resources", - "url": "https://founderresources.io" - }, - { - "name": "Franz", - "url": "https://meetfranz.com" - }, - { - "name": "Fraze It", - "url": "https://fraze.it" - }, - { - "name": "Freecycle", - "url": "https://freecycle.org" - }, - { - "name": "FreeType", - "url": "https://freetype.org" - }, - { - "name": "FutureM", - "url": "https://futureme.org" - }, - { - "name": "Generated.Photos", - "url": "https://generated.photos" - }, - { - "name": "Get Human", - "url": "https://gethuman.com" - }, - { - "name": "Go Bento", - "url": "https://gobento.com" - }, - { - "name": "Good CV", - "url": "https://goodcv.com" - }, - { - "name": "Grammar Monster", - "url": "https://grammar-monster.com" - }, - { - "name": "Grammar Book", - "url": "https://grammarbook.com" - }, - { - "name": "Gummy Search", - "url": "https://gummysearch.com" - }, - { - "name": "Gumroad", - "url": "https://gumroad.com" - }, - { - "name": "HealthIcons", - "url": "https://healthicons.org" - }, - { - "name": "HexColor", - "url": "https://hexcolor.co" - }, - { - "name": "Hidden Life Radio", - "url": "https://hiddenliferadio.com" - }, - { - "name": "Hired", - "url": "https://lhh.com" - }, - { - "name": "Honey", - "url": "https://joinhoney.com" - }, - { - "name": "HowStuffWorks", - "url": "https://howstuffworks.com" - }, - { - "name": "HugeIcons Pro", - "url": "https://hugeicons.com" - }, - { - "name": "Humble Bundle", - "url": "https://humblebundle.com" - }, - { - "name": "I Have No TV", - "url": "https://ihavenotv.com" - }, - { - "name": "I Miss the Office", - "url": "https://imisstheoffice.eu" - }, - { - "name": "IcoMoon", - "url": "https://icomoon.io" - }, - { - "name": "Iconfinder", - "url": "https://iconfinder.com" - }, - { - "name": "Icon Packs", - "url": "https://iconpacks.net" - }, - { - "name": "Iconshock", - "url": "https://iconshock.com" - }, - { - "name": "Iconz Design", - "url": "https://iconz.design" - }, - { - "name": "iFixit", - "url": "https://ifixit.com" - }, - { - "name": "IFTTT", - "url": "https://ifttt.com" - }, - { - "name": "Illlustrations", - "url": "https://illlustrations.co" - }, - { - "name": "Illustration Kit", - "url": "https://illustrationkit.com" - }, - { - "name": "IMSDB", - "url": "https://imsdb.com" - }, - { - "name": "Incompetech", - "url": "https://incompetech.com" - }, - { - "name": "Incredibox", - "url": "https://incredibox.com" - }, - { - "name": "InnerBod", - "url": "https://innerbody.com" - }, - { - "name": "Instructables", - "url": "https://instructables.com" - }, - { - "name": "Integromat", - "url": "https://make.com" - }, - { - "name": "Investopedia", - "url": "https://investopedia.com" - }, - { - "name": "Japanese Wiki Corpus", - "url": "https://japanesewiki.com" - }, - { - "name": "Jitter.Video", - "url": "https://jitter.video" - }, - { - "name": "Jobspresso", - "url": "https://jobspresso.co" - }, - { - "name": "JPEG-Optimizer", - "url": "https://jpeg-optimizer.com" - }, - { - "name": "JS Remotely", - "url": "https://jsremotely.com" - }, - { - "name": "JScreenFix", - "url": "https://jscreenfix.com" - }, - { - "name": "JSON Resume", - "url": "https://jsonresume.io" - }, - { - "name": "Just Join", - "url": "https://justjoin.it" - }, - { - "name": "Just the Recipe", - "url": "https://justtherecipe.com" - }, - { - "name": "JustRemote", - "url": "https://justremote.co" - }, - { - "name": "JustWatch", - "url": "https://justwatch.com" - }, - { - "name": "Kanopy", - "url": "https://kanopy.com" - }, - { - "name": "Kassellabs", - "url": "https://kassellabs.io" - }, - { - "name": "Key Differences", - "url": "https://keydifferences.com" - }, - { - "name": "Keybase", - "url": "https://keybase.io" - }, - { - "name": "KeyValues", - "url": "https://keyvalues.com" - }, - { - "name": "KHInsider", - "url": "https://khinsider.com" - }, - { - "name": "Killed by Google", - "url": "https://killedbygoogle.com" - }, - { - "name": "Kimovil", - "url": "https://kimovil.com" - }, - { - "name": "Lalal.ai", - "url": "https://www.lalal.ai" - }, - { - "name": "Learn Anything", - "url": "https://learn-anything.xyz" - }, - { - "name": "LendingTree", - "url": "https://lendingtree.com" - }, - { - "name": "Lightyear.fm", - "url": "https://lightyear.fm" - }, - { - "name": "LittleSis", - "url": "https://littlesis.org" - }, - { - "name": "Looria", - "url": "https://looria.com" - }, - { - "name": "Lucidchart", - "url": "https://lucidchart.com" - }, - { - "name": "Lunar", - "url": "https://lunar.fyi" - }, - { - "name": "Manuals Lib", - "url": "https://manualslib.com" - }, - { - "name": "Map Crunch", - "url": "https://mapcrunch.com" - }, - { - "name": "Masterworks", - "url": "https://masterworks.com" - }, - { - "name": "MediaFire", - "url": "https://mediafire.com" - }, - { - "name": "Mixlr", - "url": "https://mixlr.com" - }, - { - "name": "Moises AI", - "url": "https://moises.ai" - }, - { - "name": "Money", - "url": "https://money.com" - }, - { - "name": "Mountain Project", - "url": "https://mountainproject.com" - }, - { - "name": "Movie Map", - "url": "https://movie-map.com" - }, - { - "name": "Movie Sounds", - "url": "https://movie-sounds.org" - }, - { - "name": "MP3Cut", - "url": "https://mp3cut.net" - }, - { - "name": "Murmel", - "url": "https://murmel.social" - }, - { - "name": "Muscle Wiki", - "url": "https://musclewiki.com" - }, - { - "name": "Music-Map", - "url": "https://music-map.com" - }, - { - "name": "MusicTheory.net", - "url": "https://musictheory.net" - }, - { - "name": "MyFonts", - "url": "https://myfonts.com" - }, - { - "name": "MyFridgeFood", - "url": "https://myfridgefood.com" - }, - { - "name": "Nameberry", - "url": "https://nameberry.com" - }, - { - "name": "Namechk", - "url": "https://namechk.com" - }, - { - "name": "Ncase", - "url": "https://ncase.me" - }, - { - "name": "News in Levels", - "url": "https://newsinlevels.com" - }, - { - "name": "Noisli", - "url": "https://noisli.com" - }, - { - "name": "Notes.io", - "url": "https://notes.io" - }, - { - "name": "Novoresume", - "url": "https://novoresume.com" - }, - { - "name": "Ocoya", - "url": "https://ocoya.com" - }, - { - "name": "Old Computers Museum", - "url": "https://oldcomputers.net" - }, - { - "name": "Online Tone Generator", - "url": "https://onlinetonegenerator.com" - }, - { - "name": "Online-Convert", - "url": "https://online-convert.com" - }, - { - "name": "OnlineConversion", - "url": "https://onlineconversion.com" - }, - { - "name": "Online OCR", - "url": "https://onlineocr.net" - }, - { - "name": "OpenWeatherMap", - "url": "https://openweathermap.org" - }, - { - "name": "OrgPad", - "url": "https://orgpad.com" - }, - { - "name": "Passport Index", - "url": "https://passportindex.org" - }, - { - "name": "PDF Candy", - "url": "https://pdfcandy.com" - }, - { - "name": "PDF2DOC", - "url": "https://pdf2doc.com" - }, - { - "name": "PDFescape", - "url": "https://pdfescape.com" - }, - { - "name": "PfpMaker", - "url": "https://pfpmaker.com" - }, - { - "name": "PIDGI Wiki ", - "url": "https://pidgi.net" - }, - { - "name": "PimEyes", - "url": "https://pimeyes.com" - }, - { - "name": "Pipl ", - "url": "https://pipl.com" - }, - { - "name": "PixelBazaar", - "url": "https://pixelbazaar.com" - }, - { - "name": "PixelPaper", - "url": "https://pixelpaper.io" - }, - { - "name": "Ponly", - "url": "https://ponly.com" - }, - { - "name": "PowerToFly", - "url": "https://powertofly.com" - }, - { - "name": "Pretzel Rocks", - "url": "https://pretzel.rocks" - }, - { - "name": "PrintIt", - "url": "https://printit.work" - }, - { - "name": "Prismatext", - "url": "https://prismatext.com" - }, - { - "name": "Puffin Maps", - "url": "https://puffinmaps.com" - }, - { - "name": "Puzzle Loop ", - "url": "https://puzzle-loop.com" - }, - { - "name": "QuoteMaster", - "url": "https://quotemaster.org" - }, - { - "name": "Radio Garden", - "url": "https://radio.garden" - }, - { - "name": "Radiooooo", - "url": "https://radiooooo.com" - }, - { - "name": "Radiosondy", - "url": "https://radiosondy.info" - }, - { - "name": "Rainy Mood", - "url": "https://rainymood.com" - }, - { - "name": "Random Street View", - "url": "https://randomstreetview.com" - }, - { - "name": "Rap4Ever", - "url": "https://rap4all.com" - }, - { - "name": "RareFilm", - "url": "https://rarefilm.net" - }, - { - "name": "Rattibha", - "url": "https://rattibha.com" - }, - { - "name": "Reddit List ", - "url": "https://redditlist.com" - }, - { - "name": "RedditSearch.io", - "url": "https://redditsearch.io" - }, - { - "name": "Reelgood", - "url": "https://reelgood.com" - }, - { - "name": "Reface", - "url": "https://reface.ai" - }, - { - "name": "Rejected.us", - "url": "https://rejected.us" - }, - { - "name": "Relanote", - "url": "https://relanote.com" - }, - { - "name": "Remote Leaf", - "url": "https://remoteleaf.com" - }, - { - "name": "Remote OK", - "url": "https://remoteok.com" - }, - { - "name": "Remote Starter Kit ", - "url": "https://remotestarterkit.com" - }, - { - "name": "Remote.co", - "url": "https://remote.co" - }, - { - "name": "Remote Base ", - "url": "https://remotebase.com" - }, - { - "name": "Remote Bear", - "url": "https://remotebear.io" - }, - { - "name": "Remove.bg", - "url": "https://remove.bg" - }, - { - "name": "Respresso", - "url": "https://respresso.io" - }, - { - "name": "Reveddit", - "url": "https://reveddit.com" - }, - { - "name": "Rhymer", - "url": "https://rhymer.com" - }, - { - "name": "RhymeZone", - "url": "https://rhymezone.com" - }, - { - "name": "Ribbet", - "url": "https://ribbet.com" - }, - { - "name": "Roadmap.sh", - "url": "https://roadmap.sh" - }, - { - "name": "Roadtrippers", - "url": "https://roadtrippers.com" - }, - { - "name": "RxResu.me", - "url": "https://rxresu.me" - }, - { - "name": "SchemeColor", - "url": "https://schemecolor.com" - }, - { - "name": "Screenshot.Guru", - "url": "https://screenshot.guru" - }, - { - "name": "SeatGuru", - "url": "https://seatguru.com" - }, - { - "name": "Sessions", - "url": "https://sessions.us" - }, - { - "name": "Shottr", - "url": "https://shottr.cc" - }, - { - "name": "Signature Maker", - "url": "https://signature-maker.net" - }, - { - "name": "Skip The Drive", - "url": "https://skipthedrive.com" - }, - { - "name": "Slowby", - "url": "https://slowby.travel" - }, - { - "name": "Small World", - "url": "https://smallworld.kiwi" - }, - { - "name": "SmallPDF", - "url": "https://smallpdf.com" - }, - { - "name": "Social Image Maker", - "url": "https://socialimagemaker.io" - }, - { - "name": "Social Sizes", - "url": "https://socialsizes.io" - }, - { - "name": "SoundLove", - "url": "https://soundlove.se" - }, - { - "name": "Spline", - "url": "https://spline.design" - }, - { - "name": "Starkey Comics", - "url": "https://starkeycomics.com" - }, - - { - "name": "Statista", - "url": "https://statista.com" - }, - { - "name": "Stolen Camera Finder", - "url": "https://stolencamerafinder.com" - }, - { - "name": "Strobe.Cool", - "url": "https://strobe.cool" - }, - { - "name": "Sumo", - "url": "https://sumo.app" - }, - { - "name": "SuperMeme AI", - "url": "https://supermeme.ai" - }, - { - "name": "Synthesia", - "url": "https://synthesia.io" - }, - { - "name": "TablerIcons", - "url": "https://tablericons.com" - }, - { - "name": "Tango", - "url": "https://tango.us" - }, - { - "name": "TasteDive", - "url": "https://tastedive.com" - }, - { - "name": "TechSpecs", - "url": "https://techspecs.io" - }, - { - "name": "Teoria", - "url": "https://teoria.com" - }, - { - "name": "Text Faces", - "url": "https://textfac.es" - }, - { - "name": "The Balance Money", - "url": "https://thebalancemoney.com" - }, - { - "name": "The Punctuation Guide", - "url": "https://thepunctuationguide.com" - }, - { - "name": "This to That", - "url": "https://thistothat.com" - }, - { - "name": "This vs That", - "url": "https://thisvsthat.io" - }, - { - "name": "ThreadReaderApp ", - "url": "https://threadreaderapp.com" - }, - { - "name": "Thumbly", - "url": "https://tokee.ai" - }, - { - "name": "Tiii.me", - "url": "https://tiii.me" - }, - { - "name": "TikTok Video Downloader", - "url": "https://ttvdl.com" - }, - { - "name": "Time and Date", - "url": "https://timeanddate.com" - }, - { - "name": "Time.is", - "url": "https://time.is" - }, - { - "name": "Title Case", - "url": "https://titlecase.com" - }, - { - "name": "Toaster Central", - "url": "https://toastercentral.com" - }, - { - "name": "Tongue-Twister ", - "url": "https://tongue-twister.net" - }, - { - "name": "TradingView", - "url": "https://tradingview.com" - }, - { - "name": "Transparent Textures", - "url": "https://transparenttextures.com" - }, - { - "name": "Tubi TV", - "url": "https://tubitv.com" - }, - { - "name": "Tunefind", - "url": "https://tunefind.com" - }, - { - "name": "TuneMyMusic", - "url": "https://tunemymusic.com" - }, - { - "name": "Tweepsmap", - "url": "https://fedica.com" - }, - { - "name": "Two Peas and Their Pod", - "url": "https://twopeasandtheirpod.com" - }, - { - "name": "Typatone", - "url": "https://typatone.com" - }, - { - "name": "Under Glass", - "url": "https://underglass.io" - }, - { - "name": "UniCorner", - "url": "https://unicorner.news" - }, - { - "name": "Unita", - "url": "https://unita.co" - }, - { - "name": "UnitConverters", - "url": "https://unitconverters.net" - }, - { - "name": "Unreadit", - "url": "https://unreadit.com" - }, - { - "name": "Unscreen", - "url": "https://unscreen.com" - }, - { - "name": "UnTools ", - "url": "https://untools.co" - }, - { - "name": "Upwork", - "url": "https://upwork.com" - }, - { - "name": "UTF8 Icons", - "url": "https://utf8icons.com" - }, - { - "name": "Vector Magic", - "url": "https://vectormagic.com" - }, - { - "name": "Virtual Vacation", - "url": "https://virtualvacation.us" - }, - { - "name": "Virtual Vocations", - "url": "https://virtualvocations.com" - }, - { - "name": "Visiwig", - "url": "https://visiwig.com" - }, - { - "name": "Visual CV", - "url": "https://visualcv.com" - }, - { - "name": "Vocus.io", - "url": "https://vocus.io" - }, - { - "name": "Voscreen", - "url": "https://voscreen.com" - }, - { - "name": "Wanderprep", - "url": "https://wanderprep.com" - }, - { - "name": "Warmshowers", - "url": "https:/warmshowers.org" - }, - { - "name": "Watch Documentaries", - "url": "https://watchdocumentaries.com" - }, - { - "name": "We Work Remotely", - "url": "https://weworkremotely.com" - }, - { - "name": "Web2PDFConvert", - "url": "https://web2pdfconvert.com" - }, - { - "name": "Welcome to My Garden", - "url": "https://welcometomygarden.org" - }, - { - "name": "When2meet ", - "url": "https://when2meet.com" - }, - { - "name": "Where's George", - "url": "https://wheresgeorge.com" - }, - { - "name": "Where's Willy", - "url": "https://whereswilly.com" - }, - { - "name": "WikiHow", - "url": "https://wikihow.com" - }, - { - "name": "Windy", - "url": "https://www.windy.com" - }, - { - "name": "WonderHowTo", - "url": "https://wonderhowto.com" - }, - { - "name": "Working Nomads", - "url": "https://workingnomads.com" - }, - { - "name": "Wormhole", - "url": "https://wormhole.app" - }, - { - "name": "Y Combinator Jobs", - "url": "https://ycombinator.com" - }, - { - "name": "Yes Promo", - "url": "https://yespromo.me" - }, - { - "name": "YouGlish", - "url": "https://youglish.com" - }, - { - "name": "Zamzar", - "url": "https://zamzar.com" - }, - { - "name": "Zippyshare", - "url": "https://zippyshare.com" - }, - { - "name": "Zoom Earth", - "url": "https://zoom.earth" - }, - { - "name": "Zoom.it", - "url": "https://zoom.it" - } -] diff --git a/server/src/utils/logger.ts b/server/src/utils/logger.ts index ac1cbbf7c..4053f4a82 100755 --- a/server/src/utils/logger.ts +++ b/server/src/utils/logger.ts @@ -1,6 +1,7 @@ import { createLogger, format, transports, Logger as WinstonLogger } from "winston"; import type { Logform } from "winston"; import dotenv from "dotenv"; +import { EnvConfig } from "@/service/system/settingsService.js"; dotenv.config(); const SERVICE_NAME = "Logger"; @@ -18,19 +19,26 @@ interface LogEntry extends LogConfig { timestamp: string; } -interface EnvSettings { - logLevel?: string; +export interface ILogger { + readonly serviceName: string; + info(config: LogConfig): void; + warn(config: LogConfig): void; + error(config: LogConfig): void; + debug(config: LogConfig): void; + cacheLog(entry: LogEntry): void; + getLogs(): LogEntry[]; + buildLogEntry(level: string, config: LogConfig): LogEntry; } -class Logger { +class Logger implements ILogger { static SERVICE_NAME = SERVICE_NAME; private logger: WinstonLogger; - private envSettings: EnvSettings; + private envSettings: Partial; private logCache: LogEntry[]; private maxCacheSize: number; - constructor({ envSettings }: { envSettings: EnvSettings }) { + constructor({ envSettings }: { envSettings: Partial }) { this.envSettings = envSettings; this.logCache = []; this.maxCacheSize = 1000; diff --git a/server/src/utils/roleUtils.js b/server/src/utils/roleUtils.js deleted file mode 100644 index f359246b2..000000000 --- a/server/src/utils/roleUtils.js +++ /dev/null @@ -1,13 +0,0 @@ -export const ROLES = { - SUPERADMIN: "superadmin", - ADMIN: "admin", - USER: "user", - DEMO: "demo", -}; - -export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO]; - -export const EDITABLE_ROLES = [ - { role: ROLES.ADMIN, _id: ROLES.ADMIN }, - { role: ROLES.USER, _id: ROLES.USER }, -]; diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index 8533b4e44..474feebe5 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -1,5 +1,5 @@ import joi from "joi"; -import { ROLES, VALID_ROLES } from "../utils/roleUtils.js"; +import { UserRoles } from "@/types/user.js"; //**************************************** // Custom Validators @@ -689,7 +689,7 @@ const editUserByIdBodyValidation = joi.object({ email: joi.string().email().required(), role: joi .array() - .items(joi.string().valid(...VALID_ROLES)) + .items(joi.string().valid(...UserRoles)) .min(1) .required(), }); @@ -700,7 +700,7 @@ const editSuperadminUserByIdBodyValidation = joi.object({ email: joi.string().email().required(), role: joi .array() - .items(joi.string().valid(...VALID_ROLES, ROLES.SUPERADMIN)) + .items(joi.string().valid(...UserRoles)) .min(1) .required(), });