From 088ac173a7588ba35387ca01b681b3257c5b85d5 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Fri, 23 Jan 2026 12:51:49 +0530 Subject: [PATCH] db-password from google secrets aligned to server start gogle secrets initialized before migration --- package.json | 6 +++--- src/app.ts | 28 +++++++++----------------- src/scripts/auto-setup.ts | 26 +++++++++++++++--------- src/scripts/migrate.ts | 42 ++++++++++++++++++++++----------------- src/server.ts | 37 ++++++++++++++++++++++------------ 5 files changed, 77 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index b345113..fc740a1 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "Royal Enfield Workflow Management System - Backend API (TypeScript)", "main": "dist/server.js", "scripts": { - "start": "npm run setup && npm run build && npm run start:prod", - "dev": "npm run setup && nodemon --exec ts-node -r tsconfig-paths/register src/server.ts", + "start": "npm run build && npm run start:prod && npm run setup", + "dev": "nodemon --exec ts-node -r tsconfig-paths/register src/server.ts && npm run setup", "dev:no-setup": "nodemon --exec ts-node -r tsconfig-paths/register src/server.ts", "build": "tsc && tsc-alias", "build:watch": "tsc --watch", @@ -89,4 +89,4 @@ "node": ">=22.0.0", "npm": ">=10.0.0" } -} +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 2f4ebe0..599a556 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,17 +16,7 @@ import path from 'path'; // Load environment variables from .env file first dotenv.config(); -// Initialize Google Secret Manager (async, but we'll wait for it in server.ts) -// This will merge secrets from GCS into process.env if USE_GOOGLE_SECRET_MANAGER=true -// Export initialization function so server.ts can await it before starting -export async function initializeSecrets(): Promise { - try { - await initializeGoogleSecretManager(); - } catch (error) { - // Log error but don't throw - allow fallback to .env - console.error('⚠️ Failed to initialize Google Secret Manager, using .env file:', error); - } -} +// Secrets are now initialized in server.ts before app is imported const app: express.Application = express(); const userService = new UserService(); @@ -123,8 +113,8 @@ app.use(createMetricsRouter()); // Health check endpoint (before API routes) app.get('/health', (_req: express.Request, res: express.Response) => { - res.status(200).json({ - status: 'OK', + res.status(200).json({ + status: 'OK', timestamp: new Date(), uptime: process.uptime(), environment: process.env.NODE_ENV || 'development' @@ -142,7 +132,7 @@ app.use('/uploads', express.static(UPLOAD_DIR)); app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.Response): Promise => { try { const ssoData: SSOUserData = req.body; - + // Validate required fields - email and oktaSub are required if (!ssoData.email || !ssoData.oktaSub) { res.status(400).json({ @@ -155,7 +145,7 @@ app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express. // Create or update user const user = await userService.createOrUpdateUser(ssoData); - + res.status(200).json({ success: true, message: 'User processed successfully', @@ -193,7 +183,7 @@ app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express. app.get('/api/v1/users', async (_req: express.Request, res: express.Response): Promise => { try { const users = await userService.getAllUsers(); - + res.status(200).json({ success: true, message: 'Users retrieved successfully', @@ -254,7 +244,7 @@ if (reactBuildPath && fs.existsSync(path.join(reactBuildPath, "index.html"))) { } } })); - + // Catch-all handler: serve React app for all non-API routes // This must be AFTER all API routes to avoid intercepting API requests app.get('*', (req: express.Request, res: express.Response): void => { @@ -267,7 +257,7 @@ if (reactBuildPath && fs.existsSync(path.join(reactBuildPath, "index.html"))) { }); return; } - + // Serve React app for all other routes (SPA routing) // This handles client-side routing in React Router // CSP headers from Helmet will be applied to this response @@ -284,7 +274,7 @@ if (reactBuildPath && fs.existsSync(path.join(reactBuildPath, "index.html"))) { note: 'React build not found. API is available at /api/v1' }); }); - + // Standard 404 handler for non-existent routes app.use((req: express.Request, res: express.Response): void => { res.status(404).json({ diff --git a/src/scripts/auto-setup.ts b/src/scripts/auto-setup.ts index 7a64778..7bbec19 100644 --- a/src/scripts/auto-setup.ts +++ b/src/scripts/auto-setup.ts @@ -11,8 +11,8 @@ */ import { Client } from 'pg'; -import { sequelize } from '../config/database'; import { QueryTypes } from 'sequelize'; +import { initializeGoogleSecretManager } from '../services/googleSecretManager.service'; import { exec } from 'child_process'; import { promisify } from 'util'; import dotenv from 'dotenv'; @@ -21,14 +21,15 @@ import path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); const execAsync = promisify(exec); - -const DB_HOST = process.env.DB_HOST || 'localhost'; -const DB_PORT = parseInt(process.env.DB_PORT || '5432'); -const DB_USER = process.env.DB_USER || 'postgres'; -const DB_PASSWORD = process.env.DB_PASSWORD || ''; -const DB_NAME = process.env.DB_NAME || 'royal_enfield_workflow'; +// DB constants moved inside functions to ensure secrets are loaded first async function checkAndCreateDatabase(): Promise { + const DB_HOST = process.env.DB_HOST || 'localhost'; + const DB_PORT = parseInt(process.env.DB_PORT || '5432'); + const DB_USER = process.env.DB_USER || 'postgres'; + const DB_PASSWORD = process.env.DB_PASSWORD || ''; + const DB_NAME = process.env.DB_NAME || 'royal_enfield_workflow'; + const client = new Client({ host: DB_HOST, port: DB_PORT, @@ -156,6 +157,8 @@ async function runMigrations(): Promise { { name: '20260122-create-workflow-templates', module: m30 }, ]; + // Dynamically import sequelize after secrets are loaded + const { sequelize } = require('../config/database'); const queryInterface = sequelize.getQueryInterface(); // Ensure migrations tracking table exists @@ -171,10 +174,10 @@ async function runMigrations(): Promise { } // Get already executed migrations - const executedResults = await sequelize.query<{ name: string }>( + const executedResults = await sequelize.query( 'SELECT name FROM migrations ORDER BY id', { type: QueryTypes.SELECT } - ); + ) as { name: string }[]; const executedMigrations = executedResults.map(r => r.name); // Find pending migrations @@ -222,6 +225,7 @@ async function runMigrations(): Promise { async function testConnection(): Promise { try { console.log('🔌 Testing database connection...'); + const { sequelize } = require('../config/database'); await sequelize.authenticate(); console.log('✅ Database connection established!'); } catch (error: any) { @@ -236,6 +240,10 @@ async function autoSetup(): Promise { console.log('========================================\n'); try { + // Step 0: Initialize secrets + console.log('🔐 Initializing secrets...'); + await initializeGoogleSecretManager(); + // Step 1: Check and create database if needed const wasCreated = await checkAndCreateDatabase(); diff --git a/src/scripts/migrate.ts b/src/scripts/migrate.ts index e7be3b5..52e7a1f 100644 --- a/src/scripts/migrate.ts +++ b/src/scripts/migrate.ts @@ -1,5 +1,5 @@ -import { sequelize } from '../config/database'; import { QueryInterface, QueryTypes } from 'sequelize'; +import { initializeGoogleSecretManager } from '../services/googleSecretManager.service'; import * as m0 from '../migrations/2025103000-create-users'; import * as m1 from '../migrations/2025103001-create-workflow-requests'; import * as m2 from '../migrations/2025103002-create-approval-levels'; @@ -41,7 +41,7 @@ interface Migration { const migrations: Migration[] = [ // 1. FIRST: Create base tables with no dependencies { name: '2025103000-create-users', module: m0 }, // ← MUST BE FIRST - + // 2. Tables that depend on users { name: '2025103001-create-workflow-requests', module: m1 }, { name: '2025103002-create-approval-levels', module: m2 }, @@ -51,7 +51,7 @@ const migrations: Migration[] = [ { name: '20251031_02_create_activities', module: m6 }, { name: '20251031_03_create_work_notes', module: m7 }, { name: '20251031_04_create_work_note_attachments', module: m8 }, - + // 3. Table modifications and additional features { name: '20251104-add-tat-alert-fields', module: m9 }, { name: '20251104-create-tat-alerts', module: m10 }, @@ -82,7 +82,7 @@ const migrations: Migration[] = [ async function ensureMigrationsTable(queryInterface: QueryInterface): Promise { try { const tables = await queryInterface.showAllTables(); - + if (!tables.includes('migrations')) { await queryInterface.sequelize.query(` CREATE TABLE migrations ( @@ -102,12 +102,12 @@ async function ensureMigrationsTable(queryInterface: QueryInterface): Promise { +async function getExecutedMigrations(sequelize: any): Promise { try { - const results = await sequelize.query<{ name: string }>( + const results = await sequelize.query( 'SELECT name FROM migrations ORDER BY id', { type: QueryTypes.SELECT } - ); + ) as { name: string }[]; return results.map(r => r.name); } catch (error) { // Table might not exist yet @@ -118,7 +118,7 @@ async function getExecutedMigrations(): Promise { /** * Mark migration as executed */ -async function markMigrationExecuted(name: string): Promise { +async function markMigrationExecuted(sequelize: any, name: string): Promise { await sequelize.query( 'INSERT INTO migrations (name) VALUES (:name) ON CONFLICT (name) DO NOTHING', { @@ -133,41 +133,47 @@ async function markMigrationExecuted(name: string): Promise { */ async function run() { try { + console.log('🔐 Initializing secrets...'); + await initializeGoogleSecretManager(); + + // Dynamically import sequelize after secrets are loaded + const { sequelize } = require('../config/database'); + await sequelize.authenticate(); - + const queryInterface = sequelize.getQueryInterface(); - + // Ensure migrations tracking table exists await ensureMigrationsTable(queryInterface); - + // Get already executed migrations - const executedMigrations = await getExecutedMigrations(); - + const executedMigrations = await getExecutedMigrations(sequelize); + // Find pending migrations const pendingMigrations = migrations.filter( m => !executedMigrations.includes(m.name) ); - + if (pendingMigrations.length === 0) { console.log('✅ Migrations up-to-date'); process.exit(0); return; } - + console.log(`🔄 Running ${pendingMigrations.length} migration(s)...`); - + // Run each pending migration for (const migration of pendingMigrations) { try { await migration.module.up(queryInterface); - await markMigrationExecuted(migration.name); + await markMigrationExecuted(sequelize, migration.name); console.log(`✅ ${migration.name}`); } catch (error: any) { console.error(`❌ Migration failed: ${migration.name} - ${error.message}`); throw error; } } - + console.log(`✅ Applied ${pendingMigrations.length} migration(s)`); process.exit(0); } catch (err: any) { diff --git a/src/server.ts b/src/server.ts index 04abeff..ca933ec 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,16 +1,13 @@ import http from 'http'; -import { initializeSecrets } from './app'; // Import initialization function -import app from './app'; -import { initSocket } from './realtime/socket'; -import './queues/tatWorker'; // Initialize TAT worker -import { logTatConfig } from './config/tat.config'; -import { logSystemConfig } from './config/system.config'; -import { initializeHolidaysCache } from './utils/tatTimeUtils'; -import { seedDefaultConfigurations } from './services/configSeed.service'; -import { startPauseResumeJob } from './jobs/pauseResumeJob'; -import './queues/pauseResumeWorker'; // Initialize pause resume worker -import { initializeQueueMetrics, stopQueueMetrics } from './utils/queueMetrics'; -import { emailService } from './services/email.service'; +import dotenv from 'dotenv'; +import path from 'path'; + +// Load environment variables from .env file FIRST +dotenv.config({ path: path.resolve(__dirname, '../.env') }); +import { initializeGoogleSecretManager } from './services/googleSecretManager.service'; +import { stopQueueMetrics } from './utils/queueMetrics'; + +// Dynamic imports will be used inside startServer to ensure secrets are loaded first const PORT: number = parseInt(process.env.PORT || '5000', 10); @@ -19,7 +16,21 @@ const startServer = async (): Promise => { try { // Initialize Google Secret Manager before starting server // This will merge secrets from GCS into process.env if enabled - await initializeSecrets(); + console.log('🔐 Initializing secrets...'); + await initializeGoogleSecretManager(); + + // Dynamically import everything else after secrets are loaded + const app = require('./app').default; + const { initSocket } = require('./realtime/socket'); + require('./queues/tatWorker'); // Initialize TAT worker + const { logTatConfig } = require('./config/tat.config'); + const { logSystemConfig } = require('./config/system.config'); + const { initializeHolidaysCache } = require('./utils/tatTimeUtils'); + const { seedDefaultConfigurations } = require('./services/configSeed.service'); + const { startPauseResumeJob } = require('./jobs/pauseResumeJob'); + require('./queues/pauseResumeWorker'); // Initialize pause resume worker + const { initializeQueueMetrics } = require('./utils/queueMetrics'); + const { emailService } = require('./services/email.service'); // Re-initialize email service after secrets are loaded (in case SMTP credentials were loaded) // This ensures the email service uses production SMTP if credentials are available