diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4c3b5a09..80cd9aca 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -31,6 +31,7 @@ jobs: write-mode: overwrite contents: | { + "stackLabel": "dev", "amazonConnect": { "instanceArn": "placeholder", "securityKeyId": "placeholder", diff --git a/bin/c3-amazon-connect.ts b/bin/c3-amazon-connect.ts index 836aa8e6..589f26ba 100644 --- a/bin/c3-amazon-connect.ts +++ b/bin/c3-amazon-connect.ts @@ -7,15 +7,18 @@ import { exec } from 'child_process'; (async () => { const stackVersion = await getMostRecentGitTag(); - console.log(`Deploying stack version ${stackVersion}...`); - const app = new App(); - new C3AmazonConnectStack(app, 'C3AmazonConnectStack', { + const stackLabel = app.node.tryGetContext('stackLabel') as string; + console.log(`Deploying stack version ${stackVersion} for "${stackLabel}"...`); + + const formattedStackLabel = getFormattedStackLabel(stackLabel); + new C3AmazonConnectStack(app, `C3AmazonConnect${formattedStackLabel}Stack`, { description: `Stack containing the resources for C3 for Amazon Connect (${stackVersion}).`, }); })(); /** + * Fetches the most recent git tag in the repository. * * @returns {Promise} The most recent git tag in the repository */ @@ -32,3 +35,14 @@ async function getMostRecentGitTag(): Promise { return 'v?.?.?'; } } + +/** + * Gets the stack label from the context, formatted as a title. + * + * @param stackLabel The stack label from the context. + * @returns A formatted stack label. + */ +function getFormattedStackLabel(stackLabel: string): string { + const lowerCaseValue = stackLabel.toLowerCase(); + return lowerCaseValue.charAt(0).toUpperCase() + lowerCaseValue.slice(1); +} diff --git a/cdk.context.json b/cdk.context.json index 88bf9d3e..b69d94b2 100644 --- a/cdk.context.json +++ b/cdk.context.json @@ -1,4 +1,5 @@ { + "stackLabel": "", "amazonConnect": { "instanceArn": "", "securityKeyId": "", diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index c98a536c..a4989a33 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -41,6 +41,10 @@ In order to facilitate this process, you will need to provide some values to the #### Configuration Values +| Value | Description | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `stackLabel` | **Optional**. A unique label to give to the deployed stack. To enable multiple stacks to be deployed to a single AWS account, this field must be populated with a unique name. | + ##### Amazon Connect | Value | Description | diff --git a/lib/c3-amazon-connect-stack.ts b/lib/c3-amazon-connect-stack.ts index 395d4f7d..dde99192 100644 --- a/lib/c3-amazon-connect-stack.ts +++ b/lib/c3-amazon-connect-stack.ts @@ -30,6 +30,7 @@ export class C3AmazonConnectStack extends Stack { private c3AppUrlFragment: string; // Context variables. + private stackLabel: string; private amazonConnectContext: AmazonConnectContext; private c3Context: C3Context; private featuresContext: FeaturesContext; @@ -129,6 +130,8 @@ export class C3AmazonConnectStack extends Stack { * Ensures that all required context variables are set. Throws an error if any are missing. */ private validateContextVariables(): void { + this.stackLabel = this.node.tryGetContext('stackLabel'); + this.amazonConnectContext = this.node.tryGetContext('amazonConnect'); validateAmazonConnectContext(this.amazonConnectContext); @@ -202,10 +205,13 @@ export class C3AmazonConnectStack extends Stack { */ private createC3ApiKeySecret(): void { console.log('Creating secret for C3 API key...'); + const secretLabel = this.stackLabel + ? `_${this.stackLabel.toUpperCase()}` + : ''; this.c3ApiKeySecret = new Secret(this, 'C3APIKey', { - secretName: 'C3_API_KEY', + secretName: 'C3_API_KEY' + secretLabel, secretStringValue: SecretValue.unsafePlainText(''), - description: 'The API key used for C3 payments.', + description: 'The API key used for C3 Payment.', }); } @@ -217,8 +223,11 @@ export class C3AmazonConnectStack extends Stack { */ private createPrivateKeySecret(): void { console.log('Creating private key secret...'); + const secretLabel = this.stackLabel + ? `_${this.stackLabel.toUpperCase()}` + : ''; this.privateKeySecret = new Secret(this, 'C3PrivateKey', { - secretName: 'C3_PRIVATE_KEY', + secretName: 'C3_PRIVATE_KEY' + secretLabel, secretStringValue: SecretValue.unsafePlainText( '', ), @@ -243,8 +252,9 @@ export class C3AmazonConnectStack extends Stack { join(__dirname, 'lambda/c3-create-payment-request'), ), environment: { - C3_VENDOR_ID: this.c3Context.vendorId, C3_BASE_URL: this.c3BaseUrl, + C3_VENDOR_ID: this.c3Context.vendorId, + C3_API_KEY_SECRET_ID: this.c3ApiKeySecret.secretName, LOGO_URL: this.logoUrl, SUPPORT_PHONE: this.supportPhone, SUPPORT_EMAIL: this.supportEmail, @@ -277,6 +287,7 @@ export class C3AmazonConnectStack extends Stack { code: Code.fromAsset(join(__dirname, 'lambda/c3-tokenize-transaction')), environment: { C3_ENV: this.c3Context.env, + C3_PRIVATE_KEY_SECRET_ID: this.privateKeySecret.secretName, C3_PAYMENT_GATEWAY: this.c3Context.paymentGateway, CONNECT_SECURITY_KEY_ID: this.amazonConnectContext.securityKeyId, }, @@ -299,7 +310,12 @@ export class C3AmazonConnectStack extends Stack { // Create additional payment gateway secrets and add to policy. switch (this.c3Context.paymentGateway) { case C3PaymentGateway.Zift: - new Zift(this, getSecretValuePolicy); + new Zift( + this, + this.tokenizeTransactionFunction, + getSecretValuePolicy, + this.stackLabel, + ); break; default: throw new Error( @@ -322,6 +338,7 @@ export class C3AmazonConnectStack extends Stack { code: Code.fromAsset(join(__dirname, 'lambda/c3-submit-payment')), environment: { C3_BASE_URL: this.c3BaseUrl, + C3_API_KEY_SECRET_ID: this.c3ApiKeySecret.secretName, }, codeSigningConfig: this.codeSigningConfig, }); @@ -347,6 +364,7 @@ export class C3AmazonConnectStack extends Stack { code: Code.fromAsset(join(__dirname, 'lambda/c3-email-receipt')), environment: { C3_BASE_URL: this.c3BaseUrl, + C3_API_KEY_SECRET_ID: this.c3ApiKeySecret.secretName, }, codeSigningConfig: this.codeSigningConfig, }); @@ -390,18 +408,25 @@ export class C3AmazonConnectStack extends Stack { } // Create the app. - const application = new CfnApplication(this, 'C3ConnectApp', { - name: 'Payment Request', - namespace: 'c3-payment', - description: 'Agent application for collecting payments with C3.', - permissions: ['User.Details.View', 'Contact.Details.View'], - applicationSourceConfig: { - externalUrlConfig: { - accessUrl: `https://${this.c3Context.vendorId}.${this.c3AppUrlFragment}/agent-workspace?contactCenter=amazon&instanceId=${instanceId}®ion=${region}${agentAssistedIVRParams}${configuredFeatureParams}`, - approvedOrigins: [], // Don't allow any other origins. + const stackLabelTitleCase = + this.stackLabel.charAt(0).toUpperCase() + this.stackLabel.slice(1); + const appLabel = this.stackLabel ? ` - ${stackLabelTitleCase}` : ''; + const application = new CfnApplication( + this, + `C3ConnectApp${stackLabelTitleCase}`, + { + name: 'Payment Request' + appLabel, // App name is unfortunately required to be unique to create. + namespace: `c3-payment-${this.stackLabel}`, + description: 'Agent application for collecting payments with C3.', + permissions: ['User.Details.View', 'Contact.Details.View'], + applicationSourceConfig: { + externalUrlConfig: { + accessUrl: `https://${this.c3Context.vendorId}.${this.c3AppUrlFragment}/agent-workspace?contactCenter=amazon&instanceId=${instanceId}®ion=${region}${agentAssistedIVRParams}${configuredFeatureParams}`, + approvedOrigins: [], // Don't allow any other origins. + }, }, }, - }); + ); // Associate the app with the Amazon Connect instance. new CfnIntegrationAssociation(this, `C3ConnectIntegrationApp`, { diff --git a/lib/features/agent-assisted-payment-ivr.ts b/lib/features/agent-assisted-payment-ivr.ts index d82ee536..ecdc221e 100644 --- a/lib/features/agent-assisted-payment-ivr.ts +++ b/lib/features/agent-assisted-payment-ivr.ts @@ -85,6 +85,7 @@ export class AgentAssistedPaymentIVR { environment: { C3_ENV: c3Context.env, C3_BASE_URL: this.c3BaseUrl, + C3_API_KEY_SECRET_ID: this.c3ApiKeySecret.secretName, }, codeSigningConfig: this.codeSigningConfig, }, diff --git a/lib/lambda/c3-create-payment-request/index.js b/lib/lambda/c3-create-payment-request/index.js index 9f3d63f3..a31dbd88 100644 --- a/lib/lambda/c3-create-payment-request/index.js +++ b/lib/lambda/c3-create-payment-request/index.js @@ -19,15 +19,19 @@ export async function handler(event) { if (!c3ApiKey) { const secretsManagerClient = new SecretsManagerClient(); const getSecretValueCommand = new GetSecretValueCommand({ - SecretId: 'C3_API_KEY', + SecretId: process.env.C3_API_KEY_SECRET_ID, }); c3ApiKey = (await secretsManagerClient.send(getSecretValueCommand)) .SecretString; if (!c3ApiKey) { - throw new Error('No value found for C3_API_KEY secret.'); + throw new Error( + `No value found for ${process.env.C3_API_KEY_SECRET_ID} secret.`, + ); } if (c3ApiKey === '') { - throw new Error('Value for C3_API_KEY secret is not set.'); + throw new Error( + `Value for ${process.env.C3_API_KEY_SECRET_ID} secret is not set.`, + ); } } else { console.log('Using API key in memory.'); diff --git a/lib/lambda/c3-email-receipt/index.js b/lib/lambda/c3-email-receipt/index.js index 96faddbf..e812b2a4 100644 --- a/lib/lambda/c3-email-receipt/index.js +++ b/lib/lambda/c3-email-receipt/index.js @@ -19,21 +19,25 @@ export async function handler(event) { if (!c3ApiKey) { const secretsManagerClient = new SecretsManagerClient(); const getSecretValueCommand = new GetSecretValueCommand({ - SecretId: 'C3_API_KEY', + SecretId: process.env.C3_API_KEY_SECRET_ID, }); c3ApiKey = (await secretsManagerClient.send(getSecretValueCommand)) .SecretString; if (!c3ApiKey) { - throw new Error('No value found for C3_API_KEY secret.'); + throw new Error( + `No value found for ${process.env.C3_API_KEY_SECRET_ID} secret.`, + ); } if (c3ApiKey === '') { - throw new Error('Value for C3_API_KEY secret is not set.'); + throw new Error( + `Value for ${process.env.C3_API_KEY_SECRET_ID} secret is not set.`, + ); } } else { console.log('Using API key in memory.'); } - // Email the receipt + // Email the receipt. const transactionId = contactAttributes.TransactionId; const emailAddress = contactAttributes.Email; await emailReceipt(transactionId, emailAddress); diff --git a/lib/lambda/c3-send-agent-message/index.js b/lib/lambda/c3-send-agent-message/index.js index 54ce28c7..6eb29512 100644 --- a/lib/lambda/c3-send-agent-message/index.js +++ b/lib/lambda/c3-send-agent-message/index.js @@ -25,15 +25,19 @@ export async function handler(event) { if (!c3ApiKey) { const secretsManagerClient = new SecretsManagerClient(); const getSecretValueCommand = new GetSecretValueCommand({ - SecretId: 'C3_API_KEY', + SecretId: process.env.C3_API_KEY_SECRET_ID, }); c3ApiKey = (await secretsManagerClient.send(getSecretValueCommand)) .SecretString; if (!c3ApiKey) { - throw new Error('No value found for C3_API_KEY secret.'); + throw new Error( + `No value found for ${process.env.C3_API_KEY_SECRET_ID} secret.`, + ); } if (c3ApiKey === '') { - throw new Error('Value for C3_API_KEY secret is not set.'); + throw new Error( + `Value for ${process.env.C3_API_KEY_SECRET_ID} secret is not set.`, + ); } } else { console.log('Using API key in memory.'); diff --git a/lib/lambda/c3-submit-payment/index.js b/lib/lambda/c3-submit-payment/index.js index f068b26f..3a4b0ab2 100644 --- a/lib/lambda/c3-submit-payment/index.js +++ b/lib/lambda/c3-submit-payment/index.js @@ -31,15 +31,19 @@ export async function handler(event) { if (!c3ApiKey) { const secretsManagerClient = new SecretsManagerClient(); const getSecretValueCommand = new GetSecretValueCommand({ - SecretId: 'C3_API_KEY', + SecretId: process.env.C3_API_KEY_SECRET_ID, }); c3ApiKey = (await secretsManagerClient.send(getSecretValueCommand)) .SecretString; if (!c3ApiKey) { - throw new Error('No value found for C3_API_KEY secret.'); + throw new Error( + `No value found for ${process.env.C3_API_KEY_SECRET_ID} secret.`, + ); } if (c3ApiKey === '') { - throw new Error('Value for C3_API_KEY secret is not set.'); + throw new Error( + `Value for ${process.env.C3_API_KEY_SECRET_ID} secret is not set.`, + ); } } else { console.log('Using API key in memory.'); diff --git a/lib/lambda/c3-tokenize-transaction/gateways/zift.js b/lib/lambda/c3-tokenize-transaction/gateways/zift.js index cf49d7a1..2db35b30 100644 --- a/lib/lambda/c3-tokenize-transaction/gateways/zift.js +++ b/lib/lambda/c3-tokenize-transaction/gateways/zift.js @@ -96,34 +96,36 @@ export class Zift { } const secretsManagerClient = new SecretsManagerClient(); const getSecretValueCommand = new GetSecretValueCommand({ - SecretId: 'ZIFT_CREDENTIALS', + SecretId: process.env.ZIFT_CREDENTIALS_SECRET_ID, }); ziftCredentials = JSON.parse( (await secretsManagerClient.send(getSecretValueCommand)).SecretString, ); if (!ziftCredentials) { - throw new Error('No value found for ZIFT_CREDENTIALS secret.'); + throw new Error( + `No value found for ${process.env.ZIFT_CREDENTIALS_SECRET_ID} secret.`, + ); } if ( !ziftCredentials.accountId || ziftCredentials.accountId === '' ) { throw new Error( - 'Value for accountId in ZIFT_CREDENTIALS secret is not set.', + `Value for accountId in ${process.env.ZIFT_CREDENTIALS_SECRET_ID} secret is not set.`, ); } else if ( !ziftCredentials.username || ziftCredentials.username === '' ) { throw new Error( - 'Value for username in ZIFT_CREDENTIALS secret is not set.', + `Value for username in ${process.env.ZIFT_CREDENTIALS_SECRET_ID} secret is not set.`, ); } else if ( !ziftCredentials.password || ziftCredentials.password === '' ) { throw new Error( - 'Value for password in ZIFT_CREDENTIALS secret is not set.', + `Value for password in ${process.env.ZIFT_CREDENTIALS_SECRET_ID} secret is not set.`, ); } } diff --git a/lib/lambda/c3-tokenize-transaction/index.js b/lib/lambda/c3-tokenize-transaction/index.js index 4b29573d..6a9debd8 100644 --- a/lib/lambda/c3-tokenize-transaction/index.js +++ b/lib/lambda/c3-tokenize-transaction/index.js @@ -66,15 +66,19 @@ export async function handler(event) { if (!c3PrivateKey) { const secretsManagerClient = new SecretsManagerClient(); const getSecretValueCommand = new GetSecretValueCommand({ - SecretId: 'C3_PRIVATE_KEY', + SecretId: process.env.C3_PRIVATE_KEY_SECRET_ID, }); c3PrivateKey = (await secretsManagerClient.send(getSecretValueCommand)) .SecretString; if (!c3PrivateKey) { - throw new Error('No value found for C3_PRIVATE_KEY secret.'); + throw new Error( + `No value found for ${process.env.C3_PRIVATE_KEY_SECRET_ID} secret.`, + ); } if (c3PrivateKey === '') { - throw new Error('Value for C3_PRIVATE_KEY secret is not set.'); + throw new Error( + `Value for ${process.env.C3_PRIVATE_KEY_SECRET_ID} secret is not set.`, + ); } } else { console.log('Using private key in memory.'); diff --git a/lib/models/context.ts b/lib/models/context.ts index a402f8a5..9eaaa7f5 100644 --- a/lib/models/context.ts +++ b/lib/models/context.ts @@ -3,6 +3,7 @@ import { C3Context } from './c3-context'; import { FeaturesContext } from './features-context'; export interface Context { + stackLabel: string; amazonConnect: AmazonConnectContext; c3: C3Context; logoUrl: string; diff --git a/lib/payment-gateways/zift.ts b/lib/payment-gateways/zift.ts index 803419d1..825ecb0c 100644 --- a/lib/payment-gateways/zift.ts +++ b/lib/payment-gateways/zift.ts @@ -1,5 +1,6 @@ import { SecretValue, Stack } from 'aws-cdk-lib'; import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { Function } from 'aws-cdk-lib/aws-lambda'; import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; /** @@ -13,11 +14,14 @@ export class Zift { */ constructor( private stack: Stack, + private tokenizeTransactionFunction: Function, private tokenizeTransactionPolicy: PolicyStatement, + private stackLabel: string, ) { console.log('Creating resources for Zift...'); this.createZiftCredentialsSecret(); this.addSecretsToTokenizeTransactionPolicy(); + this.addSecretIdToTokenizeTransactionFunctionEnvironment(); } /** @@ -27,8 +31,11 @@ export class Zift { */ private createZiftCredentialsSecret(): void { console.log('Creating Zift credentials secret...'); + const secretLabel = this.stackLabel + ? `_${this.stackLabel.toUpperCase()}` + : ''; this.ziftCredentialsSecret = new Secret(this.stack, 'C3ZiftCredentials', { - secretName: 'ZIFT_CREDENTIALS', + secretName: 'ZIFT_CREDENTIALS' + secretLabel, secretObjectValue: { accountId: SecretValue.unsafePlainText(''), username: SecretValue.unsafePlainText(''), @@ -49,4 +56,17 @@ export class Zift { this.ziftCredentialsSecret.secretArn, ); } + + /** + * Updates the tokenize transaction function environment to include the secret ID for the Zift credentials. + * + * This is necessary for the tokenize transaction function to access the Zift credentials. The secret ID for these + * credentials change based on your stack label. + */ + private addSecretIdToTokenizeTransactionFunctionEnvironment(): void { + this.tokenizeTransactionFunction.addEnvironment( + 'ZIFT_CREDENTIALS_SECRET_ID', + this.ziftCredentialsSecret.secretName, + ); + } } diff --git a/test/c3-amazon-connect.test.ts b/test/c3-amazon-connect.test.ts index 9aea7a08..82624cba 100644 --- a/test/c3-amazon-connect.test.ts +++ b/test/c3-amazon-connect.test.ts @@ -9,6 +9,7 @@ import { } from '../lib/models'; const mockContext: Context = { + stackLabel: 'dev', amazonConnect: { instanceArn: 'placeholder', securityKeyId: 'placeholder', diff --git a/test/features/agent-assisted-ivr.test.ts b/test/features/agent-assisted-ivr.test.ts index 857fa695..a3498ff9 100644 --- a/test/features/agent-assisted-ivr.test.ts +++ b/test/features/agent-assisted-ivr.test.ts @@ -9,6 +9,7 @@ import { } from '../../lib/models'; const mockContext: Context = { + stackLabel: 'dev', amazonConnect: { instanceArn: 'placeholder', securityKeyId: 'placeholder', diff --git a/test/features/agent-assisted-link.test.ts b/test/features/agent-assisted-link.test.ts index 502d23c6..98c6361c 100644 --- a/test/features/agent-assisted-link.test.ts +++ b/test/features/agent-assisted-link.test.ts @@ -9,6 +9,7 @@ import { } from '../../lib/models'; const mockContext: Context = { + stackLabel: 'dev', amazonConnect: { instanceArn: 'placeholder', securityKeyId: 'placeholder', diff --git a/test/features/self-service-ivr.test.ts b/test/features/self-service-ivr.test.ts index 5f349c7a..dfb3decb 100644 --- a/test/features/self-service-ivr.test.ts +++ b/test/features/self-service-ivr.test.ts @@ -9,6 +9,7 @@ import { } from '../../lib/models'; const mockContext: Context = { + stackLabel: 'dev', amazonConnect: { instanceArn: 'placeholder', securityKeyId: 'placeholder', diff --git a/test/features/subject-lookup.test.ts b/test/features/subject-lookup.test.ts index 503590b6..10332d25 100644 --- a/test/features/subject-lookup.test.ts +++ b/test/features/subject-lookup.test.ts @@ -9,6 +9,7 @@ import { } from '../../lib/models'; const mockContext: Context = { + stackLabel: 'dev', amazonConnect: { instanceArn: 'placeholder', securityKeyId: 'placeholder',