diff --git a/cloud/src/Signal.Core/CoreExtensions.cs b/cloud/src/Signal.Core/CoreExtensions.cs index e51392a009..ba2484194b 100644 --- a/cloud/src/Signal.Core/CoreExtensions.cs +++ b/cloud/src/Signal.Core/CoreExtensions.cs @@ -12,7 +12,7 @@ public static class CoreExtensions { public static IServiceCollection AddCore(this IServiceCollection services) => services - .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/cloud/src/Signal.Core/Notifications/INotificationSmtpService.cs b/cloud/src/Signal.Core/Notifications/INotificationSmtpService.cs index 7bfc578a2e..d3512a6e80 100644 --- a/cloud/src/Signal.Core/Notifications/INotificationSmtpService.cs +++ b/cloud/src/Signal.Core/Notifications/INotificationSmtpService.cs @@ -9,5 +9,6 @@ Task SendAsync( string recipientEmail, string title, string content, + string? sender = "system", CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/cloud/src/Signal.Core/Notifications/NotificationEmailService.cs b/cloud/src/Signal.Core/Notifications/NotificationEmailService.cs new file mode 100644 index 0000000000..72ae3ed9f8 --- /dev/null +++ b/cloud/src/Signal.Core/Notifications/NotificationEmailService.cs @@ -0,0 +1,40 @@ +using Azure.Communication.Email; +using Signal.Core.Secrets; +using System.Threading; +using System.Threading.Tasks; + +namespace Signal.Core.Notifications; + +internal class NotificationEmailService(ISecretsProvider secretsProvider) : INotificationSmtpService +{ + public async Task SendAsync( + string recipientEmail, + string title, + string content, + string? sender = "system", + CancellationToken cancellationToken = default) + { + var acsConnectionString = await secretsProvider.GetSecretAsync(SecretKeys.AzureCommunicationServices.ConnectionString, cancellationToken); + var acsDomain = await secretsProvider.GetSecretAsync(SecretKeys.AzureCommunicationServices.Domain, cancellationToken); + + + var emailClient = new EmailClient(acsConnectionString); + var emailSendOperation = await emailClient.SendAsync( + Azure.WaitUntil.Started, + $"{sender ?? "system"}@{acsDomain}", + recipientEmail, + title, + content, + cancellationToken: cancellationToken); + + while (true) + { + await emailSendOperation.UpdateStatusAsync(cancellationToken); + if (emailSendOperation.HasCompleted) + { + break; + } + await Task.Delay(100, cancellationToken); + } + } +} \ No newline at end of file diff --git a/cloud/src/Signal.Core/Notifications/NotificationService.cs b/cloud/src/Signal.Core/Notifications/NotificationService.cs index 49a621c141..bac42c44c9 100644 --- a/cloud/src/Signal.Core/Notifications/NotificationService.cs +++ b/cloud/src/Signal.Core/Notifications/NotificationService.cs @@ -31,7 +31,7 @@ await smtpService.SendAsync( throw new InvalidOperationException($"Email not available for user {userId}"), content.Title, content.Content?.ToString() ?? string.Empty, - cancellationToken); + cancellationToken: cancellationToken); } } catch diff --git a/cloud/src/Signal.Core/Notifications/NotificationSmtpService.cs b/cloud/src/Signal.Core/Notifications/NotificationSmtpService.cs deleted file mode 100644 index 78dd4b74c3..0000000000 --- a/cloud/src/Signal.Core/Notifications/NotificationSmtpService.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Signal.Core.Secrets; -using System.Threading; -using System.Threading.Tasks; - -namespace Signal.Core.Notifications; - -internal class NotificationSmtpService(ISecretsProvider secretsProvider) : INotificationSmtpService -{ - public async Task SendAsync( - string recipientEmail, - string title, - string content, - CancellationToken cancellationToken = default) - { - var fromDomain = await secretsProvider.GetSecretAsync(SecretKeys.SmtpNotification.FromDomain, cancellationToken); - var username = await secretsProvider.GetSecretAsync(SecretKeys.SmtpNotification.Username, cancellationToken); - var password = await secretsProvider.GetSecretAsync(SecretKeys.SmtpNotification.Password, cancellationToken); - var host = await secretsProvider.GetSecretAsync(SecretKeys.SmtpNotification.Server, cancellationToken); - const int port = 25; - - using var client = new System.Net.Mail.SmtpClient(host, port); - client.Credentials = new System.Net.NetworkCredential(username, password); - client.EnableSsl = true; - await client.SendMailAsync( - $"info@{fromDomain}", - recipientEmail, - title, - content, - cancellationToken); - } -} \ No newline at end of file diff --git a/cloud/src/Signal.Core/Secrets/SecretKeys.cs b/cloud/src/Signal.Core/Secrets/SecretKeys.cs index 7a8481ca84..0dee7c1895 100644 --- a/cloud/src/Signal.Core/Secrets/SecretKeys.cs +++ b/cloud/src/Signal.Core/Secrets/SecretKeys.cs @@ -37,6 +37,12 @@ public static class AzureSpeech public const string Region = "AzureSpeech--Region"; } + public static class AzureCommunicationServices + { + public const string ConnectionString = "AcsConnectionString"; + public const string Domain = "AcsDomain"; + } + public static class SmtpNotification { public const string Username = "SmtpNotificationUsername"; diff --git a/cloud/src/Signal.Core/Signal.Core.csproj b/cloud/src/Signal.Core/Signal.Core.csproj index eb2ef0d4ad..96e88fab7a 100644 --- a/cloud/src/Signal.Core/Signal.Core.csproj +++ b/cloud/src/Signal.Core/Signal.Core.csproj @@ -7,6 +7,7 @@ AGPL-3.0-only + diff --git a/infra/apps/cloud-primary/package.json b/infra/apps/cloud-primary/package.json index 5a04e656c7..0ea0071722 100644 --- a/infra/apps/cloud-primary/package.json +++ b/infra/apps/cloud-primary/package.json @@ -30,6 +30,6 @@ "@pulumi/command": "1.0.1", "@pulumi/docker": "4.5.6", "@pulumi/pulumi": "3.134.1", - "@pulumiverse/vercel": "1.14.2" + "@pulumiverse/vercel": "1.11.0" } } \ No newline at end of file diff --git a/infra/apps/cloud-primary/src/index.ts b/infra/apps/cloud-primary/src/index.ts index b169443de7..f3f46a01b8 100644 --- a/infra/apps/cloud-primary/src/index.ts +++ b/infra/apps/cloud-primary/src/index.ts @@ -14,8 +14,8 @@ import { createFunctionsStorage, createLogWorkspace, createAppInsights, + acsEmails, } from '@infra/pulumi/azure'; -import { createSes } from '@infra/pulumi/aws'; import { apiStatusCheck } from '@infra/pulumi/checkly'; import { dnsRecord } from '@infra/pulumi/cloudflare'; import { publishProjectAsync } from '@infra/pulumi/dotnet'; @@ -160,8 +160,22 @@ const up = async () => { // Create general storage and prepare tables const storage = createStorageAccount(resourceGroup, storagePrefix, shouldProtect); - // Create AWS SES service - const ses = createSes(`ses-${stack}`, 'notification'); + // ACS (Email) + // DomainName can be root (e.g. signalco.io) or subdomain (e.g. next.signalco.io) + // ACS Domain name should be the root domain (e.g. signalco.io) + // Subdomain is used for the ACS subdomain (e.g. next) + const acsDomainName = domainName.split('.').length > 2 ? domainName.split('.').slice(1).join('.') : domainName; + const acsSubDomain = domainName.split('.').length > 2 ? domainName.split('.')[0] : null; + const { acsPrimaryConnectionString } = await acsEmails( + 'cp', + resourceGroup, + acsDomainName, + acsSubDomain, + stack, + [ + { subdomain: 'notifications', displayName: 'Signalco Notifications' }, + { subdomain: 'system', displayName: 'Signalco' }, + ]); // Create and populate vault const vault = createKeyVault(resourceGroup, keyvaultPrefix, shouldProtect, [ @@ -188,10 +202,8 @@ const up = async () => { }; const internalEnvVariables = { - SmtpNotificationServerUrl: ses.smtpServer, - SmtpNotificationFromDomain: ses.smtpFromDomain, - SmtpNotificationUsername: ses.smtpUsername, - SmtpNotificationPassword: ses.smtpPassword, + AcsConnectionString: acsPrimaryConnectionString, + AcsDomain: acsDomainName, 'Auth0_ClientId_Station': config.requireSecret('secret-auth0ClientIdStation'), 'Auth0_ClientSecret_Station': config.requireSecret('secret-auth0ClientSecretStation'), 'HCaptcha_Secret': config.requireSecret('secret-hcaptchaSecret'), diff --git a/infra/apps/doprocess/package.json b/infra/apps/doprocess/package.json index ab7641a8d1..53124a0c2b 100644 --- a/infra/apps/doprocess/package.json +++ b/infra/apps/doprocess/package.json @@ -28,6 +28,6 @@ "@pulumi/azure-native": "2.63.0", "@pulumi/cloudflare": "5.39.1", "@pulumi/docker": "4.5.6", - "@pulumiverse/vercel": "1.14.2" + "@pulumiverse/vercel": "1.11.0" } } \ No newline at end of file diff --git a/infra/apps/remote-browser/package.json b/infra/apps/remote-browser/package.json index 6888127f27..55a4551039 100644 --- a/infra/apps/remote-browser/package.json +++ b/infra/apps/remote-browser/package.json @@ -20,12 +20,11 @@ }, "dependencies": { "@checkly/pulumi": "1.1.4", - "@pulumi/aws": "6.54.0", "@pulumi/azure-native": "2.63.0", "@pulumi/cloudflare": "5.39.1", "@pulumi/command": "1.0.1", "@pulumi/docker": "4.5.6", "@pulumi/pulumi": "3.134.1", - "@pulumiverse/vercel": "1.14.2" + "@pulumiverse/vercel": "1.11.0" } } \ No newline at end of file diff --git a/infra/apps/uier/package.json b/infra/apps/uier/package.json index 40e36ddf1e..79d465562e 100644 --- a/infra/apps/uier/package.json +++ b/infra/apps/uier/package.json @@ -24,12 +24,11 @@ }, "dependencies": { "@checkly/pulumi": "1.1.4", - "@pulumi/aws": "6.54.0", "@pulumi/azure-native": "2.63.0", "@pulumi/cloudflare": "5.39.1", "@pulumi/command": "1.0.1", "@pulumi/docker": "4.5.6", "@pulumi/pulumi": "3.134.1", - "@pulumiverse/vercel": "1.14.2" + "@pulumiverse/vercel": "1.11.0" } } \ No newline at end of file diff --git a/infra/apps/workingparty/package.json b/infra/apps/workingparty/package.json index 92105cdbc8..f4f9c11219 100644 --- a/infra/apps/workingparty/package.json +++ b/infra/apps/workingparty/package.json @@ -27,6 +27,6 @@ "@pulumi/azure-native": "2.63.0", "@pulumi/cloudflare": "5.39.1", "@pulumi/docker": "4.5.6", - "@pulumiverse/vercel": "1.14.2" + "@pulumiverse/vercel": "1.11.0" } } \ No newline at end of file diff --git a/infra/apps/workingparty/src/index.ts b/infra/apps/workingparty/src/index.ts index be907992c3..7670ba4d60 100644 --- a/infra/apps/workingparty/src/index.ts +++ b/infra/apps/workingparty/src/index.ts @@ -5,8 +5,7 @@ import { DatabaseAccount, SqlResourceSqlDatabase, SqlResourceSqlContainer, Datab import { nextJsApp } from '@infra/pulumi/vercel'; import { dnsRecord } from '@infra/pulumi/cloudflare'; import { ProjectDomain, ProjectEnvironmentVariable } from '@pulumiverse/vercel'; -import { CommunicationService, EmailService, Domain, DomainManagement, UserEngagementTracking, SenderUsername, listCommunicationServiceKeysOutput } from '@pulumi/azure-native/communication/index.js'; -import { createStorageAccount } from '@infra/pulumi/azure'; +import { createStorageAccount, acsEmails } from '@infra/pulumi/azure'; const up = async () => { const config = new Config(); @@ -114,70 +113,17 @@ const up = async () => { queueName: 'email-queue', }); - // Create ACS - const aes = new EmailService('wp-azure-email-service', { - dataLocation: 'Europe', - emailServiceName: 'wpemail', - location: 'Global', - resourceGroupName: resourceGroup.name, - }); - const aesDomain = new Domain('wp-aes-domain', { - resourceGroupName: resourceGroup.name, - emailServiceName: aes.name, - domainManagement: DomainManagement.CustomerManaged, - domainName: domainName, - location: 'Global', - userEngagementTracking: UserEngagementTracking.Disabled, - }); - if (aesDomain.verificationRecords.domain) { - const aesDomainVerifyDataName = aesDomain.verificationRecords.domain.apply(domainVerification => domainVerification?.name ?? ''); - const aesDomainVerifyDataValue = aesDomain.verificationRecords.domain.apply(domainVerification => domainVerification?.value ?? ''); - dnsRecord('wp-aes-domain-domainverify', aesDomainVerifyDataName, aesDomainVerifyDataValue, 'TXT', false); - } - if (aesDomain.verificationRecords.sPF) { - const aesDomainVerifySpfName = aesDomain.verificationRecords.sPF.apply(dkimVerification => dkimVerification?.name ?? ''); - const aesDomainVerifySpfValue = aesDomain.verificationRecords.sPF.apply(dkimVerification => dkimVerification?.value ?? ''); - dnsRecord('wp-aes-domain-spf', aesDomainVerifySpfName, aesDomainVerifySpfValue, 'TXT', false); - } - if (aesDomain.verificationRecords.dKIM) { - const aesDomainVerifyDkimName = aesDomain.verificationRecords.dKIM.apply(dkimVerification => subdomain ? (`${dkimVerification?.name ?? ''}.${subdomain}`) : dkimVerification?.name ?? ''); - const aesDomainVerifyDkimValue = aesDomain.verificationRecords.dKIM.apply(dkimVerification => dkimVerification?.value ?? ''); - dnsRecord('wp-aes-domain-dkim', aesDomainVerifyDkimName, aesDomainVerifyDkimValue, 'CNAME', false); - } - if (aesDomain.verificationRecords.dKIM2) { - const aesDomainVerifyDkimName = aesDomain.verificationRecords.dKIM2.apply(dkimVerification => subdomain ? (`${dkimVerification?.name ?? ''}.${subdomain}`) : dkimVerification?.name ?? ''); - const aesDomainVerifyDkimValue = aesDomain.verificationRecords.dKIM2.apply(dkimVerification => dkimVerification?.value ?? ''); - dnsRecord('wp-aes-domain-dkim2', aesDomainVerifyDkimName, aesDomainVerifyDkimValue, 'CNAME', false); - } - // NOTE: Domain needs to be verified manually in Azure Communication Services - - new SenderUsername('wp-aes-sender-notifications', { - resourceGroupName: resourceGroup.name, - emailServiceName: aes.name, - domainName: aesDomain.name, - displayName: 'WorkingParty Notifications', - senderUsername: 'notifications', - username: 'notifications', - }); - new SenderUsername('wp-aes-sender-system', { - resourceGroupName: resourceGroup.name, - emailServiceName: aes.name, - domainName: aesDomain.name, - displayName: 'WorkingParty', - senderUsername: 'system', - username: 'system', - }); - const communicaionService = new CommunicationService('wp-azure-communication-service', { - communicationServiceName: `wpacs-${stack}`, - dataLocation: 'Europe', - location: 'Global', - resourceGroupName: resourceGroup.name, - linkedDomains: [aesDomain.id], - }); - const acsPrimaryConnectionString = listCommunicationServiceKeysOutput({ - resourceGroupName: resourceGroup.name, - communicationServiceName: communicaionService.name, - }).apply(keys => keys.primaryConnectionString ?? ''); + // ACS (Email) + const { acsPrimaryConnectionString } = await acsEmails( + 'wp', + resourceGroup, + domainName, + subdomain, + stack, + [ + { subdomain: 'notifications', displayName: 'WorkingParty Notifications' }, + { subdomain: 'system', displayName: 'WorkingParty' }, + ]); // Vercel setup const app = nextJsApp('wp', 'workingparty', 'web/apps/workingparty'); diff --git a/infra/packages/pulumi/package.json b/infra/packages/pulumi/package.json index 2441f58e8d..9809e6e384 100644 --- a/infra/packages/pulumi/package.json +++ b/infra/packages/pulumi/package.json @@ -10,9 +10,6 @@ "./azure": { "import": "./src/azure/index.ts" }, - "./aws": { - "import": "./src/aws/index.ts" - }, "./cloudflare": { "import": "./src/cloudflare/index.ts" }, @@ -40,12 +37,11 @@ }, "dependencies": { "@checkly/pulumi": "1.1.4", - "@pulumi/aws": "6.54.0", "@pulumi/azure-native": "2.63.0", "@pulumi/cloudflare": "5.39.1", "@pulumi/command": "1.0.1", "@pulumi/docker": "4.5.6", "@pulumi/pulumi": "3.134.1", - "@pulumiverse/vercel": "1.14.2" + "@pulumiverse/vercel": "1.11.0" } } \ No newline at end of file diff --git a/infra/packages/pulumi/src/aws/createSes.ts b/infra/packages/pulumi/src/aws/createSes.ts deleted file mode 100644 index ba09e31e7e..0000000000 --- a/infra/packages/pulumi/src/aws/createSes.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Config, interpolate } from '@pulumi/pulumi'; -import { User, AccessKey, UserPolicy } from '@pulumi/aws/iam/index.js'; -import { DomainIdentity, MailFrom, DomainDkim } from '@pulumi/aws/ses/index.js'; -import { dnsRecord } from '../cloudflare/dnsRecord.js'; - -export function createSes(prefix: string, subdomain: string) { - const config = new Config(); - const baseDomain = config.require('domain'); - const sesRegion = config.require('ses-region'); - - const emailUser = new User( - `${prefix}-usr`, - { - name: `${prefix}-email`, - path: '/system/', - }, - ); - - // Policy - const allowedFromAddress = `*@${subdomain}.${baseDomain}`; - new UserPolicy( - `${prefix}-ses-policy`, - { - user: emailUser.name, - policy: JSON.stringify({ - Version: '2012-10-17', - Statement: [ - { - Action: [ - 'ses:SendEmail', - 'ses:SendTemplatedEmail', - 'ses:SendRawEmail', - 'ses:SendBulkTemplatedEmail', - ], - Effect: 'Allow', - Resource: '*', - Condition: { - StringLike: { - 'ses:FromAddress': allowedFromAddress, - }, - }, - }, - ], - }, null, ' '), - }, - ); - - // Email Access key - const emailAccessKey = new AccessKey( - `${prefix}-ses-access-key`, - { user: emailUser.name }, - ); - - const sesSmtpUsername = interpolate`${emailAccessKey.id}`; - const sesSmtpPassword = interpolate`${emailAccessKey.sesSmtpPasswordV4}`; - - const sesDomainIdentity = new DomainIdentity(`${prefix}-domainIdentity`, { - domain: `${subdomain}.${baseDomain}`, - }); - - // MailFrom - const mailFromDomain = sesDomainIdentity.domain; - const mailFrom = new MailFrom( - `${prefix}-ses-mail-from`, - { - domain: sesDomainIdentity.domain, - mailFromDomain: interpolate`bounce.${sesDomainIdentity.domain}`, - }); - - dnsRecord(`${prefix}-ses-mail-from-mx-record`, `bounce.${subdomain}`, `feedback-smtp.${sesRegion}.amazonses.com`, 'MX', false); - dnsRecord(`${prefix}-spf`, mailFrom.mailFromDomain, 'v=spf1 include:amazonses.com -all', 'TXT', false); - dnsRecord(`${prefix}-ses-dmarc`, `_dmarc.${subdomain}`, 'v=DMARC1; p=none; rua=mailto:contact@signalco.io; fo=1;', 'TXT', false); - - const sesDomainDkim = new DomainDkim(`${prefix}-sesDomainDkim`, { - domain: sesDomainIdentity.domain, - }); - for (let i = 0; i < 3; i++) { - const dkimValue = interpolate`${sesDomainDkim.dkimTokens[i]}.dkim.amazonses.com`; - const dkimName = interpolate`${sesDomainDkim.dkimTokens[i]}._domainkey.${subdomain}`; - dnsRecord(`${prefix}-dkim${i}`, dkimName, dkimValue, 'CNAME', false); - } - - return { - smtpUsername: sesSmtpUsername, - smtpPassword: sesSmtpPassword, - smtpServer: `email-smtp.${sesRegion}.amazonaws.com`, - smtpFromDomain: mailFromDomain, - }; -} diff --git a/infra/packages/pulumi/src/aws/index.ts b/infra/packages/pulumi/src/aws/index.ts deleted file mode 100644 index 1e4bbf290b..0000000000 --- a/infra/packages/pulumi/src/aws/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './createSes.js'; \ No newline at end of file diff --git a/infra/packages/pulumi/src/azure/acsEmails.ts b/infra/packages/pulumi/src/azure/acsEmails.ts new file mode 100644 index 0000000000..9fd0288d69 --- /dev/null +++ b/infra/packages/pulumi/src/azure/acsEmails.ts @@ -0,0 +1,70 @@ +import { dnsRecord } from '@infra/pulumi/cloudflare'; +import { EmailService, Domain, DomainManagement, UserEngagementTracking, SenderUsername, CommunicationService, listCommunicationServiceKeysOutput } from '@pulumi/azure-native/communication/index.js'; +import { ResourceGroup } from '@pulumi/azure-native/resources/index.js'; + +export async function acsEmails(prefix: string, resourceGroup: ResourceGroup, domainName: string, subdomain: string | null | undefined, stack: string, emails: { subdomain: string; displayName: string; }[]) { + // Create ACS + const aes = new EmailService(`${prefix}-azure-email-service`, { + dataLocation: 'Europe', + emailServiceName: `${prefix}email`, + location: 'Global', + resourceGroupName: resourceGroup.name, + }); + const aesDomain = new Domain(`${prefix}-aes-domain`, { + resourceGroupName: resourceGroup.name, + emailServiceName: aes.name, + domainManagement: DomainManagement.CustomerManaged, + domainName: domainName, + location: 'Global', + userEngagementTracking: UserEngagementTracking.Disabled, + }); + if (aesDomain.verificationRecords.domain) { + const aesDomainVerifyDataName = aesDomain.verificationRecords.domain.apply(domainVerification => domainVerification?.name ?? ''); + const aesDomainVerifyDataValue = aesDomain.verificationRecords.domain.apply(domainVerification => domainVerification?.value ?? ''); + dnsRecord(`${prefix}-aes-domain-domainverify`, aesDomainVerifyDataName, aesDomainVerifyDataValue, 'TXT', false); + } + if (aesDomain.verificationRecords.sPF) { + const aesDomainVerifySpfName = aesDomain.verificationRecords.sPF.apply(dkimVerification => dkimVerification?.name ?? ''); + const aesDomainVerifySpfValue = aesDomain.verificationRecords.sPF.apply(dkimVerification => dkimVerification?.value ?? ''); + dnsRecord(`${prefix}-aes-domain-spf`, aesDomainVerifySpfName, aesDomainVerifySpfValue, 'TXT', false); + } + if (aesDomain.verificationRecords.dKIM) { + const aesDomainVerifyDkimName = aesDomain.verificationRecords.dKIM.apply(dkimVerification => subdomain ? (`${dkimVerification?.name ?? ''}.${subdomain}`) : dkimVerification?.name ?? ''); + const aesDomainVerifyDkimValue = aesDomain.verificationRecords.dKIM.apply(dkimVerification => dkimVerification?.value ?? ''); + dnsRecord(`${prefix}-aes-domain-dkim`, aesDomainVerifyDkimName, aesDomainVerifyDkimValue, 'CNAME', false); + } + if (aesDomain.verificationRecords.dKIM2) { + const aesDomainVerifyDkimName = aesDomain.verificationRecords.dKIM2.apply(dkimVerification => subdomain ? (`${dkimVerification?.name ?? ''}.${subdomain}`) : dkimVerification?.name ?? ''); + const aesDomainVerifyDkimValue = aesDomain.verificationRecords.dKIM2.apply(dkimVerification => dkimVerification?.value ?? ''); + dnsRecord(`${prefix}-aes-domain-dkim2`, aesDomainVerifyDkimName, aesDomainVerifyDkimValue, 'CNAME', false); + } + // NOTE: Domain needs to be verified manually in Azure Communication Services + + // Create senders + emails.forEach(({ subdomain: emailSubdomain, displayName }) => { + new SenderUsername(`${prefix}-aes-sender-${emailSubdomain}`, { + resourceGroupName: resourceGroup.name, + emailServiceName: aes.name, + domainName: aesDomain.name, + displayName: displayName, + senderUsername: emailSubdomain, + username: emailSubdomain, + }); + }); + + const communicaionService = new CommunicationService(`${prefix}-azure-communication-service`, { + communicationServiceName: `${prefix}acs-${stack}`, + dataLocation: 'Europe', + location: 'Global', + resourceGroupName: resourceGroup.name, + linkedDomains: [aesDomain.id], + }); + const acsPrimaryConnectionString = listCommunicationServiceKeysOutput({ + resourceGroupName: resourceGroup.name, + communicationServiceName: communicaionService.name, + }).apply(keys => keys.primaryConnectionString ?? ''); + + return { + acsPrimaryConnectionString, + }; +} diff --git a/infra/packages/pulumi/src/azure/index.ts b/infra/packages/pulumi/src/azure/index.ts index 2e5e039a43..04048b9c03 100644 --- a/infra/packages/pulumi/src/azure/index.ts +++ b/infra/packages/pulumi/src/azure/index.ts @@ -18,3 +18,4 @@ export * from './getConnectionString.js'; export * from './signedBlobReadUrl.js'; export * from './vaultSecret.js'; export * from './webAppIdentity.js'; +export * from './acsEmails.js'; \ No newline at end of file diff --git a/infra/packages/pulumi/src/cloudflare/dnsRecord.ts b/infra/packages/pulumi/src/cloudflare/dnsRecord.ts index 12c7892cab..8f41715fc0 100644 --- a/infra/packages/pulumi/src/cloudflare/dnsRecord.ts +++ b/infra/packages/pulumi/src/cloudflare/dnsRecord.ts @@ -8,7 +8,7 @@ export function dnsRecord(name: string, dnsName: Input, value: Input