db ssl related changes postman submit bug resolved

This commit is contained in:
laxmanhalaki 2026-02-06 19:53:47 +05:30
parent e03049a861
commit b925ee5217
7 changed files with 250 additions and 198 deletions

View File

@ -3,6 +3,16 @@ import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
// 1. Debugging: Print what the app actually sees
console.log('--- Database Config Debug ---');
console.log(`DB_HOST: ${process.env.DB_HOST}`);
console.log(`DB_SSL (Raw): '${process.env.DB_SSL}`); // Quotes help see trailing spaces
// 2. Fix: Trim whitespace to ensure "true " becomes "true"
const isSSL = (process.env.DB_SSL || '').trim() === 'true';
console.log(`SSL Enabled: ${isSSL}`);
console.log('---------------------------');
const sequelize = new Sequelize({ const sequelize = new Sequelize({
host: process.env.DB_HOST || 'localhost', host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10), port: parseInt(process.env.DB_PORT || '5432', 10),
@ -10,7 +20,7 @@ const sequelize = new Sequelize({
username: process.env.DB_USER || 'postgres', username: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres', password: process.env.DB_PASSWORD || 'postgres',
dialect: 'postgres', dialect: 'postgres',
logging: false, // Disable SQL query logging for cleaner console output logging: false,
pool: { pool: {
min: parseInt(process.env.DB_POOL_MIN || '2', 10), min: parseInt(process.env.DB_POOL_MIN || '2', 10),
max: parseInt(process.env.DB_POOL_MAX || '10', 10), max: parseInt(process.env.DB_POOL_MAX || '10', 10),
@ -18,7 +28,8 @@ const sequelize = new Sequelize({
idle: 10000, idle: 10000,
}, },
dialectOptions: { dialectOptions: {
ssl: process.env.DB_SSL === 'true' ? { // 3. Use the robust boolean we calculated above
ssl: isSSL ? {
require: true, require: true,
rejectUnauthorized: false, rejectUnauthorized: false,
} : false, } : false,

View File

@ -236,6 +236,7 @@ export class WorkflowController {
priority: validated.priority as Priority, priority: validated.priority as Priority,
approvalLevels: enrichedApprovalLevels, approvalLevels: enrichedApprovalLevels,
participants: autoGeneratedParticipants, participants: autoGeneratedParticipants,
isDraft: parsed.isDraft === true, // Submit by default unless isDraft is explicitly true
} as any; } as any;
const requestMeta = getRequestMetadata(req); const requestMeta = getRequestMetadata(req);
@ -682,7 +683,10 @@ export class WorkflowController {
} }
const parsed = JSON.parse(raw); const parsed = JSON.parse(raw);
const validated = validateUpdateWorkflow(parsed); const validated = validateUpdateWorkflow(parsed);
const updateData: UpdateWorkflowRequest = { ...validated } as any; const updateData: UpdateWorkflowRequest = {
...validated,
isDraft: parsed.isDraft !== undefined ? (parsed.isDraft === true) : undefined
} as any;
if (validated.priority) { if (validated.priority) {
updateData.priority = validated.priority === 'EXPRESS' ? Priority.EXPRESS : Priority.STANDARD; updateData.priority = validated.priority === 'EXPRESS' ? Priority.EXPRESS : Priority.STANDARD;
} }

View File

@ -22,7 +22,7 @@ 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 // DB constants moved inside functions to ensure secrets are loaded first
const isSSL = (process.env.DB_SSL || '').trim() === 'true';
async function checkAndCreateDatabase(): Promise<boolean> { async function checkAndCreateDatabase(): Promise<boolean> {
const DB_HOST = process.env.DB_HOST || 'localhost'; const DB_HOST = process.env.DB_HOST || 'localhost';
const DB_PORT = parseInt(process.env.DB_PORT || '5432'); const DB_PORT = parseInt(process.env.DB_PORT || '5432');
@ -36,6 +36,7 @@ async function checkAndCreateDatabase(): Promise<boolean> {
user: DB_USER, user: DB_USER,
password: DB_PASSWORD, password: DB_PASSWORD,
database: 'postgres', // Connect to default postgres database database: 'postgres', // Connect to default postgres database
ssl: isSSL ? { rejectUnauthorized: false } : undefined,
}); });
try { try {
@ -64,6 +65,7 @@ async function checkAndCreateDatabase(): Promise<boolean> {
user: DB_USER, user: DB_USER,
password: DB_PASSWORD, password: DB_PASSWORD,
database: DB_NAME, database: DB_NAME,
ssl: isSSL ? { rejectUnauthorized: false } : undefined,
}); });
await newDbClient.connect(); await newDbClient.connect();

View File

@ -337,7 +337,7 @@ class GoogleSecretManagerService {
private getDefaultSecretNames(): string[] { private getDefaultSecretNames(): string[] {
return [ return [
// Database // Database
//'DB_PASSWORD', 'DB_PASSWORD',
// JWT & Session // JWT & Session
'JWT_SECRET', 'JWT_SECRET',

View File

@ -2450,6 +2450,9 @@ export class WorkflowService {
try { try {
const requestNumber = await generateRequestNumber(); const requestNumber = await generateRequestNumber();
const totalTatHours = workflowData.approvalLevels.reduce((sum, level) => sum + level.tatHours, 0); const totalTatHours = workflowData.approvalLevels.reduce((sum, level) => sum + level.tatHours, 0);
const isDraftRequested = workflowData.isDraft === true;
const initialStatus = isDraftRequested ? WorkflowStatus.DRAFT : WorkflowStatus.PENDING;
const now = new Date();
const workflow = await WorkflowRequest.create({ const workflow = await WorkflowRequest.create({
requestNumber, requestNumber,
@ -2461,9 +2464,10 @@ export class WorkflowService {
currentLevel: 1, currentLevel: 1,
totalLevels: workflowData.approvalLevels.length, totalLevels: workflowData.approvalLevels.length,
totalTatHours, totalTatHours,
status: WorkflowStatus.DRAFT, status: initialStatus,
isDraft: true, isDraft: isDraftRequested,
isDeleted: false isDeleted: false,
submissionDate: isDraftRequested ? undefined : now
}); });
// Create approval levels // Create approval levels
@ -2549,15 +2553,18 @@ export class WorkflowService {
type: 'created', type: 'created',
user: { userId: initiatorId, name: initiatorName }, user: { userId: initiatorId, name: initiatorName },
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
action: 'Initial request submitted', action: isDraftRequested ? 'Draft request created' : 'Initial request submitted',
details: `Initial request submitted for ${workflowData.title} by ${initiatorName}`, details: isDraftRequested
? `Draft request "${workflowData.title}" created by ${initiatorName}`
: `Initial request submitted for ${workflowData.title} by ${initiatorName}`,
ipAddress: requestMetadata?.ipAddress || undefined, ipAddress: requestMetadata?.ipAddress || undefined,
userAgent: requestMetadata?.userAgent || undefined userAgent: requestMetadata?.userAgent || undefined
}); });
// NOTE: Notifications are NOT sent here because workflows are created as DRAFTS // If not a draft, initiate the workflow (approvals, notifications, etc.)
// Notifications will be sent in submitWorkflow() when the draft is actually submitted if (!isDraftRequested) {
// This prevents approvers from being notified about draft requests return await this.internalSubmitWorkflow(workflow, now);
}
return workflow; return workflow;
} catch (error) { } catch (error) {
@ -3112,6 +3119,9 @@ export class WorkflowService {
// Only allow full updates (approval levels, participants) for DRAFT workflows // Only allow full updates (approval levels, participants) for DRAFT workflows
const isDraft = (workflow as any).status === WorkflowStatus.DRAFT || (workflow as any).isDraft; const isDraft = (workflow as any).status === WorkflowStatus.DRAFT || (workflow as any).isDraft;
// Determine if this is a transition from draft to submitted
const isTransitioningToSubmitted = updateData.isDraft === false && (workflow as any).isDraft;
// Update basic workflow fields // Update basic workflow fields
const basicUpdate: any = {}; const basicUpdate: any = {};
if (updateData.title) basicUpdate.title = updateData.title; if (updateData.title) basicUpdate.title = updateData.title;
@ -3119,6 +3129,13 @@ export class WorkflowService {
if (updateData.priority) basicUpdate.priority = updateData.priority; if (updateData.priority) basicUpdate.priority = updateData.priority;
if (updateData.status) basicUpdate.status = updateData.status; if (updateData.status) basicUpdate.status = updateData.status;
if (updateData.conclusionRemark !== undefined) basicUpdate.conclusionRemark = updateData.conclusionRemark; if (updateData.conclusionRemark !== undefined) basicUpdate.conclusionRemark = updateData.conclusionRemark;
if (updateData.isDraft !== undefined) basicUpdate.isDraft = updateData.isDraft;
// If transitioning, ensure status and submissionDate are set
if (isTransitioningToSubmitted) {
basicUpdate.status = WorkflowStatus.PENDING;
basicUpdate.submissionDate = new Date();
}
await workflow.update(basicUpdate); await workflow.update(basicUpdate);
@ -3267,8 +3284,13 @@ export class WorkflowService {
logger.info(`Marked ${deleteResult[0]} documents as deleted in database (out of ${updateData.deleteDocumentIds.length} requested)`); logger.info(`Marked ${deleteResult[0]} documents as deleted in database (out of ${updateData.deleteDocumentIds.length} requested)`);
} }
// Reload the workflow instance to get latest data (without associations to avoid the error) // If transitioning, call the internal submission logic (notifications, TAT, etc.)
// The associations issue occurs when trying to include them, so we skip that if (isTransitioningToSubmitted) {
logger.info(`[Workflow] Transitioning draft ${actualRequestId} to submitted state`);
return await this.internalSubmitWorkflow(workflow, (workflow as any).submissionDate);
}
// Reload the workflow instance to get latest data
const refreshed = await WorkflowRequest.findByPk(actualRequestId); const refreshed = await WorkflowRequest.findByPk(actualRequestId);
return refreshed; return refreshed;
} catch (error) { } catch (error) {
@ -3290,160 +3312,169 @@ export class WorkflowService {
const workflow = await this.findWorkflowByIdentifier(requestId); const workflow = await this.findWorkflowByIdentifier(requestId);
if (!workflow) return null; if (!workflow) return null;
// Get the actual requestId (UUID) - handle both UUID and requestNumber cases
const actualRequestId = (workflow as any).getDataValue
? (workflow as any).getDataValue('requestId')
: (workflow as any).requestId;
const now = new Date(); const now = new Date();
const updated = await workflow.update({ return await this.internalSubmitWorkflow(workflow, now);
status: WorkflowStatus.PENDING,
isDraft: false,
submissionDate: now
});
// Get initiator details for activity logging
const initiatorId = (updated as any).initiatorId;
const initiator = initiatorId ? await User.findByPk(initiatorId) : null;
const initiatorName = (initiator as any)?.displayName || (initiator as any)?.email || 'User';
const workflowTitle = (updated as any).title || 'Request';
const requestNumber = (updated as any).requestNumber;
// Check if this was a previously saved draft (has activity history before submission)
// or a direct submission (createWorkflow + submitWorkflow in same flow)
const { Activity } = require('@models/Activity');
const existingActivities = await Activity.count({
where: { requestId: actualRequestId }
});
// Only log "Request submitted" if this is a draft being submitted (has prior activities)
// For direct submissions, createWorkflow already logs "Initial request submitted"
if (existingActivities > 1) {
// This is a saved draft being submitted later
activityService.log({
requestId: actualRequestId,
type: 'submitted',
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
timestamp: new Date().toISOString(),
action: 'Draft submitted',
details: `Draft request "${workflowTitle}" submitted for approval by ${initiatorName}`
});
} else {
// Direct submission - just update the status, createWorkflow already logged the activity
activityService.log({
requestId: actualRequestId,
type: 'submitted',
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
timestamp: new Date().toISOString(),
action: 'Request submitted',
details: `Request "${workflowTitle}" submitted for approval`
});
}
const current = await ApprovalLevel.findOne({
where: { requestId: actualRequestId, levelNumber: (updated as any).currentLevel || 1 }
});
if (current) {
// Set the first level's start time and schedule TAT jobs
await current.update({
levelStartTime: now,
tatStartTime: now,
status: ApprovalStatus.IN_PROGRESS
});
// Log assignment activity for the first approver (similar to createWorkflow)
activityService.log({
requestId: actualRequestId,
type: 'assignment',
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
timestamp: new Date().toISOString(),
action: 'Assigned to approver',
details: `Request assigned to ${(current as any).approverName || (current as any).approverEmail || 'approver'} for review`
});
// Schedule TAT notification jobs for the first level
try {
const workflowPriority = (updated as any).priority || 'STANDARD';
await tatSchedulerService.scheduleTatJobs(
actualRequestId,
(current as any).levelId,
(current as any).approverId,
Number((current as any).tatHours),
now,
workflowPriority // Pass workflow priority (EXPRESS = 24/7, STANDARD = working hours)
);
logger.info(`[Workflow] TAT jobs scheduled for first level of request ${requestNumber} (Priority: ${workflowPriority})`);
} catch (tatError) {
logger.error(`[Workflow] Failed to schedule TAT jobs:`, tatError);
// Don't fail the submission if TAT scheduling fails
}
// Send notifications when workflow is submitted (not when created as draft)
// Send notification to INITIATOR confirming submission
await notificationService.sendToUsers([initiatorId], {
title: 'Request Submitted Successfully',
body: `Your request "${workflowTitle}" has been submitted and is now with the first approver.`,
requestNumber: requestNumber,
requestId: actualRequestId,
url: `/request/${requestNumber}`,
type: 'request_submitted',
priority: 'MEDIUM'
});
// Send notification to FIRST APPROVER for assignment
await notificationService.sendToUsers([(current as any).approverId], {
title: 'New Request Assigned',
body: `${workflowTitle}`,
requestNumber: requestNumber,
requestId: actualRequestId,
url: `/request/${requestNumber}`,
type: 'assignment',
priority: 'HIGH',
actionRequired: true
});
}
// Send notifications to SPECTATORS (in-app, email, and web push)
// Moved outside the if(current) block to ensure spectators are always notified on submission
try {
logger.info(`[Workflow] Querying spectators for request ${requestNumber} (requestId: ${actualRequestId})`);
const spectators = await Participant.findAll({
where: {
requestId: actualRequestId, // Use the actual UUID requestId
participantType: ParticipantType.SPECTATOR,
isActive: true,
notificationEnabled: true
},
attributes: ['userId', 'userEmail', 'userName']
});
logger.info(`[Workflow] Found ${spectators.length} active spectators for request ${requestNumber}`);
if (spectators.length > 0) {
const spectatorUserIds = spectators.map((s: any) => s.userId);
logger.info(`[Workflow] Sending notifications to ${spectatorUserIds.length} spectators: ${spectatorUserIds.join(', ')}`);
await notificationService.sendToUsers(spectatorUserIds, {
title: 'Added to Request',
body: `You have been added as a spectator to request ${requestNumber}: ${workflowTitle}`,
requestNumber: requestNumber,
requestId: actualRequestId,
url: `/request/${requestNumber}`,
type: 'spectator_added',
priority: 'MEDIUM'
});
logger.info(`[Workflow] Successfully sent notifications to ${spectators.length} spectators for request ${requestNumber}`);
} else {
logger.info(`[Workflow] No active spectators found for request ${requestNumber} (requestId: ${actualRequestId})`);
}
} catch (spectatorError) {
logger.error(`[Workflow] Failed to send spectator notifications for request ${requestNumber} (requestId: ${actualRequestId}):`, spectatorError);
// Don't fail the submission if spectator notifications fail
}
return updated;
} catch (error) { } catch (error) {
logger.error(`Failed to submit workflow ${requestId}:`, error); logger.error(`Failed to submit workflow ${requestId}:`, error);
throw new Error('Failed to submit workflow'); throw new Error('Failed to submit workflow');
} }
} }
/**
* Internal method to handle workflow submission logic (status update, notifications, TAT scheduling)
* Centralized here to support both direct creation-submission and draft-to-submission flows.
*/
private async internalSubmitWorkflow(workflow: WorkflowRequest, now: Date): Promise<WorkflowRequest> {
// Get the actual requestId (UUID) - handle both UUID and requestNumber cases
const actualRequestId = (workflow as any).getDataValue
? (workflow as any).getDataValue('requestId')
: (workflow as any).requestId;
const updated = await workflow.update({
status: WorkflowStatus.PENDING,
isDraft: false,
submissionDate: now
});
// Get initiator details for activity logging
const initiatorId = (updated as any).initiatorId;
const initiator = initiatorId ? await User.findByPk(initiatorId) : null;
const initiatorName = (initiator as any)?.displayName || (initiator as any)?.email || 'User';
const workflowTitle = (updated as any).title || 'Request';
const requestNumber = (updated as any).requestNumber;
// Check if this was a previously saved draft (has activity history before submission)
// or a direct submission (createWorkflow + submitWorkflow in same flow)
const { Activity } = require('@models/Activity');
const existingActivities = await Activity.count({
where: { requestId: actualRequestId }
});
// Only log "Request submitted" if this is a draft being submitted (has prior activities)
// For direct submissions, createWorkflow already logs "Initial request submitted"
if (existingActivities > 1) {
// This is a saved draft being submitted later
activityService.log({
requestId: actualRequestId,
type: 'submitted',
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
timestamp: new Date().toISOString(),
action: 'Draft submitted',
details: `Draft request "${workflowTitle}" submitted for approval by ${initiatorName}`
});
} else {
// Direct submission - just update the status, createWorkflow already logged the activity
activityService.log({
requestId: actualRequestId,
type: 'submitted',
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
timestamp: new Date().toISOString(),
action: 'Request submitted',
details: `Request "${workflowTitle}" submitted for approval`
});
}
const current = await ApprovalLevel.findOne({
where: { requestId: actualRequestId, levelNumber: (updated as any).currentLevel || 1 }
});
if (current) {
// Set the first level's start time and schedule TAT jobs
await current.update({
levelStartTime: now,
tatStartTime: now,
status: ApprovalStatus.IN_PROGRESS
});
// Log assignment activity for the first approver (similar to createWorkflow)
activityService.log({
requestId: actualRequestId,
type: 'assignment',
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
timestamp: new Date().toISOString(),
action: 'Assigned to approver',
details: `Request assigned to ${(current as any).approverName || (current as any).approverEmail || 'approver'} for review`
});
// Schedule TAT notification jobs for the first level
try {
const workflowPriority = (updated as any).priority || 'STANDARD';
await tatSchedulerService.scheduleTatJobs(
actualRequestId,
(current as any).levelId,
(current as any).approverId,
Number((current as any).tatHours),
now,
workflowPriority // Pass workflow priority (EXPRESS = 24/7, STANDARD = working hours)
);
logger.info(`[Workflow] TAT jobs scheduled for first level of request ${requestNumber} (Priority: ${workflowPriority})`);
} catch (tatError) {
logger.error(`[Workflow] Failed to schedule TAT jobs:`, tatError);
// Don't fail the submission if TAT scheduling fails
}
// Send notifications when workflow is submitted (not when created as draft)
// Send notification to INITIATOR confirming submission
await notificationService.sendToUsers([initiatorId], {
title: 'Request Submitted Successfully',
body: `Your request "${workflowTitle}" has been submitted and is now with the first approver.`,
requestNumber: requestNumber,
requestId: actualRequestId,
url: `/request/${requestNumber}`,
type: 'request_submitted',
priority: 'MEDIUM'
});
// Send notification to FIRST APPROVER for assignment
await notificationService.sendToUsers([(current as any).approverId], {
title: 'New Request Assigned',
body: `${workflowTitle}`,
requestNumber: requestNumber,
requestId: actualRequestId,
url: `/request/${requestNumber}`,
type: 'assignment',
priority: 'HIGH',
actionRequired: true
});
}
// Send notifications to SPECTATORS (in-app, email, and web push)
// Moved outside the if(current) block to ensure spectators are always notified on submission
try {
logger.info(`[Workflow] Querying spectators for request ${requestNumber} (requestId: ${actualRequestId})`);
const spectators = await Participant.findAll({
where: {
requestId: actualRequestId, // Use the actual UUID requestId
participantType: ParticipantType.SPECTATOR,
isActive: true,
notificationEnabled: true
},
attributes: ['userId', 'userEmail', 'userName']
});
logger.info(`[Workflow] Found ${spectators.length} active spectators for request ${requestNumber}`);
if (spectators.length > 0) {
const spectatorUserIds = spectators.map((s: any) => s.userId);
logger.info(`[Workflow] Sending notifications to ${spectatorUserIds.length} spectators: ${spectatorUserIds.join(', ')}`);
await notificationService.sendToUsers(spectatorUserIds, {
title: 'Added to Request',
body: `You have been added as a spectator to request ${requestNumber}: ${workflowTitle}`,
requestNumber: requestNumber,
requestId: actualRequestId,
url: `/request/${requestNumber}`,
type: 'spectator_added',
priority: 'MEDIUM'
});
logger.info(`[Workflow] Successfully sent notifications to ${spectators.length} spectators for request ${requestNumber}`);
} else {
logger.info(`[Workflow] No active spectators found for request ${requestNumber} (requestId: ${actualRequestId})`);
}
} catch (spectatorError) {
logger.error(`[Workflow] Failed to send spectator notifications for request ${requestNumber} (requestId: ${actualRequestId}):`, spectatorError);
// Don't fail the submission if spectator notifications fail
}
return updated;
}
} }

View File

@ -29,6 +29,7 @@ export interface CreateWorkflowRequest {
priority: Priority; priority: Priority;
approvalLevels: CreateApprovalLevel[]; approvalLevels: CreateApprovalLevel[];
participants?: CreateParticipant[]; participants?: CreateParticipant[];
isDraft?: boolean;
} }
export interface UpdateWorkflowRequest { export interface UpdateWorkflowRequest {
@ -42,6 +43,7 @@ export interface UpdateWorkflowRequest {
participants?: CreateParticipant[]; participants?: CreateParticipant[];
// Document updates (add new documents via multipart, delete via IDs) // Document updates (add new documents via multipart, delete via IDs)
deleteDocumentIds?: string[]; deleteDocumentIds?: string[];
isDraft?: boolean;
} }
export interface CreateApprovalLevel { export interface CreateApprovalLevel {

View File

@ -44,6 +44,7 @@ export const createWorkflowSchema = z.object({
priorityUi: z.string().optional(), priorityUi: z.string().optional(),
templateId: z.string().optional(), templateId: z.string().optional(),
ccList: z.array(z.any()).optional(), ccList: z.array(z.any()).optional(),
isDraft: z.boolean().optional(),
}); });
export const updateWorkflowSchema = z.object({ export const updateWorkflowSchema = z.object({
@ -73,6 +74,7 @@ export const updateWorkflowSchema = z.object({
notificationEnabled: z.boolean().optional(), notificationEnabled: z.boolean().optional(),
})).optional(), })).optional(),
deleteDocumentIds: z.array(z.string().uuid()).optional(), deleteDocumentIds: z.array(z.string().uuid()).optional(),
isDraft: z.boolean().optional(),
}); });
// Helper to validate UUID or requestNumber format // Helper to validate UUID or requestNumber format