Ver Fonte

feat: add shared vault invitation email notifications (#897)

Karol Sójko há 1 ano atrás
pai
commit
7253a0a1d9

+ 11 - 0
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<winston.Logger>(TYPES.Auth_Logger),
         ),
       )
+    container
+      .bind<UserInvitedToSharedVaultEventHandler>(TYPES.Auth_UserInvitedToSharedVaultEventHandler)
+      .toConstantValue(
+        new UserInvitedToSharedVaultEventHandler(
+          container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
+          container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
+          container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
+        ),
+      )
 
     const eventHandlers: Map<string, DomainEventHandlerInterface> = 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) {

+ 1 - 0
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'),

+ 9 - 0
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()
+}

+ 21 - 0
packages/auth/src/Domain/Email/user-invited-to-shared-vault.html.ts

@@ -0,0 +1,21 @@
+export const html = () => `
+<p>Hello,</p>
+
+<p>You've been invited to join a shared vault. This shared workspace will help you collaborate and securely manage notes and files.</p>
+
+<p>To accept this invitation and access the shared vault, please follow these steps:</p>
+
+<ol>
+    <li>Go to your account settings.</li>
+    <li>Navigate to the "Vaults" section.</li>
+    <li>You will find the invitation there — simply click to accept.</li>
+</ol>
+
+<p>If you have any questions, please contact our support team by visiting our <a href="https://standardnotes.com/help">help page</a>
+or by replying directly to this email.</p>
+
+<p>Best regards,</p>
+<p>
+Standard Notes
+</p>
+`

+ 41 - 0
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<void> {
+    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,
+      }),
+    )
+  }
+}

+ 1 - 0
packages/syncing-server/src/Bootstrap/Container.ts

@@ -652,6 +652,7 @@ export class ContainerConfigLoader {
           container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
           container.get<TimerInterface>(TYPES.Sync_Timer),
           container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
+          container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
           container.get<SendEventToClient>(TYPES.Sync_SendEventToClient),
           container.get<Logger>(TYPES.Sync_Logger),
         ),

+ 6 - 1
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<UserInvitedToSharedVaultEvent>)
 
+    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
+    domainEventPublisher.publish = jest.fn()
+
     sendEventToClientUseCase = {} as jest.Mocked<SendEventToClient>
     sendEventToClientUseCase.execute = jest.fn().mockReturnValue(Result.ok())
 

+ 4 - 0
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<SharedVaultInvite> {
   constructor(
@@ -17,6 +18,7 @@ export class InviteUserToSharedVault implements UseCaseInterface<SharedVaultInvi
     private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
     private timer: TimerInterface,
     private domainEventFactory: DomainEventFactoryInterface,
+    private domainEventPublisher: DomainEventPublisherInterface,
     private sendEventToClientUseCase: SendEventToClient,
     private logger: Logger,
   ) {}
@@ -101,6 +103,8 @@ export class InviteUserToSharedVault implements UseCaseInterface<SharedVaultInvi
       },
     })
 
+    await this.domainEventPublisher.publish(event)
+
     const result = await this.sendEventToClientUseCase.execute({
       userUuid: sharedVaultInvite.props.userUuid.value,
       event,