From 7253a0a1d92099df844c9baf6541b440bbcb0a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Thu, 2 Nov 2023 12:35:01 +0100 Subject: [PATCH] feat: add shared vault invitation email notifications (#897) --- packages/auth/src/Bootstrap/Container.ts | 11 +++++ packages/auth/src/Bootstrap/Types.ts | 1 + .../Domain/Email/UserInvitedToSharedVault.ts | 9 ++++ .../user-invited-to-shared-vault.html.ts | 21 ++++++++++ .../UserInvitedToSharedVaultEventHandler.ts | 41 +++++++++++++++++++ .../syncing-server/src/Bootstrap/Container.ts | 1 + .../InviteUserToSharedVault.spec.ts | 7 +++- .../InviteUserToSharedVault.ts | 4 ++ 8 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 packages/auth/src/Domain/Email/UserInvitedToSharedVault.ts create mode 100644 packages/auth/src/Domain/Email/user-invited-to-shared-vault.html.ts create mode 100644 packages/auth/src/Domain/Handler/UserInvitedToSharedVaultEventHandler.ts diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index cfce3d501..56afad51e 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -275,6 +275,7 @@ import { SettingPersistenceMapper } from '../Mapping/Persistence/SettingPersiste import { SubscriptionSettingPersistenceMapper } from '../Mapping/Persistence/SubscriptionSettingPersistenceMapper' import { ApplyDefaultSettings } from '../Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings' import { AuthResponseFactoryResolverInterface } from '../Domain/Auth/AuthResponseFactoryResolverInterface' +import { UserInvitedToSharedVaultEventHandler } from '../Domain/Handler/UserInvitedToSharedVaultEventHandler' export class ContainerConfigLoader { constructor(private mode: 'server' | 'worker' = 'server') {} @@ -1449,6 +1450,15 @@ export class ContainerConfigLoader { container.get(TYPES.Auth_Logger), ), ) + container + .bind(TYPES.Auth_UserInvitedToSharedVaultEventHandler) + .toConstantValue( + new UserInvitedToSharedVaultEventHandler( + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_DomainEventFactory), + container.get(TYPES.Auth_DomainEventPublisher), + ), + ) const eventHandlers: Map = new Map([ ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)], @@ -1484,6 +1494,7 @@ export class ContainerConfigLoader { 'USER_DESIGNATED_AS_SURVIVOR_IN_SHARED_VAULT', container.get(TYPES.Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler), ], + ['USER_INVITED_TO_SHARED_VAULT', container.get(TYPES.Auth_UserInvitedToSharedVaultEventHandler)], ]) if (isConfiguredForHomeServer) { diff --git a/packages/auth/src/Bootstrap/Types.ts b/packages/auth/src/Bootstrap/Types.ts index 99ba59796..5f8d32b16 100644 --- a/packages/auth/src/Bootstrap/Types.ts +++ b/packages/auth/src/Bootstrap/Types.ts @@ -195,6 +195,7 @@ const TYPES = { Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler: Symbol.for( 'Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler', ), + Auth_UserInvitedToSharedVaultEventHandler: Symbol.for('Auth_UserInvitedToSharedVaultEventHandler'), // Services Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'), Auth_SessionService: Symbol.for('Auth_SessionService'), diff --git a/packages/auth/src/Domain/Email/UserInvitedToSharedVault.ts b/packages/auth/src/Domain/Email/UserInvitedToSharedVault.ts new file mode 100644 index 000000000..64ea67913 --- /dev/null +++ b/packages/auth/src/Domain/Email/UserInvitedToSharedVault.ts @@ -0,0 +1,9 @@ +import { html } from './user-invited-to-shared-vault.html' + +export function getSubject(): string { + return "You're Invited to a Shared Vault!" +} + +export function getBody(): string { + return html() +} diff --git a/packages/auth/src/Domain/Email/user-invited-to-shared-vault.html.ts b/packages/auth/src/Domain/Email/user-invited-to-shared-vault.html.ts new file mode 100644 index 000000000..bcd1be005 --- /dev/null +++ b/packages/auth/src/Domain/Email/user-invited-to-shared-vault.html.ts @@ -0,0 +1,21 @@ +export const html = () => ` +

Hello,

+ +

You've been invited to join a shared vault. This shared workspace will help you collaborate and securely manage notes and files.

+ +

To accept this invitation and access the shared vault, please follow these steps:

+ +
    +
  1. Go to your account settings.
  2. +
  3. Navigate to the "Vaults" section.
  4. +
  5. You will find the invitation there — simply click to accept.
  6. +
+ +

If you have any questions, please contact our support team by visiting our help page +or by replying directly to this email.

+ +

Best regards,

+

+Standard Notes +

+` diff --git a/packages/auth/src/Domain/Handler/UserInvitedToSharedVaultEventHandler.ts b/packages/auth/src/Domain/Handler/UserInvitedToSharedVaultEventHandler.ts new file mode 100644 index 000000000..2e56d65fb --- /dev/null +++ b/packages/auth/src/Domain/Handler/UserInvitedToSharedVaultEventHandler.ts @@ -0,0 +1,41 @@ +import { + DomainEventHandlerInterface, + DomainEventPublisherInterface, + UserInvitedToSharedVaultEvent, +} from '@standardnotes/domain-events' +import { EmailLevel, Uuid } from '@standardnotes/domain-core' + +import { UserRepositoryInterface } from '../User/UserRepositoryInterface' +import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' +import { getBody, getSubject } from '../Email/UserInvitedToSharedVault' + +export class UserInvitedToSharedVaultEventHandler implements DomainEventHandlerInterface { + constructor( + private userRepository: UserRepositoryInterface, + private domainEventFactory: DomainEventFactoryInterface, + private domainEventPublisher: DomainEventPublisherInterface, + ) {} + + async handle(event: UserInvitedToSharedVaultEvent): Promise { + const userUuidOrError = Uuid.create(event.payload.invite.user_uuid) + if (userUuidOrError.isFailed()) { + return + } + const userUuid = userUuidOrError.getValue() + + const user = await this.userRepository.findOneByUuid(userUuid) + if (!user) { + return + } + + await this.domainEventPublisher.publish( + this.domainEventFactory.createEmailRequestedEvent({ + body: getBody(), + level: EmailLevel.LEVELS.System, + subject: getSubject(), + messageIdentifier: 'USER_INVITED_TO_SHARED_VAULT', + userEmail: user.email, + }), + ) + } +} diff --git a/packages/syncing-server/src/Bootstrap/Container.ts b/packages/syncing-server/src/Bootstrap/Container.ts index 4ba71e49c..f1fcfd9ba 100644 --- a/packages/syncing-server/src/Bootstrap/Container.ts +++ b/packages/syncing-server/src/Bootstrap/Container.ts @@ -652,6 +652,7 @@ export class ContainerConfigLoader { container.get(TYPES.Sync_SharedVaultUserRepository), container.get(TYPES.Sync_Timer), container.get(TYPES.Sync_DomainEventFactory), + container.get(TYPES.Sync_DomainEventPublisher), container.get(TYPES.Sync_SendEventToClient), container.get(TYPES.Sync_Logger), ), diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts index 6adc883f5..9b3e5ddfc 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts @@ -1,6 +1,6 @@ import { TimerInterface } from '@standardnotes/time' import { Uuid, Timestamps, Result, SharedVaultUserPermission, SharedVaultUser } from '@standardnotes/domain-core' -import { UserInvitedToSharedVaultEvent } from '@standardnotes/domain-events' +import { DomainEventPublisherInterface, UserInvitedToSharedVaultEvent } from '@standardnotes/domain-events' import { Logger } from 'winston' import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' @@ -20,6 +20,7 @@ describe('InviteUserToSharedVault', () => { let sharedVault: SharedVault let sharedVaultUser: SharedVaultUser let domainEventFactory: DomainEventFactoryInterface + let domainEventPublisher: DomainEventPublisherInterface let sendEventToClientUseCase: SendEventToClient let logger: Logger @@ -30,6 +31,7 @@ describe('InviteUserToSharedVault', () => { sharedVaultUserRepository, timer, domainEventFactory, + domainEventPublisher, sendEventToClientUseCase, logger, ) @@ -67,6 +69,9 @@ describe('InviteUserToSharedVault', () => { type: 'USER_INVITED_TO_SHARED_VAULT', } as jest.Mocked) + domainEventPublisher = {} as jest.Mocked + domainEventPublisher.publish = jest.fn() + sendEventToClientUseCase = {} as jest.Mocked sendEventToClientUseCase.execute = jest.fn().mockReturnValue(Result.ok()) diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.ts index ba30aac7b..e94784ae2 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.ts @@ -9,6 +9,7 @@ import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/Sh import { Logger } from 'winston' import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface' import { SendEventToClient } from '../../Syncing/SendEventToClient/SendEventToClient' +import { DomainEventPublisherInterface } from '@standardnotes/domain-events' export class InviteUserToSharedVault implements UseCaseInterface { constructor( @@ -17,6 +18,7 @@ export class InviteUserToSharedVault implements UseCaseInterface