From f9d5c9933f122de69ed2440d6589f7262aa86bf4 Mon Sep 17 00:00:00 2001 From: heyAbdullahBruh Date: Wed, 9 Sep 2026 15:43:45 -0400 Subject: [PATCH] feat: setup deployment funtions Dockerfile, ci cd --- .dockerignore | 11 ++ .env.example | 50 ++++-- Dockerfile | 25 +++ Dockerfile.worker | 18 ++ render.yaml | 85 +++++++++ server.js | 376 ++++++++++++++++----------------------- src/app/app.js | 1 - src/config/production.js | 26 +++ 8 files changed, 351 insertions(+), 241 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Dockerfile.worker create mode 100644 render.yaml create mode 100644 src/config/production.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..97635a5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules/ +.env +.env.local +npm-debug.log* +*.log +.DS_Store +coverage/ +package-lock.json +.github/ +tests/ +src/emails/templates/ diff --git a/.env.example b/.env.example index 83525bf..81aa432 100644 --- a/.env.example +++ b/.env.example @@ -1,27 +1,47 @@ -# ALL CREDIT open by sayed046571@gmail.com except brevo. +NODE_ENV=production +PORT=10000 +MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/hoterstellar -NODE_ENV=development -PORT=5000 -MONGODB_URI=mongodb://localhost:27017/hoterstellar -JWT_SECRET=please-change-me-to-a-long-random-string-32+chars -JWT_ACCESS_EXPIRES_IN=15m +# Admin JWT secrets +ADMIN_JWT_SECRET=your-admin-secret-at-least-32-chars +ADMIN_ACCESS_TOKEN_EXPIRES_IN=15m + +# User JWT secrets +USER_JWT_SECRET=your-user-secret-at-least-32-chars +USER_ACCESS_TOKEN_EXPIRES_IN=15m + +# Refresh token cookie names ADMIN_REFRESH_COOKIE_NAME=admin_refresh_token USER_REFRESH_COOKIE_NAME=user_refresh_token -CORS_ORIGINS=http://localhost:3000,http://localhost:5173 -UPSTASH_REDIS_REST_URL=https://your-upstash-rest-url.upstash.io -UPSTASH_REDIS_REST_TOKEN=your-upstash-rest-token -UPSTASH_REDIS_NATIVE_URL=redis://default:your-password@your-upstash-redis-host:6379 +# CORS +CORS_ORIGINS=https://hoterstellar.com,https://admin.hoterstellar.com +CLIENT_PUBLIC_URL=https://hoterstellar.com +CLIENT_DASHBOARD_URL=https://admin.hoterstellar.com +# Upstash Redis +UPSTASH_REDIS_REST_URL=https://your-db.upstash.io +UPSTASH_REDIS_REST_TOKEN=your-rest-token +UPSTASH_REDIS_NATIVE_URL=rediss://default:pass@your-db.upstash.io:6379 + +# Brevo Email BREVO_API_KEY=your-brevo-api-key BREVO_SENDER_EMAIL=no-reply@hoterstellar.com BREVO_SENDER_NAME=Hoterstellar +ADMIN_NOTIFICATION_EMAILS=admin@hoterstellar.com -IMAGEKIT_PUBLIC_KEY=your-imagekit-public-key -IMAGEKIT_PRIVATE_KEY=your-imagekit-private-key +# ImageKit +IMAGEKIT_PUBLIC_KEY=your-public-key +IMAGEKIT_PRIVATE_KEY=your-private-key IMAGEKIT_URL_ENDPOINT=https://ik.imagekit.io/your-id -RECAPTCHA_SECRET_KEY=your-recaptcha-secret -RECAPTCHA_SITE_KEY=your-recaptcha-site-key +# Google reCAPTCHA +RECAPTCHA_SECRET_KEY=your-secret-key +RECAPTCHA_SITE_KEY=your-site-key + +# Business +BUSINESS_TIMEZONE=Asia/Dhaka -BUSINESS_TIMEZONE=Asia/Dhaka \ No newline at end of file +# Admin Seed +SUPER_ADMIN_EMAIL=superadmin@hoterstellar.com +SUPER_ADMIN_PASSWORD=your-strong-password \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1e32e74 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:22-alpine + +WORKDIR /app + +# Copy package files +COPY package.json ./ + +# Install dependencies +RUN npm install --omit=dev + +# Copy source code +COPY . . + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +# Change ownership +RUN chown -R nodejs:nodejs /app + +USER nodejs + +EXPOSE 10000 + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/Dockerfile.worker b/Dockerfile.worker new file mode 100644 index 0000000..ef17d3c --- /dev/null +++ b/Dockerfile.worker @@ -0,0 +1,18 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package.json ./ + +RUN npm install --omit=dev + +COPY . . + +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +RUN chown -R nodejs:nodejs /app + +USER nodejs + +CMD ["node", "worker.js"] \ No newline at end of file diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..5bcaf5a --- /dev/null +++ b/render.yaml @@ -0,0 +1,85 @@ +services: + - type: web + name: hoterstellar-api + env: node + plan: starter + buildCommand: npm install + startCommand: node server.js + healthCheckPath: /health + autoDeploy: true + envVars: + - key: NODE_ENV + value: production + - key: PORT + value: 10000 + - key: MONGODB_URI + sync: false + - key: ADMIN_JWT_SECRET + sync: false + - key: USER_JWT_SECRET + sync: false + - key: CORS_ORIGINS + sync: false + - key: UPSTASH_REDIS_REST_URL + sync: false + - key: UPSTASH_REDIS_REST_TOKEN + sync: false + - key: UPSTASH_REDIS_NATIVE_URL + sync: false + - key: BREVO_API_KEY + sync: false + - key: BREVO_SENDER_EMAIL + sync: false + - key: BREVO_SENDER_NAME + sync: false + - key: IMAGEKIT_PUBLIC_KEY + sync: false + - key: IMAGEKIT_PRIVATE_KEY + sync: false + - key: IMAGEKIT_URL_ENDPOINT + sync: false + - key: RECAPTCHA_SECRET_KEY + sync: false + - key: RECAPTCHA_SITE_KEY + sync: false + - key: BUSINESS_TIMEZONE + value: Asia/Dhaka + - key: CLIENT_PUBLIC_URL + sync: false + - key: CLIENT_DASHBOARD_URL + sync: false + - key: ADMIN_NOTIFICATION_EMAILS + sync: false + + - type: worker + name: hoterstellar-worker + env: node + plan: starter + buildCommand: npm install + startCommand: node worker.js + autoDeploy: true + envVars: + - key: NODE_ENV + value: production + - key: MONGODB_URI + sync: false + - key: UPSTASH_REDIS_REST_URL + sync: false + - key: UPSTASH_REDIS_REST_TOKEN + sync: false + - key: UPSTASH_REDIS_NATIVE_URL + sync: false + - key: BREVO_API_KEY + sync: false + - key: BREVO_SENDER_EMAIL + sync: false + - key: BREVO_SENDER_NAME + sync: false + - key: IMAGEKIT_PUBLIC_KEY + sync: false + - key: IMAGEKIT_PRIVATE_KEY + sync: false + - key: IMAGEKIT_URL_ENDPOINT + sync: false + - key: ADMIN_NOTIFICATION_EMAILS + sync: false diff --git a/server.js b/server.js index 5724850..2caff8e 100644 --- a/server.js +++ b/server.js @@ -1,74 +1,63 @@ -import http from "http"; -import mongoose from "mongoose"; -import dayjs from "dayjs"; -import { connectDB } from "./src/config/database.js"; -import { env } from "./src/config/env.js"; -import { connectRedis, isRedisReady } from "./src/config/redis.js"; -import { initializeSocket } from "./src/config/socket.js"; -import { verifyBrevoOnStartup, isBrevoConfigured } from "./src/config/brevo.js"; -import { getQueue, QUEUE_NAMES } from "./src/config/queue.js"; -import { logger, chalk } from "./src/utils/logger.js"; -import app from "./src/app/app.js"; - +import http from 'http'; +import mongoose from 'mongoose'; +import dayjs from 'dayjs'; +import { connectDB } from './src/config/database.js'; +import { env } from './src/config/env.js'; +import { connectRedis, isRedisReady } from './src/config/redis.js'; +import { initializeSocket } from './src/config/socket.js'; +import { verifyBrevoOnStartup, isBrevoConfigured } from './src/config/brevo.js'; +import { getQueue, QUEUE_NAMES } from './src/config/queue.js'; +import { logger, chalk } from './src/utils/logger.js'; +import app from './src/app/app.js'; +import { validateProductionConfig } from './src/config/production.js'; + +validateProductionConfig(); const SHUTDOWN_TIMEOUT_MS = 10_000; const printDivider = () => { - console.log(chalk.hex("#334155")("─".repeat(65))); + console.log(chalk.hex('#334155')('─'.repeat(65))); }; const printBanner = () => { - console.log(""); - console.log(chalk.hex("#6366f1")("═".repeat(65))); - console.log(""); - console.log( - chalk.hex("#8b5cf6").bold(" Hoterstellar — Backend API Server"), - ); - console.log(""); - console.log( - chalk.hex("#a78bfa")(" Hotel & Restaurant Management Platform"), - ); - console.log(""); - console.log(chalk.hex("#6366f1")("═".repeat(65))); - console.log(""); + console.log(''); + console.log(chalk.hex('#6366f1')('═'.repeat(65))); + console.log(''); + console.log(chalk.hex('#8b5cf6').bold(' Hoterstellar — Backend API Server')); + console.log(''); + console.log(chalk.hex('#a78bfa')(' Hotel & Restaurant Management Platform')); + console.log(''); + console.log(chalk.hex('#6366f1')('═'.repeat(65))); + console.log(''); }; const printStartupHeader = () => { - console.log(""); - console.log(chalk.hex("#06b6d4").bold("▶ Starting Server...")); - console.log(""); - printKeyValue("Environment", env.NODE_ENV); - printKeyValue("Port", env.PORT.toString()); - printKeyValue("Time", dayjs().format("YYYY-MM-DD HH:mm:ss Z")); + console.log(''); + console.log(chalk.hex('#06b6d4').bold('▶ Starting Server...')); + console.log(''); + printKeyValue('Environment', env.NODE_ENV); + printKeyValue('Port', env.PORT.toString()); + printKeyValue('Time', dayjs().format('YYYY-MM-DD HH:mm:ss Z')); printDivider(); }; -const printKeyValue = ( - key, - value, - keyColor = "#94a3b8", - valueColor = "#e2e8f0", -) => { - console.log( - ` ${chalk.hex(keyColor)(key.padEnd(20))} ${chalk.hex(valueColor)(value)}`, - ); +const printKeyValue = (key, value, keyColor = '#94a3b8', valueColor = '#e2e8f0') => { + console.log(` ${chalk.hex(keyColor)(key.padEnd(20))} ${chalk.hex(valueColor)(value)}`); }; const printStep = (step, total, label, status, details) => { const icon = - status === "pass" - ? chalk.green(" ✓") - : status === "fail" - ? chalk.red(" ✗") - : status === "warn" - ? chalk.yellow(" ⚠") - : chalk.cyan(" ⓘ"); - - const stepLabel = `[${String(step).padStart(2, "0")}/${total}]`; - console.log( - ` ${chalk.hex("#6366f1")(stepLabel)}${icon} ${chalk.hex("#e2e8f0")(label)}`, - ); + status === 'pass' + ? chalk.green(' ✓') + : status === 'fail' + ? chalk.red(' ✗') + : status === 'warn' + ? chalk.yellow(' ⚠') + : chalk.cyan(' ⓘ'); + + const stepLabel = `[${String(step).padStart(2, '0')}/${total}]`; + console.log(` ${chalk.hex('#6366f1')(stepLabel)}${icon} ${chalk.hex('#e2e8f0')(label)}`); if (details) { - console.log(` ${chalk.hex("#64748b")(details)}`); + console.log(` ${chalk.hex('#64748b')(details)}`); } }; @@ -77,45 +66,36 @@ const getServicesHealth = () => { const mongoState = mongoose.connection.readyState; services.push({ - name: "MongoDB", - status: - mongoState === 1 - ? "healthy" - : mongoState === 2 - ? "degraded" - : "unhealthy", + name: 'MongoDB', + status: mongoState === 1 ? 'healthy' : mongoState === 2 ? 'degraded' : 'unhealthy', details: mongoState === 0 - ? "Disconnected" + ? 'Disconnected' : mongoState === 1 - ? "Connected" + ? 'Connected' : mongoState === 2 - ? "Connecting..." - : "Disconnecting...", + ? 'Connecting...' + : 'Disconnecting...', }); services.push({ - name: "Redis", - status: isRedisReady() ? "healthy" : "degraded", - details: isRedisReady() - ? "Connected & ready" - : "Not available — cache disabled", + name: 'Redis', + status: isRedisReady() ? 'healthy' : 'degraded', + details: isRedisReady() ? 'Connected & ready' : 'Not available — cache disabled', }); services.push({ - name: "Brevo Email", - status: isBrevoConfigured() ? "healthy" : "degraded", - details: isBrevoConfigured() - ? "Configured & verified" - : "Not configured — email disabled", + name: 'Brevo Email', + status: isBrevoConfigured() ? 'healthy' : 'degraded', + details: isBrevoConfigured() ? 'Configured & verified' : 'Not configured — email disabled', }); services.push({ - name: "BullMQ Queues", - status: isRedisReady() ? "healthy" : "degraded", + name: 'BullMQ Queues', + status: isRedisReady() ? 'healthy' : 'degraded', details: isRedisReady() - ? "Email, Analytics, Media queues ready" - : "Queues disabled — Redis unavailable", + ? 'Email, Analytics, Media queues ready' + : 'Queues disabled — Redis unavailable', }); return services; @@ -126,22 +106,20 @@ const setupGracefulShutdown = (server) => { const gracefulShutdown = (signal) => { if (isShuttingDown) { - logger.warn("Shutdown already in progress — forcing exit..."); + logger.warn('Shutdown already in progress — forcing exit...'); process.exit(1); } isShuttingDown = true; - console.log(""); + console.log(''); printDivider(); - console.log(""); - logger.info( - chalk.yellow.bold(` ${signal} received — Starting graceful shutdown...`), - ); - console.log(""); + console.log(''); + logger.info(chalk.yellow.bold(` ${signal} received — Starting graceful shutdown...`)); + console.log(''); server.close(() => { - logger.info(chalk.blue(" ✓ HTTP server closed")); + logger.info(chalk.blue(' ✓ HTTP server closed')); }); const forceExit = setTimeout(() => { @@ -156,61 +134,56 @@ const setupGracefulShutdown = (server) => { void (async () => { try { // Close Socket.IO - const { getIO } = await import("./src/config/socket.js"); + const { getIO } = await import('./src/config/socket.js'); const io = getIO(); if (io) { await io.close(); - logger.info(chalk.blue(" ✓ Socket.IO closed")); + logger.info(chalk.blue(' ✓ Socket.IO closed')); } // Close MongoDB if (mongoose.connection.readyState !== 0) { await mongoose.connection.close(); - logger.info(chalk.blue(" ✓ MongoDB connection closed")); + logger.info(chalk.blue(' ✓ MongoDB connection closed')); } else { - logger.info(chalk.gray(" - MongoDB already disconnected")); + logger.info(chalk.gray(' - MongoDB already disconnected')); } // Redis (Upstash REST) — no cleanup needed - logger.info(chalk.gray(" - Redis (Upstash REST) — no cleanup needed")); + logger.info(chalk.gray(' - Redis (Upstash REST) — no cleanup needed')); clearTimeout(forceExit); - console.log(""); - logger.info( - chalk.green.bold(" ✓ Graceful shutdown complete. Goodbye! 👋"), - ); - console.log(""); + console.log(''); + logger.info(chalk.green.bold(' ✓ Graceful shutdown complete. Goodbye! 👋')); + console.log(''); printDivider(); - console.log(""); + console.log(''); process.exit(0); } catch (err) { clearTimeout(forceExit); logger.error( - chalk.red(" ✗ Error during shutdown:"), - err instanceof Error ? err.message : "Unknown error", + chalk.red(' ✗ Error during shutdown:'), + err instanceof Error ? err.message : 'Unknown error', ); process.exit(1); } })(); }; - process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); - process.on("SIGINT", () => gracefulShutdown("SIGINT")); + process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); + process.on('SIGINT', () => gracefulShutdown('SIGINT')); - process.on("unhandledRejection", (reason, promise) => { - logger.error("Unhandled Rejection at:", promise); - logger.error( - "Reason:", - reason instanceof Error ? reason.message : String(reason), - ); + process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Rejection at:', promise); + logger.error('Reason:', reason instanceof Error ? reason.message : String(reason)); }); - process.on("uncaughtException", (error) => { - logger.error("Uncaught Exception:", error.message); - logger.error(error.stack || "No stack trace"); - gracefulShutdown("uncaughtException"); + process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception:', error.message); + logger.error(error.stack || 'No stack trace'); + gracefulShutdown('uncaughtException'); }); }; @@ -223,55 +196,51 @@ const startServer = async () => { // Step 1: Validate Environment currentStep++; - printStep(currentStep, totalSteps, "Validating environment", "pass"); - logger.info( - ` Node ${process.version} | ${process.platform} ${process.arch}`, - ); + printStep(currentStep, totalSteps, 'Validating environment', 'pass'); + logger.info(` Node ${process.version} | ${process.platform} ${process.arch}`); logger.info(` Public URL: ${env.CLIENT_PUBLIC_URL}`); logger.info(` Dashboard URL: ${env.CLIENT_DASHBOARD_URL}`); printDivider(); // Step 2: Connect to MongoDB currentStep++; - printStep(currentStep, totalSteps, "Connecting to MongoDB...", "info"); + printStep(currentStep, totalSteps, 'Connecting to MongoDB...', 'info'); try { await connectDB(); - printStep(currentStep, totalSteps, "MongoDB connected", "pass"); - const dbName = mongoose.connection.db?.databaseName || "unknown"; - const host = mongoose.connection.host || "unknown"; + printStep(currentStep, totalSteps, 'MongoDB connected', 'pass'); + const dbName = mongoose.connection.db?.databaseName || 'unknown'; + const host = mongoose.connection.host || 'unknown'; logger.info(` Database: ${dbName} @ ${host}`); } catch (error) { - printStep(currentStep, totalSteps, "MongoDB connection failed", "fail"); - logger.error( - ` ${error instanceof Error ? error.message : "Unknown error"}`, - ); - console.log(""); - logger.error(" ✗ Cannot start without MongoDB. Exiting."); + printStep(currentStep, totalSteps, 'MongoDB connection failed', 'fail'); + logger.error(` ${error instanceof Error ? error.message : 'Unknown error'}`); + console.log(''); + logger.error(' ✗ Cannot start without MongoDB. Exiting.'); process.exit(1); } printDivider(); // Step 3: Check Email Configuration currentStep++; - printStep(currentStep, totalSteps, "Checking email configuration...", "info"); + printStep(currentStep, totalSteps, 'Checking email configuration...', 'info'); let emailReady = false; try { emailReady = await verifyBrevoOnStartup(); } catch (error) { logger.warn( - ` Email verification threw an error: ${error instanceof Error ? error.message : "Unknown"}`, + ` Email verification threw an error: ${error instanceof Error ? error.message : 'Unknown'}`, ); } if (emailReady) { - printStep(currentStep, totalSteps, "Email service ready", "pass"); + printStep(currentStep, totalSteps, 'Email service ready', 'pass'); } else { printStep( currentStep, totalSteps, - "Email service not configured", - "warn", - "Transactional emails will be disabled", + 'Email service not configured', + 'warn', + 'Transactional emails will be disabled', ); } printDivider(); @@ -279,151 +248,108 @@ const startServer = async () => { // Step 4: Connect to Redis + Initialize Queues // Step 4: Connect to Redis + Initialize Queues currentStep++; - printStep(currentStep, totalSteps, "Connecting to Redis...", "info"); + printStep(currentStep, totalSteps, 'Connecting to Redis...', 'info'); try { await connectRedis(); if (isRedisReady()) { - printStep(currentStep, totalSteps, "Redis connected", "pass"); - logger.info( - ` URL: ${env.UPSTASH_REDIS_REST_URL.replace(/\/\/.*@/, "//***@")}`, - ); + printStep(currentStep, totalSteps, 'Redis connected', 'pass'); + logger.info(` URL: ${env.UPSTASH_REDIS_REST_URL.replace(/\/\/.*@/, '//***@')}`); // Initialize queues (await each one) const emailQueue = await getQueue(QUEUE_NAMES.EMAIL); const analyticsQueue = await getQueue(QUEUE_NAMES.ANALYTICS_ROLLUP); const mediaQueue = await getQueue(QUEUE_NAMES.MEDIA_CLEANUP); - if (emailQueue) console.log(chalk.gray(" Email queue ready")); - if (analyticsQueue) console.log(chalk.gray(" Analytics queue ready")); - if (mediaQueue) console.log(chalk.gray(" Media cleanup queue ready")); + if (emailQueue) console.log(chalk.gray(' Email queue ready')); + if (analyticsQueue) console.log(chalk.gray(' Analytics queue ready')); + if (mediaQueue) console.log(chalk.gray(' Media cleanup queue ready')); } else { - printStep( - currentStep, - totalSteps, - "Redis unavailable", - "warn", - "Continuing without cache", - ); + printStep(currentStep, totalSteps, 'Redis unavailable', 'warn', 'Continuing without cache'); } } catch { - printStep( - currentStep, - totalSteps, - "Redis unavailable", - "warn", - "Continuing without cache", - ); + printStep(currentStep, totalSteps, 'Redis unavailable', 'warn', 'Continuing without cache'); } printDivider(); // Step 5: Initialize WebSocket currentStep++; - printStep(currentStep, totalSteps, "Initializing WebSocket...", "info"); + printStep(currentStep, totalSteps, 'Initializing WebSocket...', 'info'); const server = http.createServer(app); initializeSocket(server); - printStep(currentStep, totalSteps, "WebSocket ready", "pass"); - logger.info(" Socket.IO attached to HTTP server"); + printStep(currentStep, totalSteps, 'WebSocket ready', 'pass'); + logger.info(' Socket.IO attached to HTTP server'); printDivider(); // Step 6: Start HTTP Server currentStep++; - printStep(currentStep, totalSteps, "Starting HTTP server...", "info"); + printStep(currentStep, totalSteps, 'Starting HTTP server...', 'info'); server.listen(env.PORT, () => { - printStep( - currentStep, - totalSteps, - `Server listening on port ${env.PORT}`, - "pass", - ); + printStep(currentStep, totalSteps, `Server listening on port ${env.PORT}`, 'pass'); - console.log(""); + console.log(''); console.log( - chalk - .hex("#10b981") - .bold(" ╭────────────────────────────────────────────────────╮"), + chalk.hex('#10b981').bold(' ╭────────────────────────────────────────────────────╮'), ); console.log( - chalk.hex("#10b981").bold(" │") + - chalk.hex("#e2e8f0").bold(" 🚀 Server Started Successfully") + - " " + - chalk.hex("#10b981").bold("│"), + chalk.hex('#10b981').bold(' │') + + chalk.hex('#e2e8f0').bold(' 🚀 Server Started Successfully') + + ' ' + + chalk.hex('#10b981').bold('│'), ); console.log( - chalk - .hex("#10b981") - .bold(" ╰────────────────────────────────────────────────────╯"), - ); - console.log(""); - printKeyValue( - "Environment", - env.NODE_ENV.toUpperCase(), - "#94a3b8", - "#10b981", + chalk.hex('#10b981').bold(' ╰────────────────────────────────────────────────────╯'), ); - printKeyValue("Port", env.PORT.toString(), "#94a3b8", "#e2e8f0"); - printKeyValue("Public URL", env.CLIENT_PUBLIC_URL, "#94a3b8", "#6366f1"); - printKeyValue( - "Dashboard URL", - env.CLIENT_DASHBOARD_URL, - "#94a3b8", - "#8b5cf6", - ); - printKeyValue("WebSocket", "Enabled (same port)", "#94a3b8", "#e2e8f0"); - printKeyValue("API Version", "/api/v1", "#94a3b8", "#e2e8f0"); - console.log(""); + console.log(''); + printKeyValue('Environment', env.NODE_ENV.toUpperCase(), '#94a3b8', '#10b981'); + printKeyValue('Port', env.PORT.toString(), '#94a3b8', '#e2e8f0'); + printKeyValue('Public URL', env.CLIENT_PUBLIC_URL, '#94a3b8', '#6366f1'); + printKeyValue('Dashboard URL', env.CLIENT_DASHBOARD_URL, '#94a3b8', '#8b5cf6'); + printKeyValue('WebSocket', 'Enabled (same port)', '#94a3b8', '#e2e8f0'); + printKeyValue('API Version', '/api/v1', '#94a3b8', '#e2e8f0'); + console.log(''); const services = getServicesHealth(); - console.log(chalk.hex("#94a3b8")(" Services:")); + console.log(chalk.hex('#94a3b8')(' Services:')); services.forEach((svc) => { const icon = - svc.status === "healthy" - ? chalk.green(" ●") - : svc.status === "degraded" - ? chalk.yellow(" ◐") - : chalk.red(" ○"); + svc.status === 'healthy' + ? chalk.green(' ●') + : svc.status === 'degraded' + ? chalk.yellow(' ◐') + : chalk.red(' ○'); console.log( - ` ${icon} ${chalk.hex("#e2e8f0")(svc.name.padEnd(15))} ${chalk.hex("#64748b")(svc.details)}`, + ` ${icon} ${chalk.hex('#e2e8f0')(svc.name.padEnd(15))} ${chalk.hex('#64748b')(svc.details)}`, ); }); - console.log(""); - console.log(chalk.hex("#6366f1")("═".repeat(65))); - console.log(""); + console.log(''); + console.log(chalk.hex('#6366f1')('═'.repeat(65))); + console.log(''); console.log( - chalk.hex("#64748b")( - ` Press ${chalk.hex("#e2e8f0")("CTRL+C")} to stop the server`, - ), + chalk.hex('#64748b')(` Press ${chalk.hex('#e2e8f0')('CTRL+C')} to stop the server`), ); - console.log(""); + console.log(''); }); setupGracefulShutdown(server); }; startServer().catch((error) => { - console.log(""); - console.log( - chalk.red.bold( - "╔══════════════════════════════════════════════════════════╗", - ), - ); - console.log( - chalk.red.bold("║") + - chalk.white.bold(" ✗ FATAL: Server failed to start") + - " " + - chalk.red.bold("║"), - ); + console.log(''); + console.log(chalk.red.bold('╔══════════════════════════════════════════════════════════╗')); console.log( - chalk.red.bold( - "╚══════════════════════════════════════════════════════════╝", - ), + chalk.red.bold('║') + + chalk.white.bold(' ✗ FATAL: Server failed to start') + + ' ' + + chalk.red.bold('║'), ); - console.log(""); - logger.error(error instanceof Error ? error.message : "Unknown error"); + console.log(chalk.red.bold('╚══════════════════════════════════════════════════════════╝')); + console.log(''); + logger.error(error instanceof Error ? error.message : 'Unknown error'); if (error instanceof Error && error.stack) { logger.error(error.stack); } - console.log(""); + console.log(''); process.exit(1); }); diff --git a/src/app/app.js b/src/app/app.js index 1384fbf..b1048c2 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -86,7 +86,6 @@ app.get('/ready', async (req, res) => { }); } } catch (err) { - console.log(err); return res.status(503).json({ success: false, statusCode: 503, diff --git a/src/config/production.js b/src/config/production.js new file mode 100644 index 0000000..69d099b --- /dev/null +++ b/src/config/production.js @@ -0,0 +1,26 @@ +import { env } from './env.js'; +import { logger } from '../utils/logger.js'; + +export const validateProductionConfig = () => { + if (env.NODE_ENV === 'production') { + const requiredVars = [ + 'MONGODB_URI', + 'ADMIN_JWT_SECRET', + 'USER_JWT_SECRET', + 'UPSTASH_REDIS_REST_URL', + 'UPSTASH_REDIS_REST_TOKEN', + 'UPSTASH_REDIS_NATIVE_URL', + ]; + + const missingVars = requiredVars.filter((varName) => !process.env[varName]); + + if (missingVars.length > 0) { + logger.error('Missing required production environment variables:', { + missingVars, + }); + process.exit(1); + } + + logger.info('Production configuration validated'); + } +};