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

This commit is contained in:
Karol Sójko
2023-11-02 12:35:01 +01:00
committed by GitHub
parent f2c5810023
commit 7253a0a1d9
8 changed files with 94 additions and 1 deletions

View File

@@ -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) {

View File

@@ -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'),

View File

@@ -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()
}

View File

@@ -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>
`

View File

@@ -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,
}),
)
}
}

View File

@@ -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),
),

View File

@@ -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())

View File

@@ -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,