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