Skip to content

Commit 7253a0a

Browse files
authored
feat: add shared vault invitation email notifications (#897)
1 parent f2c5810 commit 7253a0a

File tree

8 files changed

+94
-1
lines changed

8 files changed

+94
-1
lines changed

packages/auth/src/Bootstrap/Container.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ import { SettingPersistenceMapper } from '../Mapping/Persistence/SettingPersiste
275275
import { SubscriptionSettingPersistenceMapper } from '../Mapping/Persistence/SubscriptionSettingPersistenceMapper'
276276
import { ApplyDefaultSettings } from '../Domain/UseCase/ApplyDefaultSettings/ApplyDefaultSettings'
277277
import { AuthResponseFactoryResolverInterface } from '../Domain/Auth/AuthResponseFactoryResolverInterface'
278+
import { UserInvitedToSharedVaultEventHandler } from '../Domain/Handler/UserInvitedToSharedVaultEventHandler'
278279

279280
export class ContainerConfigLoader {
280281
constructor(private mode: 'server' | 'worker' = 'server') {}
@@ -1449,6 +1450,15 @@ export class ContainerConfigLoader {
14491450
container.get<winston.Logger>(TYPES.Auth_Logger),
14501451
),
14511452
)
1453+
container
1454+
.bind<UserInvitedToSharedVaultEventHandler>(TYPES.Auth_UserInvitedToSharedVaultEventHandler)
1455+
.toConstantValue(
1456+
new UserInvitedToSharedVaultEventHandler(
1457+
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
1458+
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
1459+
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
1460+
),
1461+
)
14521462

14531463
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
14541464
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)],
@@ -1484,6 +1494,7 @@ export class ContainerConfigLoader {
14841494
'USER_DESIGNATED_AS_SURVIVOR_IN_SHARED_VAULT',
14851495
container.get(TYPES.Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler),
14861496
],
1497+
['USER_INVITED_TO_SHARED_VAULT', container.get(TYPES.Auth_UserInvitedToSharedVaultEventHandler)],
14871498
])
14881499

