secret credentials mpped and template preview modified

This commit is contained in:
laxmanhalaki 2025-12-26 15:10:56 +05:30
parent f69814ce98
commit 4cf7288857
15 changed files with 142 additions and 56 deletions

View File

@ -1,16 +1,20 @@
import { SSOConfig, SSOUserData } from '../types/auth.types'; 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 = { const ssoConfig: SSOConfig = {
jwtSecret: process.env.JWT_SECRET || '', get jwtSecret() { return process.env.JWT_SECRET || ''; },
jwtExpiry: process.env.JWT_EXPIRY || '24h', get jwtExpiry() { return process.env.JWT_EXPIRY || '24h'; },
refreshTokenExpiry: process.env.REFRESH_TOKEN_EXPIRY || '7d', get refreshTokenExpiry() { return process.env.REFRESH_TOKEN_EXPIRY || '7d'; },
sessionSecret: process.env.SESSION_SECRET || '', get sessionSecret() { return process.env.SESSION_SECRET || ''; },
// Use only FRONTEND_URL from environment - no fallbacks // 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 // Okta/Auth0 configuration for token exchange
oktaDomain: process.env.OKTA_DOMAIN || 'https://dev-830839.oktapreview.com', get oktaDomain() { return process.env.OKTA_DOMAIN || 'https://dev-830839.oktapreview.com'; },
oktaClientId: process.env.OKTA_CLIENT_ID || '', get oktaClientId() { return process.env.OKTA_CLIENT_ID || ''; },
oktaClientSecret: process.env.OKTA_CLIENT_SECRET || '', get oktaClientSecret() { return process.env.OKTA_CLIENT_SECRET || ''; },
}; };
export { ssoConfig }; export { ssoConfig };

View File

