diff --git a/src/config/sso.ts b/src/config/sso.ts index a79e23d..278ce16 100644 --- a/src/config/sso.ts +++ b/src/config/sso.ts @@ -1,16 +1,20 @@ import { SSOConfig, SSOUserData } from '../types/auth.types'; +// Use getter functions to read from process.env dynamically +// This ensures values are read after secrets are loaded from Google Secret Manager const ssoConfig: SSOConfig = { - jwtSecret: process.env.JWT_SECRET || '', - jwtExpiry: process.env.JWT_EXPIRY || '24h', - refreshTokenExpiry: process.env.REFRESH_TOKEN_EXPIRY || '7d', - sessionSecret: process.env.SESSION_SECRET || '', + get jwtSecret() { return process.env.JWT_SECRET || ''; }, + get jwtExpiry() { return process.env.JWT_EXPIRY || '24h'; }, + get refreshTokenExpiry() { return process.env.REFRESH_TOKEN_EXPIRY || '7d'; }, + get sessionSecret() { return process.env.SESSION_SECRET || ''; }, // Use only FRONTEND_URL from environment - no fallbacks - allowedOrigins: process.env.FRONTEND_URL?.split(',').map(s => s.trim()).filter(Boolean) || [], + get allowedOrigins() { + return process.env.FRONTEND_URL?.split(',').map(s => s.trim()).filter(Boolean) || []; + }, // Okta/Auth0 configuration for token exchange - oktaDomain: process.env.OKTA_DOMAIN || 'https://dev-830839.oktapreview.com', - oktaClientId: process.env.OKTA_CLIENT_ID || '', - oktaClientSecret: process.env.OKTA_CLIENT_SECRET || '', + get oktaDomain() { return process.env.OKTA_DOMAIN || 'https://dev-830839.oktapreview.com'; }, + get oktaClientId() { return process.env.OKTA_CLIENT_ID || ''; }, + get oktaClientSecret() { return process.env.OKTA_CLIENT_SECRET || ''; }, }; export { ssoConfig }; diff --git a/src/emailtemplates/approvalConfirmation.template.ts b/src/emailtemplates/approvalConfirmation.template.ts index 97f1e1b..c3d0c59 100644 --- a/src/emailtemplates/approvalConfirmation.template.ts +++ b/src/emailtemplates/approvalConfirmation.template.ts @@ -3,7 +3,7 @@ */ import { ApprovalConfirmationData } from './types'; -import { getEmailFooter, getEmailHeader, HeaderStyles, getNextStepsSection, wrapRichText, getResponsiveStyles } from './helpers'; +import { getEmailFooter, getEmailHeader, HeaderStyles, getNextStepsSection, wrapRichText, getResponsiveStyles, getEmailContainerStyles } from './helpers'; import { getBrandedHeader } from './branding.config'; export function getApprovalConfirmationEmail(data: ApprovalConfirmationData): string { @@ -31,7 +31,7 @@ export function getApprovalConfirmationEmail(data: ApprovalConfirmationData): st
- + ${getEmailHeader(getBrandedHeader({ title: 'Request Approved', ...HeaderStyles.success diff --git a/src/emailtemplates/approvalRequest.template.ts b/src/emailtemplates/approvalRequest.template.ts index 295d4f2..4b200e4 100644 --- a/src/emailtemplates/approvalRequest.template.ts +++ b/src/emailtemplates/approvalRequest.template.ts @@ -3,7 +3,7 @@ */ import { ApprovalRequestData } from './types'; -import { getEmailFooter, getPrioritySection, getEmailHeader, HeaderStyles, getResponsiveStyles, wrapRichText } from './helpers'; +import { getEmailFooter, getPrioritySection, getEmailHeader, HeaderStyles, getResponsiveStyles, wrapRichText, getEmailContainerStyles } from './helpers'; import { getBrandedHeader } from './branding.config'; export function getApprovalRequestEmail(data: ApprovalRequestData): string { @@ -22,7 +22,7 @@ export function getApprovalRequestEmail(data: ApprovalRequestData): string {
- + ${getEmailHeader(getBrandedHeader({ title: 'Approval Request', diff --git a/src/emailtemplates/approverSkipped.template.ts b/src/emailtemplates/approverSkipped.template.ts index 3619bdf..0831635 100644 --- a/src/emailtemplates/approverSkipped.template.ts +++ b/src/emailtemplates/approverSkipped.template.ts @@ -3,7 +3,7 @@ */ import { ApproverSkippedData } from './types'; -import { getEmailFooter, getEmailHeader, HeaderStyles, wrapRichText, getResponsiveStyles } from './helpers'; +import { getEmailFooter, getEmailHeader, HeaderStyles, wrapRichText, getResponsiveStyles, getEmailContainerStyles } from './helpers'; import { getBrandedHeader } from './branding.config'; export function getApproverSkippedEmail(data: ApproverSkippedData): string { @@ -22,7 +22,7 @@ export function getApproverSkippedEmail(data: ApproverSkippedData): string {
- + ${getEmailHeader(getBrandedHeader({ title: 'Approval Level Skipped', ...HeaderStyles.infoSecondary diff --git a/src/emailtemplates/helpers.ts b/src/emailtemplates/helpers.ts index 944c98d..8fa60a2 100644 --- a/src/emailtemplates/helpers.ts +++ b/src/emailtemplates/helpers.ts @@ -144,6 +144,15 @@ export function wrapRichText(htmlContent: string): string { `; } +/** + * Get inline styles for email container table + * This ensures width is preserved when emails are forwarded + * Email clients often strip CSS classes, so inline styles are critical + */ +export function getEmailContainerStyles(): string { + return 'width: 95%; max-width: 1200px; min-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'; +} + /** * Generate all email styles (responsive + rich text) * Desktop-first design (optimized for browser) with mobile responsive breakpoints @@ -175,8 +184,22 @@ export function getResponsiveStyles(): string { /* Desktop-first base styles */ .email-container { - width: 95%; - max-width: 1200px; + width: 95% !important; + max-width: 1200px !important; + min-width: 600px !important; /* Prevent shrinking below 600px when forwarded */ + } + + /* Force full width for forwarded emails - use inline styles in templates */ + table.email-container { + width: 95% !important; + max-width: 1200px !important; + min-width: 600px !important; + } + + /* Wrapper table to force full width even when forwarded */ + .email-wrapper { + width: 100% !important; + max-width: 100% !important; } .email-content { diff --git a/src/emailtemplates/multiApproverRequest.template.ts b/src/emailtemplates/multiApproverRequest.template.ts index b1d1027..8b35229 100644 --- a/src/emailtemplates/multiApproverRequest.template.ts +++ b/src/emailtemplates/multiApproverRequest.template.ts @@ -3,7 +3,7 @@ */ import { MultiApproverRequestData } from './types'; -import { getEmailFooter, getPrioritySection, getApprovalChain, getEmailHeader, HeaderStyles, wrapRichText, getResponsiveStyles } from './helpers'; +import { getEmailFooter, getPrioritySection, getApprovalChain, getEmailHeader, HeaderStyles, wrapRichText, getResponsiveStyles, getEmailContainerStyles } from './helpers'; import { getBrandedHeader } from './branding.config'; export function getMultiApproverRequestEmail(data: MultiApproverRequestData): string { @@ -22,7 +22,7 @@ export function getMultiApproverRequestEmail(data: MultiApproverRequestData): st
- + ${getEmailHeader(getBrandedHeader({ title: 'Multi-Level Approval Request', diff --git a/src/emailtemplates/participantAdded.template.ts b/src/emailtemplates/participantAdded.template.ts index 093b965..540d391 100644 --- a/src/emailtemplates/participantAdded.template.ts +++ b/src/emailtemplates/participantAdded.template.ts @@ -3,7 +3,7 @@ */ import { ParticipantAddedData } from './types'; -import { getEmailFooter, getEmailHeader, HeaderStyles, getPermissionsContent, getRoleDescription, wrapRichText, getResponsiveStyles } from './helpers'; +import { getEmailFooter, getEmailHeader, HeaderStyles, getPermissionsContent, getRoleDescription, wrapRichText, getResponsiveStyles, getEmailContainerStyles } from './helpers'; import { getBrandedHeader } from './branding.config'; export function getParticipantAddedEmail(data: ParticipantAddedData): string { @@ -22,7 +22,7 @@ export function getParticipantAddedEmail(data: ParticipantAddedData): string {
- + ${getEmailHeader(getBrandedHeader({ title: `You've Been Added as ${data.participantRole}`, ...HeaderStyles.info diff --git a/src/emailtemplates/rejectionNotification.template.ts b/src/emailtemplates/rejectionNotification.template.ts index 87bdb05..e30335a 100644 --- a/src/emailtemplates/rejectionNotification.template.ts +++ b/src/emailtemplates/rejectionNotification.template.ts @@ -3,7 +3,7 @@ */ import { RejectionNotificationData } from './types'; -import { getEmailFooter, getEmailHeader, HeaderStyles, wrapRichText, getResponsiveStyles } from './helpers'; +import { getEmailFooter, getEmailHeader, HeaderStyles, wrapRichText, getResponsiveStyles, getEmailContainerStyles } from './helpers'; import { getBrandedHeader } from './branding.config'; export function getRejectionNotificationEmail(data: RejectionNotificationData): string { @@ -22,7 +22,7 @@ export function getRejectionNotificationEmail(data: RejectionNotificationData):
- + ${getEmailHeader(getBrandedHeader({ title: 'Request Rejected', ...HeaderStyles.error diff --git a/src/emailtemplates/requestClosed.template.ts b/src/emailtemplates/requestClosed.template.ts index 783fa9d..c36ccaf 100644 --- a/src/emailtemplates/requestClosed.template.ts +++ b/src/emailtemplates/requestClosed.template.ts @@ -3,7 +3,7 @@ */ import { RequestClosedData } from './types'; -import { getEmailFooter, getEmailHeader, HeaderStyles, getConclusionSection, getResponsiveStyles } from './helpers'; +import { getEmailFooter, getEmailHeader, HeaderStyles, getConclusionSection, getResponsiveStyles, getEmailContainerStyles } from './helpers'; import { getBrandedHeader } from './branding.config'; export function getRequestClosedEmail(data: RequestClosedData): string { @@ -22,7 +22,7 @@ export function getRequestClosedEmail(data: RequestClosedData): string {
- + ${getEmailHeader(getBrandedHeader({ title: 'Request Closed', ...HeaderStyles.complete diff --git a/src/emailtemplates/requestCreated.template.ts b/src/emailtemplates/requestCreated.template.ts index 0b3a5bd..c76ea84 100644 --- a/src/emailtemplates/requestCreated.template.ts +++ b/src/emailtemplates/requestCreated.template.ts @@ -3,7 +3,7 @@ */ import { RequestCreatedData } from './types'; -import { getEmailFooter, getEmailHeader, HeaderStyles, getResponsiveStyles, wrapRichText } from './helpers'; +import { getEmailFooter, getEmailHeader, HeaderStyles, getResponsiveStyles, wrapRichText, getEmailContainerStyles } from './helpers'; import { getBrandedHeader } from './branding.config'; export function getRequestCreatedEmail(data: RequestCreatedData): string { @@ -22,7 +22,7 @@ export function getRequestCreatedEmail(data: RequestCreatedData): string {
- + ${getEmailHeader(getBrandedHeader({ title: 'Request Created Successfully', diff --git a/src/emailtemplates/tatBreached.template.ts b/src/emailtemplates/tatBreached.template.ts index c9173a3..ee9e74a 100644 --- a/src/emailtemplates/tatBreached.template.ts +++ b/src/emailtemplates/tatBreached.template.ts @@ -3,7 +3,7 @@ */ import { TATBreachedData } from './types'; -import { getEmailFooter, getEmailHeader, HeaderStyles, getResponsiveStyles } from './helpers'; +import { getEmailFooter, getEmailHeader, HeaderStyles, getResponsiveStyles, getEmailContainerStyles } from './helpers'; import { getBrandedHeader } from './branding.config'; export function getTATBreachedEmail(data: TATBreachedData): string { @@ -22,7 +22,7 @@ export function getTATBreachedEmail(data: TATBreachedData): string {
- + ${getEmailHeader(getBrandedHeader({ title: 'TAT Breached', subtitle: 'Immediate Action Required', diff --git a/src/emailtemplates/tatReminder.template.ts b/src/emailtemplates/tatReminder.template.ts index 0274468..c3f4ecd 100644 --- a/src/emailtemplates/tatReminder.template.ts +++ b/src/emailtemplates/tatReminder.template.ts @@ -3,7 +3,7 @@ */ import { TATReminderData } from './types'; -import { getEmailFooter, getEmailHeader, HeaderStyles, getResponsiveStyles } from './helpers'; +import { getEmailFooter, getEmailHeader, HeaderStyles, getResponsiveStyles, getEmailContainerStyles } from './helpers'; import { getBrandedHeader } from './branding.config'; /** @@ -52,7 +52,7 @@ export function getTATReminderEmail(data: TATReminderData): string {
- + ${getEmailHeader(getBrandedHeader({ title: 'TAT Reminder', subtitle: `${data.thresholdPercentage}% Elapsed - ${urgencyStyle.title}`, diff --git a/src/emailtemplates/workflowPaused.template.ts b/src/emailtemplates/workflowPaused.template.ts index 7ae4058..ba31222 100644 --- a/src/emailtemplates/workflowPaused.template.ts +++ b/src/emailtemplates/workflowPaused.template.ts @@ -3,7 +3,7 @@ */ import { WorkflowPausedData } from './types'; -import { getEmailFooter, getEmailHeader, HeaderStyles, wrapRichText, getResponsiveStyles } from './helpers'; +import { getEmailFooter, getEmailHeader, HeaderStyles, wrapRichText, getResponsiveStyles, getEmailContainerStyles } from './helpers'; import { getBrandedHeader } from './branding.config'; export function getWorkflowPausedEmail(data: WorkflowPausedData): string { @@ -22,7 +22,7 @@ export function getWorkflowPausedEmail(data: WorkflowPausedData): string {
- + ${getEmailHeader(getBrandedHeader({ title: 'Workflow Paused', ...HeaderStyles.neutral diff --git a/src/emailtemplates/workflowResumed.template.ts b/src/emailtemplates/workflowResumed.template.ts index 79f7648..8a4322b 100644 --- a/src/emailtemplates/workflowResumed.template.ts +++ b/src/emailtemplates/workflowResumed.template.ts @@ -3,7 +3,7 @@ */ import { WorkflowResumedData } from './types'; -import { getEmailFooter, getEmailHeader, HeaderStyles, getActionRequiredSection, getResponsiveStyles } from './helpers'; +import { getEmailFooter, getEmailHeader, HeaderStyles, getActionRequiredSection, getResponsiveStyles, getEmailContainerStyles } from './helpers'; import { getBrandedHeader } from './branding.config'; export function getWorkflowResumedEmail(data: WorkflowResumedData): string { @@ -22,7 +22,7 @@ export function getWorkflowResumedEmail(data: WorkflowResumedData): string {
- + ${getEmailHeader(getBrandedHeader({ title: 'Workflow Resumed', ...HeaderStyles.success diff --git a/src/services/googleSecretManager.service.ts b/src/services/googleSecretManager.service.ts index 5d6022f..afad2dd 100644 --- a/src/services/googleSecretManager.service.ts +++ b/src/services/googleSecretManager.service.ts @@ -139,15 +139,24 @@ class GoogleSecretManagerService { logger.debug(`[Secret Manager] Secret ${fullSecretName} exists but payload is empty`); return null; } catch (error: any) { + const isOktaSecret = /OKTA_/i.test(secretName); + const logLevel = isOktaSecret ? logger.info.bind(logger) : logger.debug.bind(logger); + // Handle "not found" errors (code 5 = NOT_FOUND) if (error.code === 5 || error.code === 'NOT_FOUND' || error.message?.includes('not found')) { - logger.debug(`[Secret Manager] Secret not found: ${fullSecretName} (project: ${this.projectId})`); + logLevel(`[Secret Manager] Secret not found: ${fullSecretName} (project: ${this.projectId})`); + if (isOktaSecret) { + logger.info(`[Secret Manager] Searched path: projects/${this.projectId}/secrets/${fullSecretName}/versions/latest`); + } return null; } // Handle permission errors (code 7 = PERMISSION_DENIED) if (error.code === 7 || error.code === 'PERMISSION_DENIED' || error.message?.includes('Permission denied')) { logger.warn(`[Secret Manager] ❌ Permission denied for secret '${fullSecretName}'`); + if (isOktaSecret) { + logger.warn(`[Secret Manager] This is an OKTA secret - check service account permissions`); + } logger.warn(`[Secret Manager] Service account needs 'Secret Manager Secret Accessor' role`); logger.warn(`[Secret Manager] To grant access, run:`); logger.warn(`[Secret Manager] gcloud secrets add-iam-policy-binding ${fullSecretName} \\`); @@ -157,11 +166,12 @@ class GoogleSecretManagerService { return null; } - // Log full error details for debugging - logger.warn(`[Secret Manager] Failed to fetch secret '${fullSecretName}' (project: ${this.projectId})`); - logger.warn(`[Secret Manager] Error code: ${error.code || 'unknown'}, Message: ${error.message || 'no message'}`); + // Log full error details for debugging (info level for OKTA secrets) + const errorLogLevel = isOktaSecret ? logger.warn.bind(logger) : logger.warn.bind(logger); + errorLogLevel(`[Secret Manager] Failed to fetch secret '${fullSecretName}' (project: ${this.projectId})`); + errorLogLevel(`[Secret Manager] Error code: ${error.code || 'unknown'}, Message: ${error.message || 'no message'}`); if (error.details) { - logger.warn(`[Secret Manager] Error details: ${JSON.stringify(error.details)}`); + errorLogLevel(`[Secret Manager] Error details: ${JSON.stringify(error.details)}`); } return null; @@ -216,26 +226,61 @@ class GoogleSecretManagerService { const notFoundSecrets: string[] = []; let loadedCount = 0; + // Log OKTA and EMAIL secrets specifically if they're in the list + const oktaSecrets = secretsToLoad.filter(name => /^OKTA_/i.test(name)); + const emailSecrets = secretsToLoad.filter(name => /^EMAIL_|^SMTP_/i.test(name)); + if (oktaSecrets.length > 0) { + logger.info(`[Secret Manager] 🔍 Attempting to load OKTA secrets: ${oktaSecrets.join(', ')}`); + } + if (emailSecrets.length > 0) { + logger.info(`[Secret Manager] 📧 Attempting to load EMAIL secrets: ${emailSecrets.join(', ')}`); + } + // Load each secret for (const secretName of secretsToLoad) { + const fullSecretName = this.secretPrefix + ? `${this.secretPrefix}-${secretName}` + : secretName; + + // Log OKTA and EMAIL secret attempts in detail + const isOktaSecret = /^OKTA_/i.test(secretName); + const isEmailSecret = /^EMAIL_|^SMTP_/i.test(secretName); + if (isOktaSecret || isEmailSecret) { + logger.info(`[Secret Manager] Attempting to load: ${secretName} (full name: ${fullSecretName})`); + } + const secretValue = await this.getSecret(secretName); if (secretValue !== null) { const envVarName = this.getEnvVarName(secretName); loadedSecrets[envVarName] = secretValue; loadedCount++; + if (isOktaSecret || isEmailSecret) { + logger.info(`[Secret Manager] ✅ Successfully loaded: ${secretName} -> ${envVarName}`); + } } else { // Track which secrets weren't found for better logging - const fullSecretName = this.secretPrefix - ? `${this.secretPrefix}-${secretName}` - : secretName; notFoundSecrets.push(fullSecretName); + if (isOktaSecret || isEmailSecret) { + logger.warn(`[Secret Manager] ❌ Not found: ${secretName} (searched as: ${fullSecretName})`); + } } } // Merge secrets into process.env (only override if secret exists) + // Log when overriding existing env vars vs setting new ones for (const [envVar, value] of Object.entries(loadedSecrets)) { + const existingValue = process.env[envVar]; + const isOverriding = existingValue !== undefined; + process.env[envVar] = value; + + // Log override behavior for debugging + if (isOverriding) { + logger.debug(`[Secret Manager] 🔄 Overrode existing env var: ${envVar} (was: ${existingValue ? 'set' : 'undefined'}, now: from Secret Manager)`); + } else { + logger.debug(`[Secret Manager] ✨ Set new env var: ${envVar} (from Secret Manager)`); + } } logger.info(`[Secret Manager] ✅ Successfully loaded ${loadedCount}/${secretsToLoad.length} secrets`); @@ -251,6 +296,33 @@ class GoogleSecretManagerService { } } + // Log summary of not found secrets, especially OKTA and EMAIL ones + if (notFoundSecrets.length > 0) { + const notFoundOkta = notFoundSecrets.filter(name => /OKTA_/i.test(name)); + const notFoundEmail = notFoundSecrets.filter(name => /EMAIL_|SMTP_/i.test(name)); + + if (notFoundOkta.length > 0) { + logger.warn(`[Secret Manager] ⚠️ OKTA secrets not found (${notFoundOkta.length}): ${notFoundOkta.join(', ')}`); + logger.info(`[Secret Manager] 💡 To create OKTA secrets, use:`); + notFoundOkta.forEach(secretName => { + logger.info(`[Secret Manager] gcloud secrets create ${secretName} --data-file=- --project=${this.projectId}`); + }); + } + + if (notFoundEmail.length > 0) { + logger.warn(`[Secret Manager] ⚠️ EMAIL secrets not found (${notFoundEmail.length}): ${notFoundEmail.join(', ')}`); + logger.info(`[Secret Manager] 💡 To create EMAIL secrets, use:`); + notFoundEmail.forEach(secretName => { + logger.info(`[Secret Manager] gcloud secrets create ${secretName} --data-file=- --project=${this.projectId}`); + }); + } + + const otherNotFound = notFoundSecrets.filter(name => !/OKTA_|EMAIL_|SMTP_/i.test(name)); + if (otherNotFound.length > 0) { + logger.debug(`[Secret Manager] Other secrets not found (${otherNotFound.length}): ${otherNotFound.slice(0, 10).join(', ')}${otherNotFound.length > 10 ? '...' : ''}`); + } + } + this.isInitialized = true; } catch (error: any) { logger.error('[Secret Manager] Failed to load secrets:', error); @@ -265,10 +337,6 @@ class GoogleSecretManagerService { private getDefaultSecretNames(): string[] { return [ // Database - 'DB_HOST', - 'DB_PORT', - 'DB_NAME', - 'DB_USER', 'DB_PASSWORD', // JWT & Session @@ -277,7 +345,6 @@ class GoogleSecretManagerService { 'SESSION_SECRET', // Okta/SSO - 'OKTA_DOMAIN', 'OKTA_CLIENT_ID', 'OKTA_CLIENT_SECRET', 'OKTA_API_TOKEN', @@ -287,15 +354,7 @@ class GoogleSecretManagerService { 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASSWORD', - - // VAPID (Web Push) - 'VAPID_PUBLIC_KEY', - 'VAPID_PRIVATE_KEY', - - // Loki - 'LOKI_HOST', - 'LOKI_USER', - 'LOKI_PASSWORD', + 'EMAIL_FROM', ]; }