From 09534757a273481d24bd2b00b3c9764b2f7c53e6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 2 Mar 2026 21:52:22 +0000 Subject: [PATCH 1/2] validation --- server/.env.example | 26 +++++++++++ server/src/index.ts | 8 +++- server/src/service/system/settingsService.ts | 37 +++++++-------- server/src/validation/envValidation.ts | 47 ++++++++++++++++++++ server/src/validation/index.ts | 3 ++ 5 files changed, 98 insertions(+), 23 deletions(-) create mode 100644 server/.env.example create mode 100644 server/src/validation/envValidation.ts diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 000000000..1a05a6ff7 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,26 @@ +# ============================================== +# Checkmate Server Configuration +# ============================================== +# Copy this file to .env and fill in your values + +# Server Configuration +# -------------------- +NODE_ENV=development +LOG_LEVEL=debug + +# Database +# -------- +DB_CONNECTION_STRING=mongodb://localhost:27017/uptime_db + +# JWT Authentication +# ------------------ +JWT_SECRET=my_secret +TOKEN_TTL=99d + +# Client Configuration +# -------------------- +CLIENT_HOST=http://localhost:5173 + +# Optional Configuration +# ---------------------- +ORIGIN=localhost diff --git a/server/src/index.ts b/server/src/index.ts index ca6cea2ba..e5b679beb 100755 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -2,6 +2,7 @@ import { initializeServices } from "./config/services.js"; import { initializeControllers } from "./config/controllers.js"; import { createApp } from "./app.js"; import { initShutdownListener } from "./shutdown.js"; +import { validateEnv } from "./validation/envValidation.js"; import { fileURLToPath } from "url"; import path from "path"; import fs from "fs"; @@ -15,6 +16,9 @@ const SERVICE_NAME = "Server"; let logger: ILogger; const startApp = async () => { + // Validate environment variables first + const env = validateEnv(); + // FE path const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -23,7 +27,7 @@ const startApp = async () => { // Create services const settingsRepository = new MongoSettingsRepository(); - const settingsService = new SettingsService(settingsRepository); + const settingsService = new SettingsService(settingsRepository, env); const envSettings = settingsService.loadSettings(); @@ -46,7 +50,7 @@ const startApp = async () => { openApiSpec, }); - const port = envSettings.port || 52345; + const port = 52345; const server = app.listen(port, () => { logger.info({ message: `Server started on port:${port}` }); }); diff --git a/server/src/service/system/settingsService.ts b/server/src/service/system/settingsService.ts index a0db6a140..3390ba770 100755 --- a/server/src/service/system/settingsService.ts +++ b/server/src/service/system/settingsService.ts @@ -1,29 +1,17 @@ import { ISettingsRepository } from "@/repositories/index.js"; import { Settings } from "@/types/index.js"; import { AppError } from "@/utils/AppError.js"; +import { ValidatedEnv } from "@/validation/envValidation.js"; const SERVICE_NAME = "SettingsService"; 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, - nodeEnv: process.env.NODE_ENV, - logLevel: process.env.LOG_LEVEL, - clientHost: process.env.CLIENT_HOST, - dbConnectionString: process.env.DB_CONNECTION_STRING, - port: process.env.PORT, + jwtSecret: string; + jwtTTL: string; + nodeEnv: string; + logLevel: string; + clientHost: string; + dbConnectionString: string; }; export interface ISettingsService { @@ -38,9 +26,16 @@ class SettingsService implements ISettingsService { private settings: EnvConfig; private settingsRepository: ISettingsRepository; - constructor(settingsRepository: ISettingsRepository) { + constructor(settingsRepository: ISettingsRepository, env: ValidatedEnv) { this.settingsRepository = settingsRepository; - this.settings = { ...envConfig }; + this.settings = { + jwtSecret: env.JWT_SECRET, + jwtTTL: env.TOKEN_TTL, + nodeEnv: env.NODE_ENV, + logLevel: env.LOG_LEVEL, + clientHost: env.CLIENT_HOST, + dbConnectionString: env.DB_CONNECTION_STRING, + }; } get serviceName() { diff --git a/server/src/validation/envValidation.ts b/server/src/validation/envValidation.ts new file mode 100644 index 000000000..20a601ae6 --- /dev/null +++ b/server/src/validation/envValidation.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +/** + * Environment variable schema with validation + */ +const envSchema = z.object({ + // Server Configuration + NODE_ENV: z.enum(["development", "production", "test"]).default("development"), + LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("debug"), + + // Database + DB_CONNECTION_STRING: z.string().min(1, "Database connection string is required"), + + // JWT Authentication + JWT_SECRET: z.string().min(1, "JWT_SECRET is required"), + TOKEN_TTL: z.string().default("99d"), + + // Client Configuration + CLIENT_HOST: z.string().url("CLIENT_HOST must be a valid URL"), + + // Optional + ORIGIN: z.string().optional(), +}); + +export type ValidatedEnv = z.infer; + +export const validateEnv = (): ValidatedEnv => { + const result = envSchema.safeParse(process.env); + + if (!result.success) { + console.error("Environment validation failed:"); + console.error(""); + + const errors = result.error.format(); + for (const [key, value] of Object.entries(errors)) { + if (key !== "_errors" && value && typeof value === "object" && "_errors" in value) { + console.error(` ${key}: ${value._errors.join(", ")}`); + } + } + + console.error("Please check your .env file and ensure all required variables are set."); + process.exit(1); + } + + console.log("Environment variables validated successfully"); + return result.data; +}; diff --git a/server/src/validation/index.ts b/server/src/validation/index.ts index 4d7140d01..7439f043d 100644 --- a/server/src/validation/index.ts +++ b/server/src/validation/index.ts @@ -8,6 +8,9 @@ // Shared utilities export * from "./shared.js"; +// Environment validation +export * from "./envValidation.js"; + // Domain-specific validations export * from "./authValidation.js"; export * from "./monitorValidation.js"; From 3854931682f59b975c19f511ef834cbbafb20dc5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 2 Mar 2026 22:00:03 +0000 Subject: [PATCH 2/2] remove comment --- server/src/validation/envValidation.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/src/validation/envValidation.ts b/server/src/validation/envValidation.ts index 20a601ae6..6dffdd0cd 100644 --- a/server/src/validation/envValidation.ts +++ b/server/src/validation/envValidation.ts @@ -1,8 +1,5 @@ import { z } from "zod"; -/** - * Environment variable schema with validation - */ const envSchema = z.object({ // Server Configuration NODE_ENV: z.enum(["development", "production", "test"]).default("development"),