-
+
${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',
];
}
| | | | | | | | | | | |