From 1e05e1f6ec0a2056072ffa56259334fdc08bf3af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 00:14:20 +0000 Subject: [PATCH 1/6] chore(deps): update dotnet monorepo to v9 --- cloud/src/Signalco.Api.RemoteBrowser/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 . . From 448f727e44acf1a5a301804ef190a5017d84efed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 04:46:43 +0000 Subject: [PATCH 2/6] fix(deps): update dependency @pulumiverse/vercel to v1.14.2 --- infra/apps/cloud-primary/package.json | 2 +- infra/apps/doprocess/package.json | 2 +- infra/apps/remote-browser/package.json | 2 +- infra/apps/uier/package.json | 2 +- infra/apps/workingparty/package.json | 2 +- infra/packages/pulumi/package.json | 2 +- infra/pnpm-lock.yaml | 30 +++++++++++++------------- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/infra/apps/cloud-primary/package.json b/infra/apps/cloud-primary/package.json index 0ea0071722..5a04e656c7 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.11.0" + "@pulumiverse/vercel": "1.14.2" } } \ No newline at end of file diff --git a/infra/apps/doprocess/package.json b/infra/apps/doprocess/package.json index 53124a0c2b..ab7641a8d1 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.11.0" + "@pulumiverse/vercel": "1.14.2" } } \ No newline at end of file diff --git a/infra/apps/remote-browser/package.json b/infra/apps/remote-browser/package.json index 7b1e58de04..6888127f27 100644 --- a/infra/apps/remote-browser/package.json +++ b/infra/apps/remote-browser/package.json @@ -26,6 +26,6 @@ "@pulumi/command": "1.0.1", "@pulumi/docker": "4.5.6", "@pulumi/pulumi": "3.134.1", - "@pulumiverse/vercel": "1.11.0" + "@pulumiverse/vercel": "1.14.2" } } \ No newline at end of file diff --git a/infra/apps/uier/package.json b/infra/apps/uier/package.json index ab4cf82c60..40e36ddf1e 100644 --- a/infra/apps/uier/package.json +++ b/infra/apps/uier/package.json @@ -30,6 +30,6 @@ "@pulumi/command": "1.0.1", "@pulumi/docker": "4.5.6", "@pulumi/pulumi": "3.134.1", - "@pulumiverse/vercel": "1.11.0" + "@pulumiverse/vercel": "1.14.2" } } \ No newline at end of file diff --git a/infra/apps/workingparty/package.json b/infra/apps/workingparty/package.json index f4f9c11219..92105cdbc8 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.11.0" + "@pulumiverse/vercel": "1.14.2" } } \ No newline at end of file diff --git a/infra/packages/pulumi/package.json b/infra/packages/pulumi/package.json index 8c3470898e..2441f58e8d 100644 --- a/infra/packages/pulumi/package.json +++ b/infra/packages/pulumi/package.json @@ -46,6 +46,6 @@ "@pulumi/command": "1.0.1", "@pulumi/docker": "4.5.6", "@pulumi/pulumi": "3.134.1", - "@pulumiverse/vercel": "1.11.0" + "@pulumiverse/vercel": "1.14.2" } } \ No newline at end of file diff --git a/infra/pnpm-lock.yaml b/infra/pnpm-lock.yaml index 1f990b758a..826e45a247 100644 --- a/infra/pnpm-lock.yaml +++ b/infra/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: 3.134.1 version: 3.134.1(ts-node@7.0.1)(typescript@5.6.2) '@pulumiverse/vercel': - specifier: 1.11.0 - version: 1.11.0(ts-node@7.0.1)(typescript@5.6.2) + specifier: 1.14.2 + version: 1.14.2(ts-node@7.0.1)(typescript@5.6.2) devDependencies: '@infra/pulumi': specifier: workspace:* @@ -91,8 +91,8 @@ importers: specifier: 3.134.1 version: 3.134.1(ts-node@7.0.1)(typescript@5.6.2) '@pulumiverse/vercel': - specifier: 1.11.0 - version: 1.11.0(ts-node@7.0.1)(typescript@5.6.2) + specifier: 1.14.2 + version: 1.14.2(ts-node@7.0.1)(typescript@5.6.2) devDependencies: '@infra/pulumi': specifier: workspace:* @@ -149,8 +149,8 @@ importers: specifier: 3.134.1 version: 3.134.1(ts-node@7.0.1)(typescript@5.6.2) '@pulumiverse/vercel': - specifier: 1.11.0 - version: 1.11.0(ts-node@7.0.1)(typescript@5.6.2) + specifier: 1.14.2 + version: 1.14.2(ts-node@7.0.1)(typescript@5.6.2) devDependencies: '@infra/pulumi': specifier: workspace:* @@ -207,8 +207,8 @@ importers: specifier: 3.134.1 version: 3.134.1(ts-node@7.0.1)(typescript@5.6.2) '@pulumiverse/vercel': - specifier: 1.11.0 - version: 1.11.0(ts-node@7.0.1)(typescript@5.6.2) + specifier: 1.14.2 + version: 1.14.2(ts-node@7.0.1)(typescript@5.6.2) devDependencies: '@infra/pulumi': specifier: workspace:* @@ -256,8 +256,8 @@ importers: specifier: 3.134.1 version: 3.134.1(ts-node@7.0.1)(typescript@5.6.2) '@pulumiverse/vercel': - specifier: 1.11.0 - version: 1.11.0(ts-node@7.0.1)(typescript@5.6.2) + specifier: 1.14.2 + version: 1.14.2(ts-node@7.0.1)(typescript@5.6.2) devDependencies: '@infra/pulumi': specifier: workspace:* @@ -341,8 +341,8 @@ importers: specifier: 3.134.1 version: 3.134.1(ts-node@7.0.1)(typescript@5.6.2) '@pulumiverse/vercel': - specifier: 1.11.0 - version: 1.11.0(ts-node@7.0.1)(typescript@5.6.2) + specifier: 1.14.2 + version: 1.14.2(ts-node@7.0.1)(typescript@5.6.2) devDependencies: '@infra/eslint-config': specifier: workspace:* @@ -805,8 +805,8 @@ packages: '@pulumi/query@0.3.0': resolution: {integrity: sha512-xfo+yLRM2zVjVEA4p23IjQWzyWl1ZhWOGobsBqRpIarzLvwNH/RAGaoehdxlhx4X92302DrpdIFgTICMN4P38w==} - '@pulumiverse/vercel@1.11.0': - resolution: {integrity: sha512-d08fjLd9iXhj3y/EsAF3/8PX9tGfGHgtx8b3BsTnGQmqRtmjdkZSnhXKmbJJmfLqoef2WOlZx05Xt1+zon040g==} + '@pulumiverse/vercel@1.14.2': + resolution: {integrity: sha512-NI6X7Kqw+VVhmwF4C0McBsfci3WxiV3wzi4r/ONKTJcBC4+GulNzIfExZOAlR8aeShN2742+U3aofpAsM18yUA==} '@rollup/rollup-android-arm-eabi@4.19.0': resolution: {integrity: sha512-JlPfZ/C7yn5S5p0yKk7uhHTTnFlvTgLetl2VxqE518QgyM7C9bSfFTYvB/Q/ftkq0RIPY4ySxTz+/wKJ/dXC0w==} @@ -3426,7 +3426,7 @@ snapshots: '@pulumi/query@0.3.0': {} - '@pulumiverse/vercel@1.11.0(ts-node@7.0.1)(typescript@5.6.2)': + '@pulumiverse/vercel@1.14.2(ts-node@7.0.1)(typescript@5.6.2)': dependencies: '@pulumi/pulumi': 3.134.1(ts-node@7.0.1)(typescript@5.6.2) transitivePeerDependencies: From 9ba99a103fa1cb8f55525598fdc1a97cafcdd7f4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Sep 2024 08:56:21 +0000 Subject: [PATCH 3/6] chore(deps): update pulumi/actions action to v6 --- .github/workflows/infra-deploy_reusable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }} From 0dbb94684cc978023d911258bce7cedc08574b95 Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Sun, 29 Sep 2024 11:29:30 +0200 Subject: [PATCH 4/6] fix(station): Update script for stable download broken --- station/Signal.Beacon.WorkerService/rpi-update.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/station/Signal.Beacon.WorkerService/rpi-update.sh b/station/Signal.Beacon.WorkerService/rpi-update.sh index a964e4e3eb..51f5c844a3 100644 --- a/station/Signal.Beacon.WorkerService/rpi-update.sh +++ b/station/Signal.Beacon.WorkerService/rpi-update.sh @@ -31,7 +31,7 @@ then URL=$( echo $RELEASE_JSON | jq -r 'map(select(.prerelease)) | first | .assets[] | select(.name | test("station-v(.*)-linux-arm64.tar.gz")) | .browser_download_url' ) else echo "Determining stable version..." - RELEASE_JSON=$( curl -s $"https://api.github.com/repos/signalco-io/station/signalco/latest" ) + RELEASE_JSON=$( curl -s $"https://api.github.com/repos/signalco-io/signalco/releases/latest" ) VER=$( echo $RELEASE_JSON | jq -r '.tag_name' ) URL=$( echo $RELEASE_JSON | jq -r '.assets[] | select(.name | test("station-v(.*)-linux-arm64.tar.gz")) | .browser_download_url' ) fi From 9c988e0fb0b08747f861873e2069983df67ca336 Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Sun, 29 Sep 2024 12:13:35 +0200 Subject: [PATCH 5/6] feat(remote-browser): Updated .NET target to 9.0 --- .../Signalco.Api.RemoteBrowser.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 05d2caa1512f29815fc13aea14eaf04af5d27596 Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Sun, 29 Sep 2024 13:56:57 +0200 Subject: [PATCH 6/6] feat(cloud-primary): Replaced AWS SES with Azure Communication Services Closes #3458 --- cloud/src/Signal.Core/CoreExtensions.cs | 2 +- .../Notifications/INotificationSmtpService.cs | 1 + .../Notifications/NotificationEmailService.cs | 40 +++++++++ .../Notifications/NotificationService.cs | 2 +- .../Notifications/NotificationSmtpService.cs | 31 ------- cloud/src/Signal.Core/Secrets/SecretKeys.cs | 6 ++ cloud/src/Signal.Core/Signal.Core.csproj | 1 + infra/apps/cloud-primary/package.json | 2 +- infra/apps/cloud-primary/src/index.ts | 26 ++++-- infra/apps/doprocess/package.json | 2 +- infra/apps/remote-browser/package.json | 3 +- infra/apps/uier/package.json | 3 +- infra/apps/workingparty/package.json | 2 +- infra/apps/workingparty/src/index.ts | 78 +++------------- infra/packages/pulumi/package.json | 6 +- infra/packages/pulumi/src/aws/createSes.ts | 89 ------------------- infra/packages/pulumi/src/aws/index.ts | 1 - infra/packages/pulumi/src/azure/acsEmails.ts | 70 +++++++++++++++ infra/packages/pulumi/src/azure/index.ts | 1 + .../pulumi/src/cloudflare/dnsRecord.ts | 2 +- infra/pnpm-lock.yaml | 39 ++++---- 21 files changed, 174 insertions(+), 233 deletions(-) create mode 100644 cloud/src/Signal.Core/Notifications/NotificationEmailService.cs delete mode 100644 cloud/src/Signal.Core/Notifications/NotificationSmtpService.cs delete mode 100644 infra/packages/pulumi/src/aws/createSes.ts delete mode 100644 infra/packages/pulumi/src/aws/index.ts create mode 100644 infra/packages/pulumi/src/azure/acsEmails.ts 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