14891500
if (isConfiguredForHomeServer) {

packages/auth/src/Bootstrap/Types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ const TYPES = {
195195
Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler: Symbol.for(
196196
'Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler',
197197
),
198+
Auth_UserInvitedToSharedVaultEventHandler: Symbol.for('Auth_UserInvitedToSharedVaultEventHandler'),
198199
// Services
199200
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
200201
Auth_SessionService: Symbol.for('Auth_SessionService'),
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { html } from './user-invited-to-shared-vault.html'
2+
3+
export function getSubject(): string {
4+
return "You're Invited to a Shared Vault!"
5+
}
6+
7+
export function getBody(): string {
8+
return html()
9+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export const html = () => `
2+
<p>Hello,</p>
3+
4+
<p>You've been invited to join a shared vault. This shared workspace will help you collaborate and securely manage notes and files.</p>
5+
6+
<p>To accept this invitation and access the shared vault, please follow these steps:</p>
7+
8+
<ol>
9+
<li>Go to your account settings.</li>
10+
<li>Navigate to the "Vaults" section.</li>
11+
<li>You will find the invitation there — simply click to accept.</li>
12+
</ol>
13+
14+
<p>If you have any questions, please contact our support team by visiting our <a href="https://standardnotes.com/help">help page</a>
15+
or by replying directly to this email.</p>
16+
17+
<p>Best regards,</p>
18+
<p>
19+
Standard Notes
20+
</p>
21+
`
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {
2+
DomainEventHandlerInterface,
3+
DomainEventPublisherInterface,
4+
UserInvitedToSharedVaultEvent,
5+
} from '@standardnotes/domain-events'
6+
import { EmailLevel, Uuid } from '@standardnotes/domain-core'
7+
8+
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
9+
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
10+
import { getBody, getSubject } from '../Email/UserInvitedToSharedVault'
11+
12+
export class UserInvitedToSharedVaultEventHandler implements DomainEventHandlerInterface {
13+
constructor(
14+
private userRepository: UserRepositoryInterface,
15+
private domainEventFactory: DomainEventFactoryInterface,
16+
private domainEventPublisher: DomainEventPublisherInterface,
17+
) {}
18+
19+
async handle(event: UserInvitedToSharedVaultEvent): Promise<void> {
20+
const userUuidOrError = Uuid.create(event.payload.invite.user_uuid)
21+
if (userUuidOrError.isFailed()) {
22+
return
23+
}
24+
const userUuid = userUuidOrError.getValue()
25+
26+
const user = await this.userRepository.findOneByUuid(userUuid)
27+
if (!user) {
28+
return
29+
}
30+
31+
await this.domainEventPublisher.publish(
32+
this.domainEventFactory.createEmailRequestedEvent({
33+
body: getBody(),
34+
level: EmailLevel.LEVELS.System,
35+
subject: getSubject(),
36+
messageIdentifier: 'USER_INVITED_TO_SHARED_VAULT',
37+
userEmail: user.email,
38+
}),
39+
)
40+
}
41+
}

packages/syncing-server/src/Bootstrap/Container.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,7 @@ export class ContainerConfigLoader {
652652
container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
653653
container.get<TimerInterface>(TYPES.Sync_Timer),
654654
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
655+
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
655656
container.get<SendEventToClient>(TYPES.Sync_SendEventToClient),
656657
container.get<Logger>(TYPES.Sync_Logger),
657658
),

packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TimerInterface } from '@standardnotes/time'
22
import { Uuid, Timestamps, Result, SharedVaultUserPermission, SharedVaultUser } from '@standardnotes/domain-core'
3-
import { UserInvitedToSharedVaultEvent } from '@standardnotes/domain-events'
3+
import { DomainEventPublisherInterface, UserInvitedToSharedVaultEvent } from '@standardnotes/domain-events'
44
import { Logger } from 'winston'
55

66
import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
@@ -20,6 +20,7 @@ describe('InviteUserToSharedVault', () => {
2020
let sharedVault: SharedVault
2121
let sharedVaultUser: SharedVaultUser
2222
let domainEventFactory: DomainEventFactoryInterface
23+
let domainEventPublisher: DomainEventPublisherInterface
2324
let sendEventToClientUseCase: SendEventToClient
2425
let logger: Logger
2526

@@ -30,6 +31,7 @@ describe('InviteUserToSharedVault', () => {
3031
sharedVaultUserRepository,
3132
timer,
3233
domainEventFactory,
34+
domainEventPublisher,
3335
sendEventToClientUseCase,
3436
logger,
3537
)
@@ -67,6 +69,9 @@ describe('InviteUserToSharedVault', () => {
6769
type: 'USER_INVITED_TO_SHARED_VAULT',
6870
} as jest.Mocked<UserInvitedToSharedVaultEvent>)
6971

72+
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
73+
domainEventPublisher.publish = jest.fn()
74+
7075
sendEventToClientUseCase = {} as jest.Mocked<SendEventToClient>
7176
sendEventToClientUseCase.execute = jest.fn().mockReturnValue(Result.ok())
7277

packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/Sh
99
import { Logger } from 'winston'
1010
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
1111
import { SendEventToClient } from '../../Syncing/SendEventToClient/SendEventToClient'
12+
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
1213

1314
export class InviteUserToSharedVault implements UseCaseInterface<SharedVaultInvite> {
1415
constructor(
@@ -17,6 +18,7 @@ export class InviteUserToSharedVault implements UseCaseInterface<SharedVaultInvi
1718
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
1819
private timer: TimerInterface,
1920
private domainEventFactory: DomainEventFactoryInterface,
21+
private domainEventPublisher: DomainEventPublisherInterface,
2022
private sendEventToClientUseCase: SendEventToClient,
2123
private logger: Logger,
2224
) {}
@@ -101,6 +103,8 @@ export class InviteUserToSharedVault implements UseCaseInterface<SharedVaultInvi
101103
},
102104
})
103105

106+
await this.domainEventPublisher.publish(event)
107+
104108
const result = await this.sendEventToClientUseCase.execute({
105109
userUuid: sharedVaultInvite.props.userUuid.value,
106110
event,

0 commit comments

Comments
 (0)