diff --git a/.github/workflows/infra-deploy_reusable.yml b/.github/workflows/infra-deploy_reusable.yml index b98d0fcacf..98de79c73e 100644 --- a/.github/workflows/infra-deploy_reusable.yml +++ b/.github/workflows/infra-deploy_reusable.yml @@ -92,7 +92,7 @@ jobs: run: pnpm build --filter=${{ inputs.packageName }} - name: ⚡ Preview Deploy Infrastructure - uses: pulumi/actions@v5 + uses: pulumi/actions@v6 with: command: ${{ inputs.command }} stack-name: signalco/${{ inputs.project }}/${{ steps.extract_branch.outputs.stack }} 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/cloud/src/Signalco.Api.RemoteBrowser/Dockerfile b/cloud/src/Signalco.Api.RemoteBrowser/Dockerfile index 0471d2dea3..37f952ffcd 100644 --- a/cloud/src/Signalco.Api.RemoteBrowser/Dockerfile +++ b/cloud/src/Signalco.Api.RemoteBrowser/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 @@ -6,7 +6,7 @@ EXPOSE 443 RUN apt-get update && \ apt install -y libglib2.0-0 libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 libpango-1.0-0 libpangocairo-1.0-0 -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src COPY . . diff --git a/cloud/src/Signalco.Api.RemoteBrowser/Signalco.Api.RemoteBrowser.csproj b/cloud/src/Signalco.Api.RemoteBrowser/Signalco.Api.RemoteBrowser.csproj index a9bb2dcb25..c5820b11ee 100644 --- a/cloud/src/Signalco.Api.RemoteBrowser/Signalco.Api.RemoteBrowser.csproj +++ b/cloud/src/Signalco.Api.RemoteBrowser/Signalco.Api.RemoteBrowser.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable 75b1b62c-8ca6-40ee-a213-e98c0340b5e6 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/remote-browser/package.json b/infra/apps/remote-browser/package.json index 7b1e58de04..55a4551039 100644 --- a/infra/apps/remote-browser/package.json +++ b/infra/apps/remote-browser/package.json @@ -20,7 +20,6 @@ }, "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", diff --git a/infra/apps/uier/package.json b/infra/apps/uier/package.json index ab4cf82c60..79d465562e 100644 --- a/infra/apps/uier/package.json +++ b/infra/apps/uier/package.json @@ -24,7 +24,6 @@ }, "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", 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 8c3470898e..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,7 +37,6 @@ }, "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", 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