@ -3,7 +3,7 @@
*/ */
import { ApprovalConfirmationData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getApprovalConfirmationEmail(data: ApprovalConfirmationData): string { export function getApprovalConfirmationEmail(data: ApprovalConfirmationData): string {
@ -31,7 +31,7 @@ export function getApprovalConfirmationEmail(data: ApprovalConfirmationData): st
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0"> <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr> <tr>
<td style="padding: 40px 0;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: 'Request Approved', title: 'Request Approved',
...HeaderStyles.success ...HeaderStyles.success

View File

@ -3,7 +3,7 @@
*/ */
import { ApprovalRequestData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getApprovalRequestEmail(data: ApprovalRequestData): string { export function getApprovalRequestEmail(data: ApprovalRequestData): string {
@ -22,7 +22,7 @@ export function getApprovalRequestEmail(data: ApprovalRequestData): string {
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0"> <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr> <tr>
<td style="padding: 40px 0;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
<!-- Header --> <!-- Header -->
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: 'Approval Request', title: 'Approval Request',

View File

@ -3,7 +3,7 @@
*/ */
import { ApproverSkippedData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getApproverSkippedEmail(data: ApproverSkippedData): string { export function getApproverSkippedEmail(data: ApproverSkippedData): string {
@ -22,7 +22,7 @@ export function getApproverSkippedEmail(data: ApproverSkippedData): string {
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0"> <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr> <tr>
<td style="padding: 40px 0;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: 'Approval Level Skipped', title: 'Approval Level Skipped',
...HeaderStyles.infoSecondary ...HeaderStyles.infoSecondary

View File

@ -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) * Generate all email styles (responsive + rich text)
* Desktop-first design (optimized for browser) with mobile responsive breakpoints * Desktop-first design (optimized for browser) with mobile responsive breakpoints
@ -175,8 +184,22 @@ export function getResponsiveStyles(): string {
/* Desktop-first base styles */ /* Desktop-first base styles */
.email-container { .email-container {
width: 95%; width: 95% !important;
max-width: 1200px; 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 { .email-content {

View File

@ -3,7 +3,7 @@
*/ */
import { MultiApproverRequestData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getMultiApproverRequestEmail(data: MultiApproverRequestData): string { export function getMultiApproverRequestEmail(data: MultiApproverRequestData): string {
@ -22,7 +22,7 @@ export function getMultiApproverRequestEmail(data: MultiApproverRequestData): st
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0"> <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr> <tr>
<td style="padding: 40px 0;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
<!-- Header --> <!-- Header -->
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: 'Multi-Level Approval Request', title: 'Multi-Level Approval Request',

View File

@ -3,7 +3,7 @@
*/ */
import { ParticipantAddedData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getParticipantAddedEmail(data: ParticipantAddedData): string { export function getParticipantAddedEmail(data: ParticipantAddedData): string {
@ -22,7 +22,7 @@ export function getParticipantAddedEmail(data: ParticipantAddedData): string {
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0"> <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr> <tr>
<td style="padding: 40px 0;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: `You've Been Added as ${data.participantRole}`, title: `You've Been Added as ${data.participantRole}`,
...HeaderStyles.info ...HeaderStyles.info

View File

@ -3,7 +3,7 @@
*/ */
import { RejectionNotificationData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getRejectionNotificationEmail(data: RejectionNotificationData): string { export function getRejectionNotificationEmail(data: RejectionNotificationData): string {
@ -22,7 +22,7 @@ export function getRejectionNotificationEmail(data: RejectionNotificationData):
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0"> <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr> <tr>
<td style="padding: 40px 0;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: 'Request Rejected', title: 'Request Rejected',
...HeaderStyles.error ...HeaderStyles.error

View File

@ -3,7 +3,7 @@
*/ */
import { RequestClosedData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getRequestClosedEmail(data: RequestClosedData): string { export function getRequestClosedEmail(data: RequestClosedData): string {
@ -22,7 +22,7 @@ export function getRequestClosedEmail(data: RequestClosedData): string {
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0"> <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr> <tr>
<td style="padding: 40px 0;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: 'Request Closed', title: 'Request Closed',
...HeaderStyles.complete ...HeaderStyles.complete

View File

@ -3,7 +3,7 @@
*/ */
import { RequestCreatedData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getRequestCreatedEmail(data: RequestCreatedData): string { export function getRequestCreatedEmail(data: RequestCreatedData): string {
@ -22,7 +22,7 @@ export function getRequestCreatedEmail(data: RequestCreatedData): string {
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0"> <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr> <tr>
<td style="padding: 40px 0;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
<!-- Header --> <!-- Header -->
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: 'Request Created Successfully', title: 'Request Created Successfully',

View File

@ -3,7 +3,7 @@
*/ */
import { TATBreachedData } from './types'; import { TATBreachedData } from './types';
import { getEmailFooter, getEmailHeader, HeaderStyles, getResponsiveStyles } from './helpers'; import { getEmailFooter, getEmailHeader, HeaderStyles, getResponsiveStyles, getEmailContainerStyles } from './helpers';
import { getBrandedHeader } from './branding.config'; import { getBrandedHeader } from './branding.config';
export function getTATBreachedEmail(data: TATBreachedData): string { export function getTATBreachedEmail(data: TATBreachedData): string {
@ -22,7 +22,7 @@ export function getTATBreachedEmail(data: TATBreachedData): string {
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0"> <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr> <tr>
<td style="padding: 40px 0;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: 'TAT Breached', title: 'TAT Breached',
subtitle: 'Immediate Action Required', subtitle: 'Immediate Action Required',

View File

@ -3,7 +3,7 @@
*/ */
import { TATReminderData } from './types'; import { TATReminderData } from './types';
import { getEmailFooter, getEmailHeader, HeaderStyles, getResponsiveStyles } from './helpers'; import { getEmailFooter, getEmailHeader, HeaderStyles, getResponsiveStyles, getEmailContainerStyles } from './helpers';
import { getBrandedHeader } from './branding.config'; import { getBrandedHeader } from './branding.config';
/** /**
@ -52,7 +52,7 @@ export function getTATReminderEmail(data: TATReminderData): string {
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0"> <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr> <tr>
<td style="padding: 40px 0;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: 'TAT Reminder', title: 'TAT Reminder',
subtitle: `${data.thresholdPercentage}% Elapsed - ${urgencyStyle.title}`, subtitle: `${data.thresholdPercentage}% Elapsed - ${urgencyStyle.title}`,

View File

@ -3,7 +3,7 @@
*/ */
import { WorkflowPausedData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getWorkflowPausedEmail(data: WorkflowPausedData): string { export function getWorkflowPausedEmail(data: WorkflowPausedData): string {
@ -22,7 +22,7 @@ export function getWorkflowPausedEmail(data: WorkflowPausedData): string {
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0"> <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr> <tr>
<td style="padding: 40px 0;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: 'Workflow Paused', title: 'Workflow Paused',
...HeaderStyles.neutral ...HeaderStyles.neutral

View File

@ -3,7 +3,7 @@
*/ */
import { WorkflowResumedData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getWorkflowResumedEmail(data: WorkflowResumedData): string { export function getWorkflowResumedEmail(data: WorkflowResumedData): string {
@ -22,7 +22,7 @@ export function getWorkflowResumedEmail(data: WorkflowResumedData): string {
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0"> <table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
<tr> <tr>
<td style="padding: 40px 0;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: 'Workflow Resumed', title: 'Workflow Resumed',
...HeaderStyles.success ...HeaderStyles.success

View File

@ -139,15 +139,24 @@ class GoogleSecretManagerService {
logger.debug(`[Secret Manager] Secret ${fullSecretName} exists but payload is empty`); logger.debug(`[Secret Manager] Secret ${fullSecretName} exists but payload is empty`);
return null; return null;
} catch (error: any) { } 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) // Handle "not found" errors (code 5 = NOT_FOUND)
if (error.code === 5 || error.code === 'NOT_FOUND' || error.message?.includes('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; return null;
} }
// Handle permission errors (code 7 = PERMISSION_DENIED) // Handle permission errors (code 7 = PERMISSION_DENIED)
if (error.code === 7 || error.code === 'PERMISSION_DENIED' || error.message?.includes('Permission denied')) { if (error.code === 7 || error.code === 'PERMISSION_DENIED' || error.message?.includes('Permission denied')) {
logger.warn(`[Secret Manager] ❌ Permission denied for secret '${fullSecretName}'`); 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] Service account needs 'Secret Manager Secret Accessor' role`);
logger.warn(`[Secret Manager] To grant access, run:`); logger.warn(`[Secret Manager] To grant access, run:`);
logger.warn(`[Secret Manager] gcloud secrets add-iam-policy-binding ${fullSecretName} \\`); logger.warn(`[Secret Manager] gcloud secrets add-iam-policy-binding ${fullSecretName} \\`);
@ -157,11 +166,12 @@ class GoogleSecretManagerService {
return null; return null;
} }
// Log full error details for debugging // Log full error details for debugging (info level for OKTA secrets)
logger.warn(`[Secret Manager] Failed to fetch secret '${fullSecretName}' (project: ${this.projectId})`); const errorLogLevel = isOktaSecret ? logger.warn.bind(logger) : logger.warn.bind(logger);
logger.warn(`[Secret Manager] Error code: ${error.code || 'unknown'}, Message: ${error.message || 'no message'}`); 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) { if (error.details) {
logger.warn(`[Secret Manager] Error details: ${JSON.stringify(error.details)}`); errorLogLevel(`[Secret Manager] Error details: ${JSON.stringify(error.details)}`);
} }
return null; return null;
@ -216,26 +226,61 @@ class GoogleSecretManagerService {
const notFoundSecrets: string[] = []; const notFoundSecrets: string[] = [];
let loadedCount = 0; 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 // Load each secret
for (const secretName of secretsToLoad) { 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); const secretValue = await this.getSecret(secretName);
if (secretValue !== null) { if (secretValue !== null) {
const envVarName = this.getEnvVarName(secretName); const envVarName = this.getEnvVarName(secretName);
loadedSecrets[envVarName] = secretValue; loadedSecrets[envVarName] = secretValue;
loadedCount++; loadedCount++;
if (isOktaSecret || isEmailSecret) {
logger.info(`[Secret Manager] ✅ Successfully loaded: ${secretName} -> ${envVarName}`);
}
} else { } else {
// Track which secrets weren't found for better logging // Track which secrets weren't found for better logging
const fullSecretName = this.secretPrefix
? `${this.secretPrefix}-${secretName}`
: secretName;
notFoundSecrets.push(fullSecretName); 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) // 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)) { for (const [envVar, value] of Object.entries(loadedSecrets)) {
const existingValue = process.env[envVar];
const isOverriding = existingValue !== undefined;
process.env[envVar] = value; 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`); 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; this.isInitialized = true;
} catch (error: any) { } catch (error: any) {
logger.error('[Secret Manager] Failed to load secrets:', error); logger.error('[Secret Manager] Failed to load secrets:', error);
@ -265,10 +337,6 @@ class GoogleSecretManagerService {
private getDefaultSecretNames(): string[] { private getDefaultSecretNames(): string[] {
return [ return [
// Database // Database
'DB_HOST',
'DB_PORT',
'DB_NAME',
'DB_USER',
'DB_PASSWORD', 'DB_PASSWORD',
// JWT & Session // JWT & Session
@ -277,7 +345,6 @@ class GoogleSecretManagerService {
'SESSION_SECRET', 'SESSION_SECRET',
// Okta/SSO // Okta/SSO
'OKTA_DOMAIN',
'OKTA_CLIENT_ID', 'OKTA_CLIENT_ID',
'OKTA_CLIENT_SECRET', 'OKTA_CLIENT_SECRET',
'OKTA_API_TOKEN', 'OKTA_API_TOKEN',
@ -287,15 +354,7 @@ class GoogleSecretManagerService {
'SMTP_PORT', 'SMTP_PORT',
'SMTP_USER', 'SMTP_USER',
'SMTP_PASSWORD', 'SMTP_PASSWORD',
'EMAIL_FROM',
// VAPID (Web Push)
'VAPID_PUBLIC_KEY',
'VAPID_PRIVATE_KEY',
// Loki
'LOKI_HOST',
'LOKI_USER',
'LOKI_PASSWORD',
]; ];
} }