Compare commits

..

35 Commits

Author SHA1 Message Date
standardci
7e74261f62 chore(release): publish new version
- @standardnotes/analytics@2.12.17
 - @standardnotes/api-gateway@1.39.21
 - @standardnotes/auth-server@1.66.3
 - @standardnotes/domain-events-infra@1.9.51
 - @standardnotes/domain-events@2.102.0
 - @standardnotes/event-store@1.6.48
 - @standardnotes/files-server@1.8.47
 - @standardnotes/revisions-server@1.9.20
 - @standardnotes/scheduler-server@1.15.1
 - @standardnotes/syncing-server@1.24.0
 - @standardnotes/websockets-server@1.4.48
 - @standardnotes/workspace-server@1.17.47
2022-12-09 13:11:30 +00:00
Karol Sójko
32601f34f1 feat(syncing-server): replace email backup attachment created with email requested 2022-12-09 14:09:30 +01:00
standardci
aef69a1a96 chore(release): publish new version
- @standardnotes/analytics@2.12.16
 - @standardnotes/api-gateway@1.39.20
 - @standardnotes/auth-server@1.66.2
 - @standardnotes/domain-events-infra@1.9.50
 - @standardnotes/domain-events@2.101.0
 - @standardnotes/event-store@1.6.47
 - @standardnotes/files-server@1.8.46
 - @standardnotes/revisions-server@1.9.19
 - @standardnotes/scheduler-server@1.15.0
 - @standardnotes/syncing-server@1.23.0
 - @standardnotes/websockets-server@1.4.47
 - @standardnotes/workspace-server@1.17.46
2022-12-09 10:22:39 +00:00
Karol Sójko
130f90bdb6 feat(syncing-server): replace one drive backup failed event with email requested 2022-12-09 11:20:34 +01:00
standardci
851c7de87f chore(release): publish new version
- @standardnotes/syncing-server@1.22.0
2022-12-09 09:35:39 +00:00
Karol Sójko
118156c62d feat(syncing-serfver): remove dropbox backup failed event in favour of email requested 2022-12-09 10:33:40 +01:00
standardci
cdad3143c9 chore(release): publish new version
- @standardnotes/analytics@2.12.15
 - @standardnotes/api-gateway@1.39.19
 - @standardnotes/auth-server@1.66.1
 - @standardnotes/domain-events-infra@1.9.49
 - @standardnotes/domain-events@2.100.0
 - @standardnotes/event-store@1.6.46
 - @standardnotes/files-server@1.8.45
 - @standardnotes/revisions-server@1.9.18
 - @standardnotes/scheduler-server@1.14.10
 - @standardnotes/syncing-server@1.21.0
 - @standardnotes/websockets-server@1.4.46
 - @standardnotes/workspace-server@1.17.45
2022-12-09 09:28:27 +00:00
Karol Sójko
00fe32136e feat(syncing-server): remove google drive backup failed event in favour of email requested 2022-12-09 10:26:13 +01:00
standardci
52f56eeb68 chore(release): publish new version
- @standardnotes/auth-server@1.66.0
2022-12-09 09:17:28 +00:00
Karol Sójko
b595264e31 feat(email): replace offline subscription token created event in favour of email requested 2022-12-09 10:15:33 +01:00
standardci
bf04262170 chore(release): publish new version
- @standardnotes/analytics@2.12.14
 - @standardnotes/api-gateway@1.39.18
 - @standardnotes/auth-server@1.65.0
 - @standardnotes/domain-events-infra@1.9.48
 - @standardnotes/domain-events@2.99.0
 - @standardnotes/event-store@1.6.45
 - @standardnotes/files-server@1.8.44
 - @standardnotes/revisions-server@1.9.17
 - @standardnotes/scheduler-server@1.14.9
 - @standardnotes/syncing-server@1.20.17
 - @standardnotes/websockets-server@1.4.45
 - @standardnotes/workspace-server@1.17.44
2022-12-09 09:03:31 +00:00
Karol Sójko
fd589922bb feat(auth): remove offline subscription token created event in favour of email requested 2022-12-09 10:01:38 +01:00
standardci
fb7029f5c1 chore(release): publish new version
- @standardnotes/analytics@2.12.13
 - @standardnotes/api-gateway@1.39.17
 - @standardnotes/auth-server@1.64.7
 - @standardnotes/domain-events-infra@1.9.47
 - @standardnotes/domain-events@2.98.4
 - @standardnotes/event-store@1.6.44
 - @standardnotes/files-server@1.8.43
 - @standardnotes/revisions-server@1.9.16
 - @standardnotes/scheduler-server@1.14.8
 - @standardnotes/syncing-server@1.20.16
 - @standardnotes/websockets-server@1.4.44
 - @standardnotes/workspace-server@1.17.43
2022-12-09 07:54:49 +00:00
Karol Sójko
cc4b4f9bf8 fix(domain-events): remove unused event 2022-12-09 08:52:51 +01:00
standardci
b048d6d7e3 chore(release): publish new version
- @standardnotes/analytics@2.12.12
 - @standardnotes/api-gateway@1.39.16
 - @standardnotes/auth-server@1.64.6
 - @standardnotes/domain-events-infra@1.9.46
 - @standardnotes/domain-events@2.98.3
 - @standardnotes/event-store@1.6.43
 - @standardnotes/files-server@1.8.42
 - @standardnotes/revisions-server@1.9.15
 - @standardnotes/scheduler-server@1.14.7
 - @standardnotes/syncing-server@1.20.15
 - @standardnotes/websockets-server@1.4.43
 - @standardnotes/workspace-server@1.17.42
2022-12-08 14:03:38 +00:00
Karol Sójko
cffc1f442f fix(domain-events): remove unused event 2022-12-08 15:01:44 +01:00
standardci
91fe710741 chore(release): publish new version
- @standardnotes/analytics@2.12.11
 - @standardnotes/api-gateway@1.39.15
 - @standardnotes/auth-server@1.64.5
 - @standardnotes/domain-events-infra@1.9.45
 - @standardnotes/domain-events@2.98.2
 - @standardnotes/event-store@1.6.42
 - @standardnotes/files-server@1.8.41
 - @standardnotes/revisions-server@1.9.14
 - @standardnotes/scheduler-server@1.14.6
 - @standardnotes/syncing-server@1.20.14
 - @standardnotes/websockets-server@1.4.42
 - @standardnotes/workspace-server@1.17.41
2022-12-08 13:55:05 +00:00
Karol Sójko
5a1eb9fdac fix(domain-events): remove unused event 2022-12-08 14:53:07 +01:00
standardci
a64ef6e750 chore(release): publish new version
- @standardnotes/analytics@2.12.10
 - @standardnotes/api-gateway@1.39.14
 - @standardnotes/auth-server@1.64.4
 - @standardnotes/domain-events-infra@1.9.44
 - @standardnotes/domain-events@2.98.1
 - @standardnotes/event-store@1.6.41
 - @standardnotes/files-server@1.8.40
 - @standardnotes/revisions-server@1.9.13
 - @standardnotes/scheduler-server@1.14.5
 - @standardnotes/syncing-server@1.20.13
 - @standardnotes/websockets-server@1.4.41
 - @standardnotes/workspace-server@1.17.40
2022-12-08 13:49:54 +00:00
Karol Sójko
47d2012b3d fix(domain-events): remove unused event 2022-12-08 14:47:39 +01:00
standardci
2c99cd2e21 chore(release): publish new version
- @standardnotes/analytics@2.12.9
 - @standardnotes/api-gateway@1.39.13
 - @standardnotes/auth-server@1.64.3
 - @standardnotes/domain-events-infra@1.9.43
 - @standardnotes/domain-events@2.98.0
 - @standardnotes/event-store@1.6.40
 - @standardnotes/files-server@1.8.39
 - @standardnotes/revisions-server@1.9.12
 - @standardnotes/scheduler-server@1.14.4
 - @standardnotes/syncing-server@1.20.12
 - @standardnotes/websockets-server@1.4.40
 - @standardnotes/workspace-server@1.17.39
2022-12-08 10:01:57 +00:00
Karol Sójko
435cd2f66a feat(domain-events): remove unused events and add attachments option for sending emails 2022-12-08 11:00:02 +01:00
standardci
372b12dfc2 chore(release): publish new version
- @standardnotes/analytics@2.12.8
 - @standardnotes/api-gateway@1.39.12
 - @standardnotes/auth-server@1.64.2
 - @standardnotes/domain-events-infra@1.9.42
 - @standardnotes/domain-events@2.97.0
 - @standardnotes/event-store@1.6.39
 - @standardnotes/files-server@1.8.38
 - @standardnotes/revisions-server@1.9.11
 - @standardnotes/scheduler-server@1.14.3
 - @standardnotes/syncing-server@1.20.11
 - @standardnotes/websockets-server@1.4.39
 - @standardnotes/workspace-server@1.17.38
2022-12-08 09:13:34 +00:00
Karol Sójko
3a12f5c1c4 feat(domain-events): remove unused account reset requested event 2022-12-08 10:11:14 +01:00
standardci
781de224b6 chore(release): publish new version
- @standardnotes/event-store@1.6.38
2022-12-07 14:36:38 +00:00
Karol Sójko
eff09454c3 fix(event-store): add email requested subscription 2022-12-07 15:34:41 +01:00
Karol Sójko
473feba6a8 fix(event-store): reduce handlers 2022-12-07 15:34:41 +01:00
standardci
e9f0704fb0 chore(release): publish new version
- @standardnotes/auth-server@1.64.1
2022-12-07 14:00:14 +00:00
Mo
8c99469d88 refactor: future-proof code verifier check on sign in (#363) 2022-12-07 07:58:26 -06:00
standardci
8ec1311dfc chore(release): publish new version
- @standardnotes/analytics@2.12.7
 - @standardnotes/api-gateway@1.39.11
 - @standardnotes/auth-server@1.64.0
 - @standardnotes/domain-events-infra@1.9.41
 - @standardnotes/domain-events@2.96.0
 - @standardnotes/event-store@1.6.37
 - @standardnotes/files-server@1.8.37
 - @standardnotes/revisions-server@1.9.10
 - @standardnotes/scheduler-server@1.14.2
 - @standardnotes/syncing-server@1.20.10
 - @standardnotes/websockets-server@1.4.38
 - @standardnotes/workspace-server@1.17.37
2022-12-07 13:47:14 +00:00
Karol Sójko
e48cca6b45 feat(auth): replace user signed in events with email requested 2022-12-07 14:45:16 +01:00
standardci
d660721f95 chore(release): publish new version
- @standardnotes/scheduler-server@1.14.1
2022-12-07 11:25:27 +00:00
Karol Sójko
c52bb93d79 fix(scheduler): importing email contents 2022-12-07 12:23:29 +01:00
standardci
ffb6bfd0c9 chore(release): publish new version
- @standardnotes/scheduler-server@1.14.0
2022-12-07 10:12:08 +00:00
Karol Sójko
6e0855f9b3 feat(scheduler): add scheduled emails contents 2022-12-07 11:10:13 +01:00
109 changed files with 1282 additions and 1101 deletions

1
.pnp.cjs generated
View File

@@ -3056,6 +3056,7 @@ const RAW_RUNTIME_STATE =
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/predicates", "workspace:packages/predicates"],\

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"semi": false
}

View File

@@ -3,6 +3,50 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.12.17](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.16...@standardnotes/analytics@2.12.17) (2022-12-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.15...@standardnotes/analytics@2.12.16) (2022-12-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.14...@standardnotes/analytics@2.12.15) (2022-12-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.13...@standardnotes/analytics@2.12.14) (2022-12-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.12...@standardnotes/analytics@2.12.13) (2022-12-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.11...@standardnotes/analytics@2.12.12) (2022-12-08)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.11](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.10...@standardnotes/analytics@2.12.11) (2022-12-08)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.9...@standardnotes/analytics@2.12.10) (2022-12-08)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.8...@standardnotes/analytics@2.12.9) (2022-12-08)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.7...@standardnotes/analytics@2.12.8) (2022-12-08)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.6...@standardnotes/analytics@2.12.7) (2022-12-07)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.5...@standardnotes/analytics@2.12.6) (2022-12-07)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.12.6",
"version": "2.12.17",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,50 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.21](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.20...@standardnotes/api-gateway@1.39.21) (2022-12-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.20](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.19...@standardnotes/api-gateway@1.39.20) (2022-12-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.19](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.18...@standardnotes/api-gateway@1.39.19) (2022-12-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.18](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.17...@standardnotes/api-gateway@1.39.18) (2022-12-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.17](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.16...@standardnotes/api-gateway@1.39.17) (2022-12-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.16](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.15...@standardnotes/api-gateway@1.39.16) (2022-12-08)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.15](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.14...@standardnotes/api-gateway@1.39.15) (2022-12-08)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.14](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.13...@standardnotes/api-gateway@1.39.14) (2022-12-08)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.13](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.12...@standardnotes/api-gateway@1.39.13) (2022-12-08)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.12](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.11...@standardnotes/api-gateway@1.39.12) (2022-12-08)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.11](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.10...@standardnotes/api-gateway@1.39.11) (2022-12-07)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.10](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.9...@standardnotes/api-gateway@1.39.10) (2022-12-07)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.39.10",
"version": "1.39.21",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,64 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.66.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.2...@standardnotes/auth-server@1.66.3) (2022-12-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.66.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.1...@standardnotes/auth-server@1.66.2) (2022-12-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.66.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.0...@standardnotes/auth-server@1.66.1) (2022-12-09)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.66.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.65.0...@standardnotes/auth-server@1.66.0) (2022-12-09)
### Features
* **email:** replace offline subscription token created event in favour of email requested ([b595264](https://github.com/standardnotes/server/commit/b595264e313ac5ae5404f6a4a05b90b8c11f7f02))
# [1.65.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.64.7...@standardnotes/auth-server@1.65.0) (2022-12-09)
### Features
* **auth:** remove offline subscription token created event in favour of email requested ([fd58992](https://github.com/standardnotes/server/commit/fd589922bba29595a0dfd154a42fe158024fad28))
## [1.64.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.64.6...@standardnotes/auth-server@1.64.7) (2022-12-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.64.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.64.5...@standardnotes/auth-server@1.64.6) (2022-12-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.64.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.64.4...@standardnotes/auth-server@1.64.5) (2022-12-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.64.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.64.3...@standardnotes/auth-server@1.64.4) (2022-12-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.64.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.64.2...@standardnotes/auth-server@1.64.3) (2022-12-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.64.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.64.1...@standardnotes/auth-server@1.64.2) (2022-12-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.64.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.64.0...@standardnotes/auth-server@1.64.1) (2022-12-07)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.64.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.63.2...@standardnotes/auth-server@1.64.0) (2022-12-07)
### Features
* **auth:** replace user signed in events with email requested ([e48cca6](https://github.com/standardnotes/server/commit/e48cca6b45b02876f2d82b726c1d2f124d90b587))
## [1.63.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.63.1...@standardnotes/auth-server@1.63.2) (2022-12-07)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -1,136 +0,0 @@
import 'reflect-metadata'
import 'newrelic'
import { Stream } from 'stream'
import { Logger } from 'winston'
import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc'
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { SettingServiceInterface } from '../src/Domain/Setting/SettingServiceInterface'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { UserSubscriptionRepositoryInterface } from '../src/Domain/Subscription/UserSubscriptionRepositoryInterface'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { MuteMarketingEmailsOption, SettingName } from '@standardnotes/settings'
import { EmailMessageIdentifier } from '@standardnotes/common'
import { TimerInterface } from '@standardnotes/time'
const inputArgs = process.argv.slice(2)
const emailMessageIdentifier = inputArgs[0]
const sendEmailCampaign = async (
userRepository: UserRepositoryInterface,
settingService: SettingServiceInterface,
userSubscriptionRepository: UserSubscriptionRepositoryInterface,
timer: TimerInterface,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
logger: Logger,
): Promise<void> => {
const stream = await userRepository.streamAll()
return new Promise((resolve, reject) => {
stream
.pipe(
new Stream.Transform({
objectMode: true,
transform: async (rawUserData, _encoding, callback) => {
try {
const emailsMutedSetting = await settingService.findSettingWithDecryptedValue({
userUuid: rawUserData.user_uuid,
settingName: SettingName.MuteMarketingEmails,
})
if (emailsMutedSetting === null || emailsMutedSetting.value === MuteMarketingEmailsOption.Muted) {
callback()
return
}
let activeSubscription = false
let subscriptionPlanName = null
const userSubscription = await userSubscriptionRepository.findOneByUserUuid(rawUserData.user_uuid)
if (userSubscription !== null) {
activeSubscription =
!userSubscription.cancelled && userSubscription.endsAt > timer.getTimestampInMicroseconds()
subscriptionPlanName = userSubscription.planName
}
await domainEventPublisher.publish(
domainEventFactory.createEmailMessageRequestedEvent({
userEmail: rawUserData.user_email,
messageIdentifier: emailMessageIdentifier as EmailMessageIdentifier,
context: {
activeSubscription,
subscriptionPlanName,
muteEmailsSettingUuid: emailsMutedSetting.uuid,
},
}),
)
} catch (error) {
logger.error(`Could not process user ${rawUserData.user_uuid}: ${(error as Error).message}`)
}
callback()
},
}),
)
.on('finish', resolve)
.on('error', reject)
})
}
const container = new ContainerConfigLoader()
void container.load().then((container) => {
dayjs.extend(utc)
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Logger)
logger.info(`Starting email campaign for email ${emailMessageIdentifier} ...`)
if (!emailMessageIdentifier) {
logger.error('No email message identifier passed as argument. Skipped sending.')
process.exit(1)
}
const userRepository: UserRepositoryInterface = container.get(TYPES.UserRepository)
const settingService: SettingServiceInterface = container.get(TYPES.SettingService)
const userSubscriptionRepository: UserSubscriptionRepositoryInterface = container.get(
TYPES.UserSubscriptionRepository,
)
const timer: TimerInterface = container.get(TYPES.Timer)
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
Promise.resolve(
sendEmailCampaign(
userRepository,
settingService,
userSubscriptionRepository,
timer,
domainEventFactory,
domainEventPublisher,
logger,
),
)
.then(() => {
logger.info(`${emailMessageIdentifier} email campaign complete.`)
process.exit(0)
})
.catch((error) => {
logger.error(`Could not finish ${emailMessageIdentifier} email campaign: ${error.message}`)
process.exit(1)
})
})

View File

@@ -50,12 +50,6 @@ case "$COMMAND" in
yarn workspace @standardnotes/auth-server daily-backup:one_drive
;;
'email-campaign' )
echo "[Docker] Starting Email Campaign Sending..."
MESSAGE_IDENTIFIER=$1 && shift 1
yarn workspace @standardnotes/auth-server email-campaign $MESSAGE_IDENTIFIER
;;
'content-recalculation' )
echo "[Docker] Starting Content Size Recalculation..."
yarn workspace @standardnotes/auth-server content-recalculation

View File

@@ -7,6 +7,6 @@ module.exports = {
transform: {
...tsjPreset.transform,
},
coveragePathIgnorePatterns: ['/Bootstrap/', '/InversifyExpressUtils/', '/Infra/', '/Projection/'],
coveragePathIgnorePatterns: ['/Bootstrap/', '/InversifyExpressUtils/', '/Infra/', '/Projection/', '/Domain/Email/'],
setupFilesAfterEnv: ['./test-setup.ts'],
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.63.2",
"version": "1.66.3",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -26,7 +26,6 @@
"daily-backup:one_drive": "yarn node dist/bin/backup.js one_drive daily",
"weekly-backup:email": "yarn node dist/bin/backup.js email weekly",
"content-recalculation": "yarn node dist/bin/content.js",
"email-campaign": "yarn node dist/bin/email.js",
"typeorm": "typeorm-ts-node-commonjs",
"upgrade:snjs": "yarn ncu -u '@standardnotes/*'"
},

View File

@@ -0,0 +1,9 @@
import { html } from './offline-subscription-token-created.html'
export function getSubject(): string {
return 'Access to your Standard Notes Subscription Dashboard'
}
export function getBody(email: string, offlineSubscriptionDashboardUrl: string): string {
return html(email, offlineSubscriptionDashboardUrl)
}

View File

@@ -0,0 +1,9 @@
import { html } from './shared-subscription-invitation-created.html'
export function getSubject(): string {
return 'You have been invited to a Standard Notes subscription'
}
export function getBody(inviterIdentifier: string, inviteUuid: string): string {
return html(inviterIdentifier, inviteUuid)
}

View File

@@ -0,0 +1,9 @@
import { html } from './user-signed-in.html'
export function getSubject(email: string): string {
return `New sign-in for ${email}`
}
export function getBody(email: string, device: string, browser: string, date: Date): string {
return html(email, device, browser, date.toLocaleString())
}

View File

@@ -0,0 +1,24 @@
export const html = (userEmail: string, offlineSubscriptionDashboardUrl: string) => `<div class="sn-component">
<div class="sk-panel static">
<div class="sk-panel-content">
<div class="sk-panel-section">
<h1 class="h1 title sk-panel-row">
<div class="sk-panel-column">
Access your Standard Notes Subscription Dashboard,
</div>
</h1>
<div class="faded sk-panel-row small">Registered as ${userEmail}</div>
</div>
<div class="sk-panel-section">
<div class="title">Link to your subscription dashboard: <a
href="${offlineSubscriptionDashboardUrl}">${offlineSubscriptionDashboardUrl}</a></div>
</div>
<div class="sk-panel-section">
<p>
Get help any time by visiting our <a href="https://standardnotes.com/help">Help page</a>
or by replying directly to this email.
</p>
</div>
</div>
</div>
</div>`

View File

@@ -0,0 +1,5 @@
export const html = (inviterIdentifier: string, inviteUuid: string) => `<p>Hello,</p>
<p>You've been invited to join a Standard Notes premium subscription at no cost. ${inviterIdentifier} has invited you to share the benefits of their subscription plan.</p>
<p>
<a href='https://app.standardnotes.com/?accept-subscription-invite=${inviteUuid}'>Accept Invite</a>
</p>`

View File

@@ -0,0 +1,25 @@
export const html = (email: string, device: string, browser: string, timeAndDate: string) => `
<div>
<p>Hello,</p>
<p>We've detected a new sign-in to your account ${email}</p>
<p>
<b>Device type</b>: ${device}
</p>
<p>
<b>Browser type</b>: ${browser}
</p>
<p>
<strong>Time and date</strong>: <span>${timeAndDate}</span>
</p>
<p>
If this was you, please disregard this email. If it wasn't you, we recommend signing into your account and
changing your password immediately, then enabling 2FA.
</p>
<p>
Thanks,
<br />
SN
</p>
<a href="https://app.standardnotes.com/?settings=account">Mute these emails</a>
</div>
`

View File

@@ -1,26 +1,24 @@
/* istanbul ignore file */
import { EmailMessageIdentifier, JSONString, ProtocolVersion, RoleName, Uuid } from '@standardnotes/common'
import { JSONString, ProtocolVersion, RoleName, Uuid } from '@standardnotes/common'
import {
AccountDeletionRequestedEvent,
UserEmailChangedEvent,
UserRegisteredEvent,
UserRolesChangedEvent,
OfflineSubscriptionTokenCreatedEvent,
EmailBackupRequestedEvent,
CloudBackupRequestedEvent,
ListedAccountRequestedEvent,
UserSignedInEvent,
UserDisabledSessionUserAgentLoggingEvent,
SharedSubscriptionInvitationCreatedEvent,
SharedSubscriptionInvitationCanceledEvent,
PredicateVerifiedEvent,
DomainEventService,
EmailMessageRequestedEvent,
WebSocketMessageRequestedEvent,
ExitDiscountApplyRequestedEvent,
UserContentSizeRecalculationRequestedEvent,
MuteEmailsSettingChangedEvent,
EmailRequestedEvent,
} from '@standardnotes/domain-events'
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
import { TimerInterface } from '@standardnotes/time'
@@ -102,13 +100,15 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
}
}
createEmailMessageRequestedEvent(dto: {
createEmailRequestedEvent(dto: {
userEmail: string
messageIdentifier: EmailMessageIdentifier
context: Record<string, unknown>
}): EmailMessageRequestedEvent {
messageIdentifier: string
level: string
body: string
subject: string
}): EmailRequestedEvent {
return {
type: 'EMAIL_MESSAGE_REQUESTED',
type: 'EMAIL_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
@@ -202,28 +202,6 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
}
}
createUserSignedInEvent(dto: {
userUuid: string
userEmail: string
device: string
browser: string
signInAlertEnabled: boolean
muteSignInEmailsSettingUuid: Uuid
}): UserSignedInEvent {
return {
type: 'USER_SIGNED_IN',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.Auth,
},
payload: dto,
}
}
createListedAccountRequestedEvent(userUuid: string, userEmail: string): ListedAccountRequestedEvent {
return {
type: 'LISTED_ACCOUNT_REQUESTED',
@@ -311,24 +289,6 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
}
}
createOfflineSubscriptionTokenCreatedEvent(token: string, email: string): OfflineSubscriptionTokenCreatedEvent {
return {
type: 'OFFLINE_SUBSCRIPTION_TOKEN_CREATED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: email,
userIdentifierType: 'email',
},
origin: DomainEventService.Auth,
},
payload: {
token,
email,
},
}
}
createUserRegisteredEvent(dto: {
userUuid: string
email: string

View File

@@ -1,4 +1,4 @@
import { Uuid, RoleName, EmailMessageIdentifier, ProtocolVersion, JSONString } from '@standardnotes/common'
import { Uuid, RoleName, ProtocolVersion, JSONString } from '@standardnotes/common'
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
import {
AccountDeletionRequestedEvent,
@@ -6,38 +6,30 @@ import {
UserRegisteredEvent,
UserRolesChangedEvent,
UserEmailChangedEvent,
OfflineSubscriptionTokenCreatedEvent,
EmailBackupRequestedEvent,
ListedAccountRequestedEvent,
UserSignedInEvent,
UserDisabledSessionUserAgentLoggingEvent,
SharedSubscriptionInvitationCreatedEvent,
SharedSubscriptionInvitationCanceledEvent,
PredicateVerifiedEvent,
EmailMessageRequestedEvent,
WebSocketMessageRequestedEvent,
ExitDiscountApplyRequestedEvent,
UserContentSizeRecalculationRequestedEvent,
MuteEmailsSettingChangedEvent,
EmailRequestedEvent,
} from '@standardnotes/domain-events'
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
export interface DomainEventFactoryInterface {
createUserContentSizeRecalculationRequestedEvent(userUuid: string): UserContentSizeRecalculationRequestedEvent
createWebSocketMessageRequestedEvent(dto: { userUuid: Uuid; message: JSONString }): WebSocketMessageRequestedEvent
createEmailMessageRequestedEvent(dto: {
createEmailRequestedEvent(dto: {
userEmail: string
messageIdentifier: EmailMessageIdentifier
context: Record<string, unknown>
}): EmailMessageRequestedEvent
createUserSignedInEvent(dto: {
userUuid: string
userEmail: string
device: string
browser: string
signInAlertEnabled: boolean
muteSignInEmailsSettingUuid: Uuid
}): UserSignedInEvent
messageIdentifier: string
level: string
body: string
subject: string
}): EmailRequestedEvent
createListedAccountRequestedEvent(userUuid: string, userEmail: string): ListedAccountRequestedEvent
createUserRegisteredEvent(dto: {
userUuid: string
@@ -63,7 +55,6 @@ export interface DomainEventFactoryInterface {
}): AccountDeletionRequestedEvent
createUserRolesChangedEvent(userUuid: string, email: string, currentRoles: RoleName[]): UserRolesChangedEvent
createUserEmailChangedEvent(userUuid: string, fromEmail: string, toEmail: string): UserEmailChangedEvent
createOfflineSubscriptionTokenCreatedEvent(token: string, email: string): OfflineSubscriptionTokenCreatedEvent
createUserDisabledSessionUserAgentLoggingEvent(dto: {
userUuid: Uuid
email: string

View File

@@ -5,7 +5,7 @@ import { TimerInterface } from '@standardnotes/time'
import { OfflineSubscriptionTokenRepositoryInterface } from '../../Auth/OfflineSubscriptionTokenRepositoryInterface'
import { CreateOfflineSubscriptionToken } from './CreateOfflineSubscriptionToken'
import { DomainEventPublisherInterface, OfflineSubscriptionTokenCreatedEvent } from '@standardnotes/domain-events'
import { DomainEventPublisherInterface, EmailRequestedEvent } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { OfflineUserSubscription } from '../../Subscription/OfflineUserSubscription'
@@ -47,9 +47,9 @@ describe('CreateOfflineSubscriptionToken', () => {
domainEventPublisher.publish = jest.fn()
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createOfflineSubscriptionTokenCreatedEvent = jest
domainEventFactory.createEmailRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<OfflineSubscriptionTokenCreatedEvent>)
.mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
timer = {} as jest.Mocked<TimerInterface>
timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(1)
@@ -71,10 +71,7 @@ describe('CreateOfflineSubscriptionToken', () => {
expiresAt: 1,
})
expect(domainEventFactory.createOfflineSubscriptionTokenCreatedEvent).toHaveBeenCalledWith(
'random-string',
'test@test.com',
)
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
@@ -91,7 +88,7 @@ describe('CreateOfflineSubscriptionToken', () => {
})
expect(offlineSubscriptionTokenRepository.save).not.toHaveBeenCalled()
expect(domainEventFactory.createOfflineSubscriptionTokenCreatedEvent).not.toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
@@ -110,7 +107,7 @@ describe('CreateOfflineSubscriptionToken', () => {
})
expect(offlineSubscriptionTokenRepository.save).not.toHaveBeenCalled()
expect(domainEventFactory.createOfflineSubscriptionTokenCreatedEvent).not.toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
@@ -129,7 +126,7 @@ describe('CreateOfflineSubscriptionToken', () => {
})
expect(offlineSubscriptionTokenRepository.save).not.toHaveBeenCalled()
expect(domainEventFactory.createOfflineSubscriptionTokenCreatedEvent).not.toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
})

View File

@@ -1,4 +1,5 @@
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { EmailLevel } from '@standardnotes/domain-core'
import { CryptoNode } from '@standardnotes/sncrypto-node'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
@@ -6,6 +7,7 @@ import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { OfflineSubscriptionTokenRepositoryInterface } from '../../Auth/OfflineSubscriptionTokenRepositoryInterface'
import { getBody, getSubject } from '../../Email/OfflineSubscriptionTokenCreated'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
@@ -62,7 +64,13 @@ export class CreateOfflineSubscriptionToken implements UseCaseInterface {
await this.offlineSubscriptionTokenRepository.save(offlineSubscriptionToken)
await this.domainEventPublisher.publish(
this.domainEventFactory.createOfflineSubscriptionTokenCreatedEvent(token, dto.userEmail),
this.domainEventFactory.createEmailRequestedEvent({
body: getBody(dto.userEmail, `https://standardnotes.com/dashboard/offline?subscription_token=${token}`),
level: EmailLevel.LEVELS.System,
subject: getSubject(),
messageIdentifier: 'OFFLINE_SUBSCRIPTION_ACCESS',
userEmail: dto.userEmail,
}),
)
return {

View File

@@ -1,6 +1,10 @@
import 'reflect-metadata'
import { DomainEventPublisherInterface, SharedSubscriptionInvitationCreatedEvent } from '@standardnotes/domain-events'
import {
DomainEventPublisherInterface,
SharedSubscriptionInvitationCreatedEvent,
EmailRequestedEvent,
} from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { SharedSubscriptionInvitationRepositoryInterface } from '../../SharedSubscription/SharedSubscriptionInvitationRepositoryInterface'
@@ -51,6 +55,7 @@ describe('InviteToSharedSubscription', () => {
domainEventFactory.createSharedSubscriptionInvitationCreatedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<SharedSubscriptionInvitationCreatedEvent>)
domainEventFactory.createEmailRequestedEvent = jest.fn().mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
})
it('should not create an inivitation for sharing the subscription if inviter has no subscription', async () => {

View File

@@ -1,9 +1,11 @@
import { RoleName } from '@standardnotes/common'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { EmailLevel } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../../../Bootstrap/Types'
import { getBody, getSubject } from '../../Email/SharedSubscriptionInvitationCreated'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
import { InviteeIdentifierType } from '../../SharedSubscription/InviteeIdentifierType'
@@ -89,6 +91,16 @@ export class InviteToSharedSubscription implements UseCaseInterface {
}),
)
await this.domainEventPublisher.publish(
this.domainEventFactory.createEmailRequestedEvent({
userEmail: dto.inviteeIdentifier,
level: EmailLevel.LEVELS.System,
body: getBody(dto.inviterEmail, savedInvitation.uuid),
messageIdentifier: 'SHARED_SUBSCRIPTION_INVITATION',
subject: getSubject(),
}),
)
return {
success: true,
sharedSubscriptionInvitationUuid: savedInvitation.uuid,

View File

@@ -1,6 +1,6 @@
import 'reflect-metadata'
import { DomainEventPublisherInterface, UserSignedInEvent } from '@standardnotes/domain-events'
import { DomainEventPublisherInterface, EmailRequestedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { AuthResponseFactoryInterface } from '../Auth/AuthResponseFactoryInterface'
@@ -10,10 +10,6 @@ import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { SignIn } from './SignIn'
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { Setting } from '../Setting/Setting'
import { MuteSignInEmailsOption } from '@standardnotes/settings'
import { PKCERepositoryInterface } from '../User/PKCERepositoryInterface'
import { CrypterInterface } from '../Encryption/CrypterInterface'
import { ProtocolVersion } from '@standardnotes/common'
@@ -26,10 +22,7 @@ describe('SignIn', () => {
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let sessionService: SessionServiceInterface
let roleService: RoleServiceInterface
let logger: Logger
let settingService: SettingServiceInterface
let setting: Setting
let pkceRepository: PKCERepositoryInterface
let crypter: CrypterInterface
@@ -40,8 +33,6 @@ describe('SignIn', () => {
domainEventPublisher,
domainEventFactory,
sessionService,
roleService,
settingService,
pkceRepository,
crypter,
logger,
@@ -68,27 +59,12 @@ describe('SignIn', () => {
domainEventPublisher.publish = jest.fn()
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createUserSignedInEvent = jest.fn().mockReturnValue({} as jest.Mocked<UserSignedInEvent>)
domainEventFactory.createEmailRequestedEvent = jest.fn().mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.getOperatingSystemInfoFromUserAgent = jest.fn().mockReturnValue('iOS 1')
sessionService.getBrowserInfoFromUserAgent = jest.fn().mockReturnValue('Firefox 1')
roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.userHasPermission = jest.fn().mockReturnValue(true)
setting = {
uuid: '3-4-5',
value: MuteSignInEmailsOption.NotMuted,
} as jest.Mocked<Setting>
settingService = {} as jest.Mocked<SettingServiceInterface>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
settingService.createOrReplace = jest.fn().mockReturnValue({
status: 'created',
setting,
})
pkceRepository = {} as jest.Mocked<PKCERepositoryInterface>
pkceRepository.removeCodeChallenge = jest.fn().mockReturnValue(true)
@@ -118,18 +94,33 @@ describe('SignIn', () => {
authResponse: { foo: 'bar' },
})
expect(domainEventFactory.createUserSignedInEvent).toHaveBeenCalledWith({
browser: 'Firefox 1',
device: 'iOS 1',
userEmail: 'test@test.com',
userUuid: '1-2-3',
signInAlertEnabled: true,
muteSignInEmailsSettingUuid: '3-4-5',
})
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should not sign in a user without code verifier', async () => {
it('should not sign in 004 user without code verifier', async () => {
expect(
await createUseCase().execute({
email: 'test@test.te',
password: 'qweqwe123123',
userAgent: 'Google Chrome',
apiVersion: '20190520',
ephemeralSession: false,
}),
).toEqual({
success: false,
errorCode: 410,
errorMessage: 'Please update your client application.',
})
})
it('should not sign in 005 user without code verifier', async () => {
user = {
uuid: '1-2-3',
email: 'test@test.com',
version: '005',
} as jest.Mocked<User>
expect(
await createUseCase().execute({
email: 'test@test.te',
@@ -160,92 +151,10 @@ describe('SignIn', () => {
authResponse: { foo: 'bar' },
})
expect(domainEventFactory.createUserSignedInEvent).toHaveBeenCalledWith({
browser: 'Firefox 1',
device: 'iOS 1',
userEmail: 'test@test.com',
userUuid: '1-2-3',
signInAlertEnabled: true,
muteSignInEmailsSettingUuid: '3-4-5',
})
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should sign in a user and disable sign in alert if setting is configured', async () => {
setting = {
uuid: '3-4-5',
value: MuteSignInEmailsOption.Muted,
} as jest.Mocked<Setting>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(
await createUseCase().execute({
email: 'test@test.te',
password: 'qweqwe123123',
userAgent: 'Google Chrome',
apiVersion: '20190520',
ephemeralSession: false,
codeVerifier: 'test',
}),
).toEqual({
success: true,
authResponse: { foo: 'bar' },
})
expect(domainEventFactory.createUserSignedInEvent).toHaveBeenCalledWith({
browser: 'Firefox 1',
device: 'iOS 1',
userEmail: 'test@test.com',
userUuid: '1-2-3',
signInAlertEnabled: false,
muteSignInEmailsSettingUuid: '3-4-5',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should sign in a user and create mute sign in email setting if it does not exist', async () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({
email: 'test@test.te',
password: 'qweqwe123123',
userAgent: 'Google Chrome',
apiVersion: '20190520',
ephemeralSession: false,
codeVerifier: 'test',
}),
).toEqual({
success: true,
authResponse: { foo: 'bar' },
})
expect(domainEventFactory.createUserSignedInEvent).toHaveBeenCalledWith({
browser: 'Firefox 1',
device: 'iOS 1',
userEmail: 'test@test.com',
userUuid: '1-2-3',
signInAlertEnabled: true,
muteSignInEmailsSettingUuid: '3-4-5',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(settingService.createOrReplace).toHaveBeenCalledWith({
props: {
name: 'MUTE_SIGN_IN_EMAILS',
sensitive: false,
serverEncryptionVersion: 0,
unencryptedValue: 'not_muted',
},
user: {
email: 'test@test.com',
encryptedPassword: '$2a$11$K3g6XoTau8VmLJcai1bB0eD9/YvBSBRtBhMprJOaVZ0U3SgasZH3a',
uuid: '1-2-3',
version: '004',
},
})
})
it('should sign in a user even if publishing a sign in event fails', async () => {
domainEventPublisher.publish = jest.fn().mockImplementation(() => {
throw new Error('Oops')

View File

@@ -1,18 +1,12 @@
import * as bcrypt from 'bcryptjs'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { PermissionName } from '@standardnotes/features'
import { MuteSignInEmailsOption, SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { AuthResponseFactoryResolverInterface } from '../Auth/AuthResponseFactoryResolverInterface'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { Setting } from '../Setting/Setting'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { SignInDTO } from './SignInDTO'
@@ -21,8 +15,10 @@ import { UseCaseInterface } from './UseCaseInterface'
import { PKCERepositoryInterface } from '../User/PKCERepositoryInterface'
import { CrypterInterface } from '../Encryption/CrypterInterface'
import { SignInDTOV2Challenged } from './SignInDTOV2Challenged'
import { ProtocolVersion } from '@standardnotes/common'
import { leftVersionGreaterThanOrEqualToRight, ProtocolVersion } from '@standardnotes/common'
import { HttpStatusCode } from '@standardnotes/api'
import { EmailLevel } from '@standardnotes/domain-core'
import { getBody, getSubject } from '../Email/UserSignedIn'
@injectable()
export class SignIn implements UseCaseInterface {
@@ -33,8 +29,6 @@ export class SignIn implements UseCaseInterface {
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.SessionService) private sessionService: SessionServiceInterface,
@inject(TYPES.RoleService) private roleService: RoleServiceInterface,
@inject(TYPES.SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.PKCERepository) private pkceRepository: PKCERepositoryInterface,
@inject(TYPES.Crypter) private crypter: CrypterInterface,
@inject(TYPES.Logger) private logger: Logger,
@@ -65,7 +59,12 @@ export class SignIn implements UseCaseInterface {
}
}
if (user.version === ProtocolVersion.V004 && !performingCodeChallengedSignIn) {
const userVersionIs004OrGreater = leftVersionGreaterThanOrEqualToRight(
user.version as ProtocolVersion,
ProtocolVersion.V004,
)
if (userVersionIs004OrGreater && !performingCodeChallengedSignIn) {
return {
success: false,
errorMessage: 'Please update your client application.',
@@ -109,18 +108,18 @@ export class SignIn implements UseCaseInterface {
private async sendSignInEmailNotification(user: User, userAgent: string): Promise<void> {
try {
const muteSignInEmailsSetting = await this.findOrCreateMuteSignInEmailsSetting(user)
await this.domainEventPublisher.publish(
this.domainEventFactory.createUserSignedInEvent({
userUuid: user.uuid,
this.domainEventFactory.createEmailRequestedEvent({
userEmail: user.email,
device: this.sessionService.getOperatingSystemInfoFromUserAgent(userAgent),
browser: this.sessionService.getBrowserInfoFromUserAgent(userAgent),
signInAlertEnabled:
(await this.roleService.userHasPermission(user.uuid, PermissionName.SignInAlerts)) &&
muteSignInEmailsSetting.value === MuteSignInEmailsOption.NotMuted,
muteSignInEmailsSettingUuid: muteSignInEmailsSetting.uuid,
level: EmailLevel.LEVELS.SignIn,
body: getBody(
user.email,
this.sessionService.getOperatingSystemInfoFromUserAgent(userAgent),
this.sessionService.getBrowserInfoFromUserAgent(userAgent),
new Date(),
),
messageIdentifier: 'SIGN_IN',
subject: getSubject(user.email),
}),
)
} catch (error) {
@@ -128,29 +127,6 @@ export class SignIn implements UseCaseInterface {
}
}
private async findOrCreateMuteSignInEmailsSetting(user: User): Promise<Setting> {
const existingMuteSignInEmailsSetting = await this.settingService.findSettingWithDecryptedValue({
userUuid: user.uuid,
settingName: SettingName.MuteSignInEmails,
})
if (existingMuteSignInEmailsSetting !== null) {
return existingMuteSignInEmailsSetting
}
const createSettingResult = await this.settingService.createOrReplace({
user,
props: {
name: SettingName.MuteSignInEmails,
sensitive: false,
unencryptedValue: MuteSignInEmailsOption.NotMuted,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
},
})
return createSettingResult.setting
}
private isCodeChallengedVersion(dto: SignInDTO): dto is SignInDTOV2Challenged {
return (dto as SignInDTOV2Challenged).codeVerifier !== undefined
}

View File

@@ -3,6 +3,50 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.51](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.50...@standardnotes/domain-events-infra@1.9.51) (2022-12-09)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.50](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.49...@standardnotes/domain-events-infra@1.9.50) (2022-12-09)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.49](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.48...@standardnotes/domain-events-infra@1.9.49) (2022-12-09)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.48](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.47...@standardnotes/domain-events-infra@1.9.48) (2022-12-09)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.47](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.46...@standardnotes/domain-events-infra@1.9.47) (2022-12-09)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.46](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.45...@standardnotes/domain-events-infra@1.9.46) (2022-12-08)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.45](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.44...@standardnotes/domain-events-infra@1.9.45) (2022-12-08)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.44](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.43...@standardnotes/domain-events-infra@1.9.44) (2022-12-08)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.43](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.42...@standardnotes/domain-events-infra@1.9.43) (2022-12-08)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.42](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.41...@standardnotes/domain-events-infra@1.9.42) (2022-12-08)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.41](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.40...@standardnotes/domain-events-infra@1.9.41) (2022-12-07)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.40](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.39...@standardnotes/domain-events-infra@1.9.40) (2022-12-07)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.9.40",
"version": "1.9.51",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,72 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.102.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.101.0...@standardnotes/domain-events@2.102.0) (2022-12-09)
### Features
* **syncing-server:** replace email backup attachment created with email requested ([32601f3](https://github.com/standardnotes/server/commit/32601f34f181b29b7c62cd2926111a0887d97fbf))
# [2.101.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.100.0...@standardnotes/domain-events@2.101.0) (2022-12-09)
### Features
* **syncing-server:** replace one drive backup failed event with email requested ([130f90b](https://github.com/standardnotes/server/commit/130f90bdb6cc88e073b9380e8aed5eebe8c49c1e))
# [2.100.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.99.0...@standardnotes/domain-events@2.100.0) (2022-12-09)
### Features
* **syncing-server:** remove google drive backup failed event in favour of email requested ([00fe321](https://github.com/standardnotes/server/commit/00fe32136e7add627e58e8ea223f7f484f1d3718))
# [2.99.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.98.4...@standardnotes/domain-events@2.99.0) (2022-12-09)
### Features
* **auth:** remove offline subscription token created event in favour of email requested ([fd58992](https://github.com/standardnotes/server/commit/fd589922bba29595a0dfd154a42fe158024fad28))
## [2.98.4](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.98.3...@standardnotes/domain-events@2.98.4) (2022-12-09)
### Bug Fixes
* **domain-events:** remove unused event ([cc4b4f9](https://github.com/standardnotes/server/commit/cc4b4f9bf831b9aabec7d506d977ee1df50d5222))
## [2.98.3](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.98.2...@standardnotes/domain-events@2.98.3) (2022-12-08)
### Bug Fixes
* **domain-events:** remove unused event ([cffc1f4](https://github.com/standardnotes/server/commit/cffc1f442f3c6f781c4468ac96245e13f57115d5))
## [2.98.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.98.1...@standardnotes/domain-events@2.98.2) (2022-12-08)
### Bug Fixes
* **domain-events:** remove unused event ([5a1eb9f](https://github.com/standardnotes/server/commit/5a1eb9fdacb8cfe8fde06df9e83fef1753b1a619))
## [2.98.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.98.0...@standardnotes/domain-events@2.98.1) (2022-12-08)
### Bug Fixes
* **domain-events:** remove unused event ([47d2012](https://github.com/standardnotes/server/commit/47d2012b3d96eddf5f6304f158659dc764f9b1c4))
# [2.98.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.97.0...@standardnotes/domain-events@2.98.0) (2022-12-08)
### Features
* **domain-events:** remove unused events and add attachments option for sending emails ([435cd2f](https://github.com/standardnotes/server/commit/435cd2f66a1332a294001e87eed3ece1b8b991ae))
# [2.97.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.96.0...@standardnotes/domain-events@2.97.0) (2022-12-08)
### Features
* **domain-events:** remove unused account reset requested event ([3a12f5c](https://github.com/standardnotes/server/commit/3a12f5c1c40ab6cb236b963bad2a987bacef55e4))
# [2.96.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.95.0...@standardnotes/domain-events@2.96.0) (2022-12-07)
### Features
* **auth:** replace user signed in events with email requested ([e48cca6](https://github.com/standardnotes/server/commit/e48cca6b45b02876f2d82b726c1d2f124d90b587))
# [2.95.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.94.1...@standardnotes/domain-events@2.95.0) (2022-12-07)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.95.0",
"version": "2.102.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -1,7 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { AccountClaimRequestedEventPayload } from './AccountClaimRequestedEventPayload'
export interface AccountClaimRequestedEvent extends DomainEventInterface {
type: 'ACCOUNT_CLAIM_REQUESTED'
payload: AccountClaimRequestedEventPayload
}

View File

@@ -1,4 +0,0 @@
export interface AccountClaimRequestedEventPayload {
email: string
token: string
}

View File

@@ -1,8 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { AccountResetRequestedEventPayload } from './AccountResetRequestedEventPayload'
export interface AccountResetRequestedEvent extends DomainEventInterface {
type: 'ACCOUNT_RESET_REQUESTED'
payload: AccountResetRequestedEventPayload
}

View File

@@ -1,4 +0,0 @@
export interface AccountResetRequestedEventPayload {
resetRequestToken: string
userEmail: string
}

View File

@@ -1,7 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { ActivationCodeRequestedEventPayload } from './ActivationCodeRequestedEventPayload'
export interface ActivationCodeRequestedEvent extends DomainEventInterface {
type: 'ACTIVATION_CODE_REQUESTED'
payload: ActivationCodeRequestedEventPayload
}

View File

@@ -1,4 +0,0 @@
export interface ActivationCodeRequestedEventPayload {
userEmail: string
offlineFeaturesToken: string
}

View File

@@ -1,8 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { DiscountAppliedEventPayload } from './DiscountAppliedEventPayload'
export interface DiscountAppliedEvent extends DomainEventInterface {
type: 'DISCOUNT_APPLIED'
payload: DiscountAppliedEventPayload
}

View File

@@ -1,4 +0,0 @@
export interface DiscountAppliedEventPayload {
userEmail: string
discountRate: number
}

View File

@@ -1,8 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { DropboxBackupFailedEventPayload } from './DropboxBackupFailedEventPayload'
export interface DropboxBackupFailedEvent extends DomainEventInterface {
type: 'DROPBOX_BACKUP_FAILED'
payload: DropboxBackupFailedEventPayload
}

View File

@@ -1,5 +0,0 @@
export interface DropboxBackupFailedEventPayload {
muteCloudEmailsSettingUuid: string
extensionSettingUuid?: string
email: string
}

View File

@@ -1,7 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { EmailBackupAttachmentCreatedEventPayload } from './EmailBackupAttachmentCreatedEventPayload'
export interface EmailBackupAttachmentCreatedEvent extends DomainEventInterface {
type: 'EMAIL_BACKUP_ATTACHMENT_CREATED'
payload: EmailBackupAttachmentCreatedEventPayload
}

View File

@@ -1,6 +0,0 @@
export interface EmailBackupAttachmentCreatedEventPayload {
backupFileName: string
backupFileIndex: number
backupFilesTotal: number
email: string
}

View File

@@ -1,7 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { EmailMessageRequestedEventPayload } from './EmailMessageRequestedEventPayload'
export interface EmailMessageRequestedEvent extends DomainEventInterface {
type: 'EMAIL_MESSAGE_REQUESTED'
payload: EmailMessageRequestedEventPayload
}

View File

@@ -1,5 +0,0 @@
export interface EmailMessageRequestedEventPayload {
userEmail: string
messageIdentifier: string
context: Record<string, unknown>
}

View File

@@ -4,4 +4,11 @@ export interface EmailRequestedEventPayload {
level: string
subject: string
body: string
sender?: string
attachments?: Array<{
filePath: string
fileName: string
attachmentFileName: string
attachmentContentType: string
}>
}

View File

@@ -1,8 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { GoogleDriveBackupFailedEventPayload } from './GoogleDriveBackupFailedEventPayload'
export interface GoogleDriveBackupFailedEvent extends DomainEventInterface {
type: 'GOOGLE_DRIVE_BACKUP_FAILED'
payload: GoogleDriveBackupFailedEventPayload
}

View File

@@ -1,5 +0,0 @@
export interface GoogleDriveBackupFailedEventPayload {
muteCloudEmailsSettingUuid: string
extensionSettingUuid?: string
email: string
}

View File

@@ -1,8 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { InvoiceGeneratedEventPayload } from './InvoiceGeneratedEventPayload'
export interface InvoiceGeneratedEvent extends DomainEventInterface {
type: 'INVOICE_GENERATED'
payload: InvoiceGeneratedEventPayload
}

View File

@@ -1,7 +0,0 @@
export interface InvoiceGeneratedEventPayload {
userEmail: string
invoiceNumber: string
paymentDateFormatted: string
s3BucketName: string
s3InvoiceObjectKey: string
}

View File

@@ -1,8 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { OfflineSubscriptionTokenCreatedEventPayload } from './OfflineSubscriptionTokenCreatedEventPayload'
export interface OfflineSubscriptionTokenCreatedEvent extends DomainEventInterface {
type: 'OFFLINE_SUBSCRIPTION_TOKEN_CREATED'
payload: OfflineSubscriptionTokenCreatedEventPayload
}

View File

@@ -1,4 +0,0 @@
export interface OfflineSubscriptionTokenCreatedEventPayload {
token: string
email: string
}

View File

@@ -1,8 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { OneDriveBackupFailedEventPayload } from './OneDriveBackupFailedEventPayload'
export interface OneDriveBackupFailedEvent extends DomainEventInterface {
type: 'ONE_DRIVE_BACKUP_FAILED'
payload: OneDriveBackupFailedEventPayload
}

View File

@@ -1,5 +0,0 @@
export interface OneDriveBackupFailedEventPayload {
muteCloudEmailsSettingUuid: string
extensionSettingUuid?: string
email: string
}

View File

@@ -1,8 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { RefundRequestedEventPayload } from './RefundRequestedEventPayload'
export interface RefundRequestedEvent extends DomainEventInterface {
type: 'REFUND_REQUESTED'
payload: RefundRequestedEventPayload
}

View File

@@ -1,4 +0,0 @@
export interface RefundRequestedEventPayload {
userEmail: string
refundProcessingLink: string
}

View File

@@ -1,8 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { StudentDiscountApprovedEventPayload } from './StudentDiscountApprovedEventPayload'
export interface StudentDiscountApprovedEvent extends DomainEventInterface {
type: 'STUDENT_DISCOUNT_APPROVED'
payload: StudentDiscountApprovedEventPayload
}

View File

@@ -1,3 +0,0 @@
export interface StudentDiscountApprovedEventPayload {
userEmail: string
}

View File

@@ -1,8 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { StudentDiscountRequestedEventPayload } from './StudentDiscountRequestedEventPayload'
export interface StudentDiscountRequestedEvent extends DomainEventInterface {
type: 'STUDENT_DISCOUNT_REQUESTED'
payload: StudentDiscountRequestedEventPayload
}

View File

@@ -1,5 +0,0 @@
export interface StudentDiscountRequestedEventPayload {
studentEmail: string
userEmail: string
adminApprovalLink: string
}

View File

@@ -1,8 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { SubscriptionRateAdjustedEventPayload } from './SubscriptionRateAdjustedEventPayload'
export interface SubscriptionRateAdjustedEvent extends DomainEventInterface {
type: 'SUBSCRIPTION_RATE_ADJUSTED'
payload: SubscriptionRateAdjustedEventPayload
}

View File

@@ -1,5 +0,0 @@
export interface SubscriptionRateAdjustedEventPayload {
userEmail: string
newRateFormatted: string
refundAmountInDollarsFormatted: string
}

View File

@@ -1,7 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { UserSignedInEventPayload } from './UserSignedInEventPayload'
export interface UserSignedInEvent extends DomainEventInterface {
type: 'USER_SIGNED_IN'
payload: UserSignedInEventPayload
}

View File

@@ -1,10 +0,0 @@
import { Uuid } from '@standardnotes/common'
export interface UserSignedInEventPayload {
userUuid: string
userEmail: string
signInAlertEnabled: boolean
muteSignInEmailsSettingUuid: Uuid
device: string
browser?: string
}

View File

@@ -1,35 +1,21 @@
export * from './Event/AccountClaimRequestedEvent'
export * from './Event/AccountClaimRequestedEventPayload'
export * from './Event/AccountDeletionRequestedEvent'
export * from './Event/AccountDeletionRequestedEventPayload'
export * from './Event/AccountResetRequestedEvent'
export * from './Event/AccountResetRequestedEventPayload'
export * from './Event/ActivationCodeRequestedEvent'
export * from './Event/ActivationCodeRequestedEventPayload'
export * from './Event/CloudBackupRequestedEvent'
export * from './Event/CloudBackupRequestedEventPayload'
export * from './Event/DailyAnalyticsReportGeneratedEvent'
export * from './Event/DailyAnalyticsReportGeneratedEventPayload'
export * from './Event/DiscountAppliedEvent'
export * from './Event/DiscountAppliedEventPayload'
export * from './Event/DiscountApplyRequestedEvent'
export * from './Event/DiscountApplyRequestedEventPayload'
export * from './Event/DiscountWithdrawRequestedEvent'
export * from './Event/DiscountWithdrawRequestedEventPayload'
export * from './Event/DomainEventInterface'
export * from './Event/DomainEventService'
export * from './Event/DropboxBackupFailedEvent'
export * from './Event/DropboxBackupFailedEventPayload'
export * from './Event/DuplicateItemSyncedEvent'
export * from './Event/DuplicateItemSyncedEventPayload'
export * from './Event/EmailArchiveExtensionSyncedEvent'
export * from './Event/EmailArchiveExtensionSyncedEventPayload'
export * from './Event/EmailBackupAttachmentCreatedEvent'
export * from './Event/EmailBackupAttachmentCreatedEventPayload'
export * from './Event/EmailBackupRequestedEvent'
export * from './Event/EmailBackupRequestedEventPayload'
export * from './Event/EmailMessageRequestedEvent'
export * from './Event/EmailMessageRequestedEventPayload'
export * from './Event/EmailRequestedEvent'
export * from './Event/EmailRequestedEventPayload'
export * from './Event/ExitDiscountAppliedEvent'
@@ -44,10 +30,6 @@ export * from './Event/FileRemovedEvent'
export * from './Event/FileRemovedEventPayload'
export * from './Event/FileUploadedEvent'
export * from './Event/FileUploadedEventPayload'
export * from './Event/GoogleDriveBackupFailedEvent'
export * from './Event/GoogleDriveBackupFailedEventPayload'
export * from './Event/InvoiceGeneratedEvent'
export * from './Event/InvoiceGeneratedEventPayload'
export * from './Event/ItemDumpedEvent'
export * from './Event/ItemDumpedEventPayload'
export * from './Event/ItemRevisionCreationRequestedEvent'
@@ -62,10 +44,6 @@ export * from './Event/ListedAccountRequestedEvent'
export * from './Event/ListedAccountRequestedEventPayload'
export * from './Event/MuteEmailsSettingChangedEvent'
export * from './Event/MuteEmailsSettingChangedEventPayload'
export * from './Event/OfflineSubscriptionTokenCreatedEvent'
export * from './Event/OfflineSubscriptionTokenCreatedEventPayload'
export * from './Event/OneDriveBackupFailedEvent'
export * from './Event/OneDriveBackupFailedEventPayload'
export * from './Event/PaymentFailedEvent'
export * from './Event/PaymentFailedEventPayload'
export * from './Event/PaymentSuccessEvent'
@@ -74,8 +52,6 @@ export * from './Event/PredicateVerificationRequestedEvent'
export * from './Event/PredicateVerificationRequestedEventPayload'
export * from './Event/PredicateVerifiedEvent'
export * from './Event/PredicateVerifiedEventPayload'
export * from './Event/RefundRequestedEvent'
export * from './Event/RefundRequestedEventPayload'
export * from './Event/RefundProcessedEvent'
export * from './Event/RefundProcessedEventPayload'
export * from './Event/RevisionsCopyRequestedEvent'
@@ -86,16 +62,10 @@ export * from './Event/SharedSubscriptionInvitationCanceledEvent'
export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
export * from './Event/SharedSubscriptionInvitationCreatedEvent'
export * from './Event/SharedSubscriptionInvitationCreatedEventPayload'
export * from './Event/StudentDiscountApprovedEvent'
export * from './Event/StudentDiscountApprovedEventPayload'
export * from './Event/StudentDiscountRequestedEvent'
export * from './Event/StudentDiscountRequestedEventPayload'
export * from './Event/SubscriptionCancelledEvent'
export * from './Event/SubscriptionCancelledEventPayload'
export * from './Event/SubscriptionPurchasedEvent'
export * from './Event/SubscriptionPurchasedEventPayload'
export * from './Event/SubscriptionRateAdjustedEvent'
export * from './Event/SubscriptionRateAdjustedEventPayload'
export * from './Event/SubscriptionReactivatedEvent'
export * from './Event/SubscriptionReactivatedEventPayload'
export * from './Event/SubscriptionReassignedEvent'
@@ -120,8 +90,6 @@ export * from './Event/UserRegisteredEvent'
export * from './Event/UserRegisteredEventPayload'
export * from './Event/UserRolesChangedEvent'
export * from './Event/UserRolesChangedEventPayload'
export * from './Event/UserSignedInEvent'
export * from './Event/UserSignedInEventPayload'
export * from './Event/WebSocketMessageRequestedEvent'
export * from './Event/WebSocketMessageRequestedEventPayload'
export * from './Event/WorkspaceInviteAcceptedEvent'

View File

@@ -3,6 +3,57 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.6.48](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.47...@standardnotes/event-store@1.6.48) (2022-12-09)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.47](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.46...@standardnotes/event-store@1.6.47) (2022-12-09)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.46](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.45...@standardnotes/event-store@1.6.46) (2022-12-09)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.45](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.44...@standardnotes/event-store@1.6.45) (2022-12-09)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.44](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.43...@standardnotes/event-store@1.6.44) (2022-12-09)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.43](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.42...@standardnotes/event-store@1.6.43) (2022-12-08)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.42](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.41...@standardnotes/event-store@1.6.42) (2022-12-08)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.41](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.40...@standardnotes/event-store@1.6.41) (2022-12-08)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.40](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.39...@standardnotes/event-store@1.6.40) (2022-12-08)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.39](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.38...@standardnotes/event-store@1.6.39) (2022-12-08)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.38](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.37...@standardnotes/event-store@1.6.38) (2022-12-07)
### Bug Fixes
* **event-store:** add email requested subscription ([eff0945](https://github.com/standardnotes/server/commit/eff09454c3a28b0124b74c2850fed19313b9e2b2))
* **event-store:** reduce handlers ([473feba](https://github.com/standardnotes/server/commit/473feba6a8f008c9d73238be82e1d197082464c0))
## [1.6.37](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.36...@standardnotes/event-store@1.6.37) (2022-12-07)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.36](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.35...@standardnotes/event-store@1.6.36) (2022-12-07)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.6.36",
"version": "1.6.48",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

View File

@@ -78,7 +78,7 @@ export class ContainerConfigLoader {
['LISTED_ACCOUNT_REQUESTED', container.get(TYPES.EventHandler)],
['LISTED_ACCOUNT_CREATED', container.get(TYPES.EventHandler)],
['LISTED_ACCOUNT_DELETED', container.get(TYPES.EventHandler)],
['USER_SIGNED_IN', container.get(TYPES.EventHandler)],
['EMAIL_REQUESTED', container.get(TYPES.EventHandler)],
['SHARED_SUBSCRIPTION_INVITATION_CREATED', container.get(TYPES.EventHandler)],
['EMAIL_BACKUP_ATTACHMENT_CREATED', container.get(TYPES.EventHandler)],
['EMAIL_BACKUP_REQUESTED', container.get(TYPES.EventHandler)],

View File

@@ -3,6 +3,50 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.8.47](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.46...@standardnotes/files-server@1.8.47) (2022-12-09)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.46](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.45...@standardnotes/files-server@1.8.46) (2022-12-09)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.45](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.44...@standardnotes/files-server@1.8.45) (2022-12-09)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.44](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.43...@standardnotes/files-server@1.8.44) (2022-12-09)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.43](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.42...@standardnotes/files-server@1.8.43) (2022-12-09)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.42](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.41...@standardnotes/files-server@1.8.42) (2022-12-08)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.41](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.40...@standardnotes/files-server@1.8.41) (2022-12-08)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.40](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.39...@standardnotes/files-server@1.8.40) (2022-12-08)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.39](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.38...@standardnotes/files-server@1.8.39) (2022-12-08)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.38](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.37...@standardnotes/files-server@1.8.38) (2022-12-08)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.37](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.36...@standardnotes/files-server@1.8.37) (2022-12-07)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.36](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.35...@standardnotes/files-server@1.8.36) (2022-12-07)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.8.36",
"version": "1.8.47",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,50 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.20](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.19...@standardnotes/revisions-server@1.9.20) (2022-12-09)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.19](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.18...@standardnotes/revisions-server@1.9.19) (2022-12-09)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.18](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.17...@standardnotes/revisions-server@1.9.18) (2022-12-09)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.17](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.16...@standardnotes/revisions-server@1.9.17) (2022-12-09)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.16](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.15...@standardnotes/revisions-server@1.9.16) (2022-12-09)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.15](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.14...@standardnotes/revisions-server@1.9.15) (2022-12-08)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.14](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.13...@standardnotes/revisions-server@1.9.14) (2022-12-08)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.13](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.12...@standardnotes/revisions-server@1.9.13) (2022-12-08)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.12](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.11...@standardnotes/revisions-server@1.9.12) (2022-12-08)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.11](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.10...@standardnotes/revisions-server@1.9.11) (2022-12-08)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.10](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.9...@standardnotes/revisions-server@1.9.10) (2022-12-07)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.9](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.8...@standardnotes/revisions-server@1.9.9) (2022-12-07)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.9.9",
"version": "1.9.20",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,64 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.15.1](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.15.0...@standardnotes/scheduler-server@1.15.1) (2022-12-09)
**Note:** Version bump only for package @standardnotes/scheduler-server
# [1.15.0](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.10...@standardnotes/scheduler-server@1.15.0) (2022-12-09)
### Features
* **syncing-server:** replace one drive backup failed event with email requested ([130f90b](https://github.com/standardnotes/server/commit/130f90bdb6cc88e073b9380e8aed5eebe8c49c1e))
## [1.14.10](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.9...@standardnotes/scheduler-server@1.14.10) (2022-12-09)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.14.9](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.8...@standardnotes/scheduler-server@1.14.9) (2022-12-09)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.14.8](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.7...@standardnotes/scheduler-server@1.14.8) (2022-12-09)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.14.7](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.6...@standardnotes/scheduler-server@1.14.7) (2022-12-08)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.14.6](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.5...@standardnotes/scheduler-server@1.14.6) (2022-12-08)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.14.5](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.4...@standardnotes/scheduler-server@1.14.5) (2022-12-08)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.14.4](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.3...@standardnotes/scheduler-server@1.14.4) (2022-12-08)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.14.3](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.2...@standardnotes/scheduler-server@1.14.3) (2022-12-08)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.14.2](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.1...@standardnotes/scheduler-server@1.14.2) (2022-12-07)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.14.1](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.0...@standardnotes/scheduler-server@1.14.1) (2022-12-07)
### Bug Fixes
* **scheduler:** importing email contents ([c52bb93](https://github.com/standardnotes/server/commit/c52bb93d794447f04d3ea173f0aac9f26e4eba20))
# [1.14.0](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.37...@standardnotes/scheduler-server@1.14.0) (2022-12-07)
### Features
* **scheduler:** add scheduled emails contents ([6e0855f](https://github.com/standardnotes/server/commit/6e0855f9b32c230c9ad5594fb6af6dd460300fc1))
## [1.13.37](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.36...@standardnotes/scheduler-server@1.13.37) (2022-12-07)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -7,4 +7,5 @@ module.exports = {
transform: {
...tsjPreset.transform,
},
coveragePathIgnorePatterns: ['/Bootstrap/', '/Infra/', '/Domain/Email/', '/Domain/Event/'],
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.13.37",
"version": "1.15.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -27,6 +27,7 @@
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",
"@standardnotes/predicates": "workspace:*",

View File

@@ -0,0 +1,9 @@
import { html } from './encourage-email-backups.html'
export function getSubject(): string {
return 'Enable email backups for your account'
}
export function getBody(): string {
return html
}

View File

@@ -0,0 +1,11 @@
import { html } from './encourage-subscription-purchasing.html'
export function getSubject(): string {
return 'Checking in after one month with Standard Notes'
}
export function getBody(registrationDate: string): string {
const body = html
return body.replace('%%REGISTRATION_DATE%%', registrationDate)
}

View File

@@ -0,0 +1,9 @@
import { html } from './exit-interview.html'
export function getSubject(): string {
return 'Can we ask why you canceled?'
}
export function getBody(): string {
return html
}

View File

@@ -0,0 +1,18 @@
export const html = `<div>
<p>
Did you know you can enable daily email backups for your account? This <strong>free</strong> feature sends an
email to your inbox with an encrypted backup file including all your notes and tags.
</p>
<p>
Email backups are an important feature that help protect you against worst-case scenarios. Your backups can be
used to restore your account to a previous state, or to import old versions of notes into your present
account.
</p>
<p>
To enable free email backups, use the Standard Notes web or desktop app, and open Preferences > Backups > Email Backups.
</p>
<a href="https://standardnotes.com/help/28/how-do-i-enable-daily-email-backups">
Learn more about daily email backups →
</a>
</div>`

View File

@@ -0,0 +1,84 @@
export const html = `<div>
<p>Hi there,</p>
<p>
We hope you've been finding great use out of Standard Notes. We built Standard Notes to be a secure place for
your most sensitive notes and files.
</p>
<p>
As a reminder,
<strong>
<em>you signed up for the Standard Notes free plan on %%REGISTRATION_DATE%%</em>
</strong>
Your free account comes with standard features like end-to-end encryption, multiple-device sync, and
two-factor authentication.
</p>
<p>
If you're ready to advance your usage of Standard Notes, we recommend upgrading to one of our more powerful
plans.
</p>
<ul>
<li>
<p>
<strong>Productivity</strong> <strong>($59/year)</strong> powers up your editing experience with unique
and special-built note-types for markdown, rich text, spreadsheets, todo, and more.
</p>
</li>
<li>
<p>
<strong>Professional</strong> <strong>($99/year)</strong> gives you all the power of Productivity plus
100GB of end-to-end encrypted file storage for your private photos, videos, and documents, plus family
subscription sharing with up to 5 people.
</p>
</li>
</ul>
<p>
Professional comes with a 90-day money back guarantee, so if you're not completely satisfied, we're happy to
refund your full purchase amount.
</p>
<p>
<strong>
<a href="https://standardnotes.com/plans">Upgrade your plan →</a>
</strong>
</p>
<p>
<strong>
<a href="https://standardnotes.com/features">Learn more about the features →</a>
</strong>
</p>
<p>
<strong>Questions & Answers</strong>
</p>
<p>
<em>How does Standard Notes compare with conventional note-taking apps?</em>
</p>
<p>
Data you store with Standard Notes is encrypted with end-to-end encryption using a key only you know. Because
of this, we can't read your notes, and neither can anyone else.
</p>
<p>
<em>What kind of notes should I store in Standard Notes?</em>
</p>
<p>
This question can be reframed as: "What shouldn't I store in non-private services?" This would include
sensitive/sensual data related to your health and wellness, secrets and keys, notes and documents with
personally identifiable information that, if leaked, would lead to the theft of your identity, and business,
financial, or legal information which cover non-public or confidential information.
</p>
<p>
<em>Where can I access my notes?</em>
</p>
<p>
Providing you with easy access to your notes and files on all your devices is a key focus for us. We provide
secure and well-designed applications for your web browser, desktop (macOS, Windows, Linux,) and mobile
(Android and iOS).
</p>
<p>
<em>I have more questions.</em>
</p>
<p>
We love questions. Head over to our Help page to see if your question is answered there. If not, reply
directly to this email or send an email to <a href="help@standardnotes.com">help@standardnotes.com</a> and
we'd be happy to help.
</p>
</div>
`

View File

@@ -0,0 +1,29 @@
export const html = `<div>
<p>
We're truly sad to see you leave. Our mission is simple: build the best, most private, and most secure
note-taking app available. It's clear we've fallen short of your expectations somewhere along the way.
</p>
<p>
We just want you to know—if price was the reason you canceled, we're not willing to lose you. That's no issue
for us and we're happy to work out something that fits better with your budget. If price is your primary
concern, please click the link below, and we'll get in touch with some options.
</p>
<a href="https://app.standardnotes.com/?user-request=exit-discount">Apply For A Limited Discount Offer →</a>
<p>
If you canceled for another reason, such as a missing feature, or a feature that wasn't behaving or working as
you expected, please let us know! We build this product for you, and feedback from customers like yourself who
are willing to pay for a product is most crucial for us as we continue to evolve and iterate on Standard
Notes.
</p>
<p>If you have a minute, please fill out this brief exit interview: </p>
<a href="https://standardnotes.typeform.com/to/dX5lzPtm">Short Exit Interview →</a>
<p>
Our team reads every single response, and your feedback will be shared with the relevant department within our
team.
</p>
<p>
If you have any other thoughts or questions, please feel free to reply directly to this email, and a member of
our support team will be in touch with you.
</p>
</div>
`

View File

@@ -1,223 +0,0 @@
import 'reflect-metadata'
import { EmailMessageIdentifier } from '@standardnotes/common'
import { TimerInterface } from '@standardnotes/time'
import { DomainEventFactory } from './DomainEventFactory'
import { PredicateAuthority, PredicateName } from '@standardnotes/predicates'
import { Job } from '../Job/Job'
import { Predicate } from '../Predicate/Predicate'
describe('DomainEventFactory', () => {
let timer: TimerInterface
const createFactory = () => new DomainEventFactory(timer)
beforeEach(() => {
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
})
it('should create a DISCOUNT_APPLY_REQUESTED event', () => {
expect(
createFactory().createDiscountApplyRequestedEvent({
userEmail: 'test@test.te',
discountCode: 'off-10',
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: 'test@test.te',
userIdentifierType: 'email',
},
origin: 'scheduler',
},
payload: {
userEmail: 'test@test.te',
discountCode: 'off-10',
},
type: 'DISCOUNT_APPLY_REQUESTED',
})
})
it('should create a DISCOUNT_WITHDRAW_REQUESTED event', () => {
expect(
createFactory().createDiscountWithdrawRequestedEvent({
userEmail: 'test@test.te',
discountCode: 'off-10',
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: 'test@test.te',
userIdentifierType: 'email',
},
origin: 'scheduler',
},
payload: {
userEmail: 'test@test.te',
discountCode: 'off-10',
},
type: 'DISCOUNT_WITHDRAW_REQUESTED',
})
})
it('should create a EXIT_DISCOUNT_WITHDRAW_REQUESTED event', () => {
expect(
createFactory().createExitDiscountWithdrawRequestedEvent({
userEmail: 'test@test.te',
discountCode: 'exit-20',
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: 'test@test.te',
userIdentifierType: 'email',
},
origin: 'scheduler',
},
payload: {
userEmail: 'test@test.te',
discountCode: 'exit-20',
},
type: 'EXIT_DISCOUNT_WITHDRAW_REQUESTED',
})
})
it('should create a EMAIL_MESSAGE_REQUESTED event', () => {
expect(
createFactory().createEmailMessageRequestedEvent({
userEmail: 'test@test.te',
messageIdentifier: EmailMessageIdentifier.ENCOURAGE_EMAIL_BACKUPS,
context: {
foo: 'bar',
},
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: 'test@test.te',
userIdentifierType: 'email',
},
origin: 'scheduler',
},
payload: {
messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
userEmail: 'test@test.te',
context: {
foo: 'bar',
},
},
type: 'EMAIL_MESSAGE_REQUESTED',
})
})
it('should create a PREDICATE_VERIFICATION_REQUESTED event dedicated for auth', () => {
expect(
createFactory().createPredicateVerificationRequestedEvent(
{
uuid: '1-2-3',
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
} as jest.Mocked<Job>,
{
authority: PredicateAuthority.Auth,
name: PredicateName.EmailBackupsEnabled,
status: 'pending',
} as jest.Mocked<Predicate>,
),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
},
origin: 'scheduler',
target: 'auth',
},
payload: {
predicate: {
authority: 'auth',
jobUuid: '1-2-3',
name: 'email-backups-enabled',
},
},
type: 'PREDICATE_VERIFICATION_REQUESTED',
})
})
it('should create a PREDICATE_VERIFICATION_REQUESTED event dedicated for syncing server', () => {
expect(
createFactory().createPredicateVerificationRequestedEvent(
{
uuid: '1-2-3',
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
} as jest.Mocked<Job>,
{
authority: PredicateAuthority.SyncingServer,
name: PredicateName.EmailBackupsEnabled,
status: 'pending',
} as jest.Mocked<Predicate>,
),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
},
origin: 'scheduler',
target: 'syncing-server',
},
payload: {
predicate: {
authority: 'syncing-server',
jobUuid: '1-2-3',
name: 'email-backups-enabled',
},
},
type: 'PREDICATE_VERIFICATION_REQUESTED',
})
})
it('should create a PREDICATE_VERIFICATION_REQUESTED event dedicated for unknown target', () => {
expect(
createFactory().createPredicateVerificationRequestedEvent(
{
uuid: '1-2-3',
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
} as jest.Mocked<Job>,
{
authority: 'foobar' as PredicateAuthority,
name: PredicateName.EmailBackupsEnabled,
status: 'pending',
} as jest.Mocked<Predicate>,
),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
},
origin: 'scheduler',
},
payload: {
predicate: {
authority: 'foobar',
jobUuid: '1-2-3',
name: 'email-backups-enabled',
},
},
type: 'PREDICATE_VERIFICATION_REQUESTED',
})
})
})

View File

@@ -1,9 +1,8 @@
import { EmailMessageIdentifier } from '@standardnotes/common'
import {
DiscountApplyRequestedEvent,
DiscountWithdrawRequestedEvent,
DomainEventService,
EmailMessageRequestedEvent,
EmailRequestedEvent,
ExitDiscountWithdrawRequestedEvent,
PredicateVerificationRequestedEvent,
} from '@standardnotes/domain-events'
@@ -70,13 +69,15 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
}
}
createEmailMessageRequestedEvent(dto: {
createEmailRequestedEvent(dto: {
userEmail: string
messageIdentifier: EmailMessageIdentifier
context: Record<string, unknown>
}): EmailMessageRequestedEvent {
messageIdentifier: string
level: string
body: string
subject: string
}): EmailRequestedEvent {
return {
type: 'EMAIL_MESSAGE_REQUESTED',
type: 'EMAIL_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {

View File

@@ -1,8 +1,7 @@
import { EmailMessageIdentifier } from '@standardnotes/common'
import {
DiscountApplyRequestedEvent,
DiscountWithdrawRequestedEvent,
EmailMessageRequestedEvent,
EmailRequestedEvent,
ExitDiscountWithdrawRequestedEvent,
PredicateVerificationRequestedEvent,
} from '@standardnotes/domain-events'
@@ -12,11 +11,13 @@ import { Predicate } from '../Predicate/Predicate'
export interface DomainEventFactoryInterface {
createPredicateVerificationRequestedEvent(job: Job, predicate: Predicate): PredicateVerificationRequestedEvent
createEmailMessageRequestedEvent(dto: {
createEmailRequestedEvent(dto: {
userEmail: string
messageIdentifier: EmailMessageIdentifier
context: Record<string, unknown>
}): EmailMessageRequestedEvent
messageIdentifier: string
level: string
body: string
subject: string
}): EmailRequestedEvent
createDiscountApplyRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountApplyRequestedEvent
createDiscountWithdrawRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountWithdrawRequestedEvent
createExitDiscountWithdrawRequestedEvent(dto: {

View File

@@ -2,10 +2,11 @@ import {
DiscountApplyRequestedEvent,
DiscountWithdrawRequestedEvent,
DomainEventPublisherInterface,
EmailMessageRequestedEvent,
EmailRequestedEvent,
ExitDiscountWithdrawRequestedEvent,
} from '@standardnotes/domain-events'
import { PredicateName } from '@standardnotes/predicates'
import { TimerInterface } from '@standardnotes/time'
import 'reflect-metadata'
import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
@@ -26,13 +27,17 @@ describe('JobDoneInterpreter', () => {
let domainEventPublisher: DomainEventPublisherInterface
let job: Job
let logger: Logger
let timer: TimerInterface
const createInterpreter = () =>
new JobDoneInterpreter(jobRepository, predicateRepository, domainEventFactory, domainEventPublisher, logger)
new JobDoneInterpreter(jobRepository, predicateRepository, domainEventFactory, domainEventPublisher, timer, logger)
beforeEach(() => {
job = {} as jest.Mocked<Job>
timer = {} as jest.Mocked<TimerInterface>
timer.convertMicrosecondsToDate = jest.fn().mockReturnValue(new Date())
jobRepository = {} as jest.Mocked<JobRepositoryInterface>
jobRepository.findOneByUuid = jest.fn().mockReturnValue(job)
@@ -40,9 +45,7 @@ describe('JobDoneInterpreter', () => {
predicateRepository.findByJobUuid = jest.fn().mockReturnValue([])
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createEmailMessageRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<EmailMessageRequestedEvent>)
domainEventFactory.createEmailRequestedEvent = jest.fn().mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
domainEventFactory.createDiscountApplyRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<DiscountApplyRequestedEvent>)
@@ -89,11 +92,7 @@ describe('JobDoneInterpreter', () => {
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createEmailMessageRequestedEvent).toHaveBeenCalledWith({
context: {},
messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
userEmail: 'test@test.te',
})
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
@@ -111,7 +110,7 @@ describe('JobDoneInterpreter', () => {
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
@@ -124,7 +123,7 @@ describe('JobDoneInterpreter', () => {
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
@@ -143,11 +142,7 @@ describe('JobDoneInterpreter', () => {
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createEmailMessageRequestedEvent).toHaveBeenCalledWith({
context: { userRegisteredAt: 123 },
messageIdentifier: 'ENCOURAGE_SUBSCRIPTION_PURCHASING',
userEmail: 'test@test.te',
})
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
@@ -160,7 +155,7 @@ describe('JobDoneInterpreter', () => {
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
@@ -173,11 +168,7 @@ describe('JobDoneInterpreter', () => {
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createEmailMessageRequestedEvent).toHaveBeenCalledWith({
context: {},
messageIdentifier: 'EXIT_INTERVIEW',
userEmail: 'test@test.te',
})
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
@@ -190,7 +181,7 @@ describe('JobDoneInterpreter', () => {
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
@@ -295,7 +286,7 @@ describe('JobDoneInterpreter', () => {
await createInterpreter().interpret('1-2-3')
expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
})

View File

@@ -1,8 +1,9 @@
import { EmailMessageIdentifier } from '@standardnotes/common'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { PredicateName } from '@standardnotes/predicates'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { EmailLevel } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import TYPES from '../../Bootstrap/Types'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
@@ -13,6 +14,15 @@ import { Job } from './Job'
import { JobDoneInterpreterInterface } from './JobDoneInterpreterInterface'
import { JobName } from './JobName'
import { JobRepositoryInterface } from './JobRepositoryInterface'
import { getSubject as getExitInterviewSubject, getBody as getExitInterviewBody } from '../Email/ExitInterview'
import {
getSubject as getEncourageEmailBackupsSubject,
getBody as getEncourageEmailBackupsBody,
} from '../Email/EncourageEmailBackups'
import {
getSubject as getEncourageSubscriptionPurchasingSubject,
getBody as getEncourageSubscriptionPurchasingBody,
} from '../Email/EncourageSubscriptionPurchasing'
@injectable()
export class JobDoneInterpreter implements JobDoneInterpreterInterface {
@@ -21,6 +31,7 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
@inject(TYPES.PredicateRepository) private predicateRepository: PredicateRepositoryInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
@@ -81,10 +92,12 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
this.logger.debug(`[${job.uuid}]${job.name}: requesting email backup encouragement email.`)
await this.domainEventPublisher.publish(
this.domainEventFactory.createEmailMessageRequestedEvent({
this.domainEventFactory.createEmailRequestedEvent({
userEmail: job.userIdentifier,
messageIdentifier: EmailMessageIdentifier.ENCOURAGE_EMAIL_BACKUPS,
context: {},
messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
subject: getEncourageEmailBackupsSubject(),
body: getEncourageEmailBackupsBody(),
level: EmailLevel.LEVELS.System,
}),
)
}
@@ -93,12 +106,14 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
this.logger.debug(`[${job.uuid}]${job.name}: requesting subscription purchase encouragement email.`)
await this.domainEventPublisher.publish(
this.domainEventFactory.createEmailMessageRequestedEvent({
this.domainEventFactory.createEmailRequestedEvent({
userEmail: job.userIdentifier,
messageIdentifier: EmailMessageIdentifier.ENCOURAGE_SUBSCRIPTION_PURCHASING,
context: {
userRegisteredAt: job.createdAt,
},
messageIdentifier: 'ENCOURAGE_SUBSCRIPTION_PURCHASING',
subject: getEncourageSubscriptionPurchasingSubject(),
body: getEncourageSubscriptionPurchasingBody(
this.timer.convertMicrosecondsToDate(job.createdAt).toLocaleString(),
),
level: EmailLevel.LEVELS.System,
}),
)
}
@@ -107,10 +122,12 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
this.logger.debug(`[${job.uuid}]${job.name}: requesting exit interview email.`)
await this.domainEventPublisher.publish(
this.domainEventFactory.createEmailMessageRequestedEvent({
this.domainEventFactory.createEmailRequestedEvent({
userEmail: job.userIdentifier,
messageIdentifier: EmailMessageIdentifier.EXIT_INTERVIEW,
context: {},
messageIdentifier: 'EXIT_INTERVIEW',
subject: getExitInterviewSubject(),
body: getExitInterviewBody(),
level: EmailLevel.LEVELS.System,
}),
)
}

View File

@@ -3,6 +3,62 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.24.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.23.0...@standardnotes/syncing-server@1.24.0) (2022-12-09)
### Features
* **syncing-server:** replace email backup attachment created with email requested ([32601f3](https://github.com/standardnotes/syncing-server-js/commit/32601f34f181b29b7c62cd2926111a0887d97fbf))
# [1.23.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.22.0...@standardnotes/syncing-server@1.23.0) (2022-12-09)
### Features
* **syncing-server:** replace one drive backup failed event with email requested ([130f90b](https://github.com/standardnotes/syncing-server-js/commit/130f90bdb6cc88e073b9380e8aed5eebe8c49c1e))
# [1.22.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.21.0...@standardnotes/syncing-server@1.22.0) (2022-12-09)
### Features
* **syncing-serfver:** remove dropbox backup failed event in favour of email requested ([118156c](https://github.com/standardnotes/syncing-server-js/commit/118156c62de70eca8fd89414f6e409abd0363e62))
# [1.21.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.17...@standardnotes/syncing-server@1.21.0) (2022-12-09)
### Features
* **syncing-server:** remove google drive backup failed event in favour of email requested ([00fe321](https://github.com/standardnotes/syncing-server-js/commit/00fe32136e7add627e58e8ea223f7f484f1d3718))
## [1.20.17](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.16...@standardnotes/syncing-server@1.20.17) (2022-12-09)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.20.16](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.15...@standardnotes/syncing-server@1.20.16) (2022-12-09)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.20.15](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.14...@standardnotes/syncing-server@1.20.15) (2022-12-08)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.20.14](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.13...@standardnotes/syncing-server@1.20.14) (2022-12-08)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.20.13](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.12...@standardnotes/syncing-server@1.20.13) (2022-12-08)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.20.12](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.11...@standardnotes/syncing-server@1.20.12) (2022-12-08)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.20.11](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.10...@standardnotes/syncing-server@1.20.11) (2022-12-08)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.20.10](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.9...@standardnotes/syncing-server@1.20.10) (2022-12-07)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.20.9](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.20.8...@standardnotes/syncing-server@1.20.9) (2022-12-07)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -7,6 +7,6 @@ module.exports = {
transform: {
...tsjPreset.transform,
},
coveragePathIgnorePatterns: ['/Bootstrap/', 'HealthCheckController', '/Infra/'],
coveragePathIgnorePatterns: ['/Bootstrap/', 'HealthCheckController', '/Infra/', '/Domain/Email/'],
setupFilesAfterEnv: ['./test-setup.ts'],
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.20.9",
"version": "1.24.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -0,0 +1,9 @@
import { html } from './dropbox-backup-failed.html'
export function getSubject(): string {
return 'Failed Daily Backup to Dropbox'
}
export function getBody(): string {
return html
}

View File

@@ -0,0 +1,14 @@
import { html } from './email-backup-attachment-created.html'
export function getSubject(fileIndex: number, numberOfFiles: number, date: string): string {
let subject = `Data Backup for ${date}`
if (numberOfFiles > 1) {
subject = `Data Backup for ${date} - Part ${fileIndex} Of ${numberOfFiles}`
}
return subject
}
export function getBody(email: string): string {
return html(email)
}

View File

@@ -0,0 +1,9 @@
import { html } from './google-drive-backup-failed.html'
export function getSubject(): string {
return 'Failed Daily Backup to Google Drive Sync'
}
export function getBody(): string {
return html
}

View File

@@ -0,0 +1,9 @@
import { html } from './one-drive-backup-failed.html'
export function getSubject(): string {
return 'Failed Daily Backup to OneDrive Sync'
}
export function getBody(): string {
return html
}

View File

@@ -0,0 +1,17 @@
export const html = `<p>Hello,</p>
<p>We recently tried backing up your data to <strong>Dropbox</strong>, but an issue prevented us from doing so.</p>
<p>
The usual cause is an expired or revoked token from your sync provider. Please follow
<a href='https://standardnotes.com/help/27/how-do-i-enable-dropbox-google-drive-or-onedrive-backups'>these
instructions</a>
to use CloudLink on the web or desktop Standard Notes application to uninstall then reinstall this sync provider.
</p>
<p>
We apologize for any inconvenience this may cause.
If you have any questions, please feel free to reply directly to this email.
</p>
<p>
Thanks,
<br>SN</br>
</p>
<a href='https://app.standardnotes.com/?settings=backups'>Mute these emails</a>`

View File

@@ -0,0 +1,33 @@
export const html = (email: string) => `
<p>
Your encrypted data backup is attached for ${email}. You can import this file using
the Standard Notes web or desktop app, or by using the offline decryption script available at
<a style="text-decoration:none !important; text-decoration:none;">standardnotes.org/offline</a>.
</p>
<p>
<strong>Please note:</strong>
<ol>
<li>
We will never send anything other than a <code>txt</code> file
as part of your daily backups. To protect yourself against phishing attacks, never open
any other kind of file, and always open the <code>txt</code> file with a text editor to
verify its contents before decrypting.
</li>
<li>
We will never include clickable links in this email. Instead, manually verify
and copy/paste the offline link above in your browser.
</li>
</ol>
</p>
<hr />
<p>
<i>
Want to disable daily backups? Uninstall 'Daily Email Backups' from your Extensions
menu in Standard Notes to immediately disable backups.
Otherwise, reply to this email with "Stop". Note that it may
take up to 72 hours or more to perform manual removal via the "Stop" method.
</i>
</p>
`

View File

@@ -0,0 +1,19 @@
export const html = `<p>Hello,</p>
<p>We recently tried backing up your data to <strong>Google Drive Sync</strong>, but an issue prevented us from
doing
so.</p>
<p>
The usual cause is an expired or revoked token from your sync provider. Please follow
<a href='https://standardnotes.com/help/27/how-do-i-enable-dropbox-google-drive-or-onedrive-backups'>these
instructions</a>
to use CloudLink on the web or desktop Standard Notes application to uninstall then reinstall this sync provider.
</p>
<p>
We apologize for any inconvenience this may cause.
If you have any questions, please feel free to reply directly to this email.
</p>
<p>
Thanks,
<br>SN</br>
</p>
<a href='https://app.standardnotes.com/?settings=backups'>Mute these emails</a>`

View File

@@ -0,0 +1,18 @@
export const html = `<p>Hello,</p>
<p>We recently tried backing up your data to <strong>OneDrive Sync</strong>, but an issue prevented us from doing
so.</p>
<p>
The usual cause is an expired or revoked token from your sync provider. Please follow
<a href='https://standardnotes.com/help/27/how-do-i-enable-dropbox-google-drive-or-onedrive-backups'>these
instructions</a>
to use CloudLink on the web or desktop Standard Notes application to uninstall then reinstall this sync provider.
</p>
<p>
We apologize for any inconvenience this may cause.
If you have any questions, please feel free to reply directly to this email.
</p>
<p>
Thanks,
<br>SN</br>
</p>
<a href='https://app.standardnotes.com/?settings=backups'>Mute these emails</a>`

View File

@@ -1,15 +1,12 @@
/* istanbul ignore file */
import {
DomainEventService,
DropboxBackupFailedEvent,
DuplicateItemSyncedEvent,
EmailArchiveExtensionSyncedEvent,
EmailBackupAttachmentCreatedEvent,
GoogleDriveBackupFailedEvent,
EmailRequestedEvent,
ItemDumpedEvent,
ItemRevisionCreationRequestedEvent,
ItemsSyncedEvent,
OneDriveBackupFailedEvent,
RevisionsCopyRequestedEvent,
RevisionsOwnershipUpdateRequestedEvent,
UserContentSizeRecalculationRequestedEvent,
@@ -131,57 +128,31 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
}
}
createDropboxBackupFailedEvent(muteCloudEmailsSettingUuid: string, email: string): DropboxBackupFailedEvent {
createEmailRequestedEvent(dto: {
userEmail: string
messageIdentifier: string
level: string
body: string
subject: string
sender?: string
attachments?: Array<{
filePath: string
fileName: string
attachmentFileName: string
attachmentContentType: string
}>
}): EmailRequestedEvent {
return {
type: 'DROPBOX_BACKUP_FAILED',
type: 'EMAIL_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: email,
userIdentifier: dto.userEmail,
userIdentifierType: 'email',
},
origin: DomainEventService.SyncingServer,
},
payload: {
muteCloudEmailsSettingUuid,
email,
},
}
}
createGoogleDriveBackupFailedEvent(muteCloudEmailsSettingUuid: string, email: string): GoogleDriveBackupFailedEvent {
return {
type: 'GOOGLE_DRIVE_BACKUP_FAILED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: email,
userIdentifierType: 'email',
},
origin: DomainEventService.SyncingServer,
},
payload: {
muteCloudEmailsSettingUuid,
email,
},
}
}
createOneDriveBackupFailedEvent(muteCloudEmailsSettingUuid: string, email: string): OneDriveBackupFailedEvent {
return {
type: 'ONE_DRIVE_BACKUP_FAILED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: email,
userIdentifierType: 'email',
},
origin: DomainEventService.SyncingServer,
},
payload: {
muteCloudEmailsSettingUuid,
email,
},
payload: dto,
}
}
@@ -225,24 +196,4 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
},
}
}
createEmailBackupAttachmentCreatedEvent(dto: {
backupFileName: string
backupFileIndex: number
backupFilesTotal: number
email: string
}): EmailBackupAttachmentCreatedEvent {
return {
type: 'EMAIL_BACKUP_ATTACHMENT_CREATED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.email,
userIdentifierType: 'email',
},
origin: DomainEventService.SyncingServer,
},
payload: dto,
}
}
}

View File

@@ -1,13 +1,10 @@
import {
DropboxBackupFailedEvent,
DuplicateItemSyncedEvent,
EmailArchiveExtensionSyncedEvent,
EmailBackupAttachmentCreatedEvent,
GoogleDriveBackupFailedEvent,
EmailRequestedEvent,
ItemDumpedEvent,
ItemRevisionCreationRequestedEvent,
ItemsSyncedEvent,
OneDriveBackupFailedEvent,
RevisionsCopyRequestedEvent,
RevisionsOwnershipUpdateRequestedEvent,
UserContentSizeRecalculationRequestedEvent,
@@ -15,9 +12,20 @@ import {
export interface DomainEventFactoryInterface {
createUserContentSizeRecalculationRequestedEvent(userUuid: string): UserContentSizeRecalculationRequestedEvent
createDropboxBackupFailedEvent(muteCloudEmailsSettingUuid: string, email: string): DropboxBackupFailedEvent
createGoogleDriveBackupFailedEvent(muteCloudEmailsSettingUuid: string, email: string): GoogleDriveBackupFailedEvent
createOneDriveBackupFailedEvent(muteCloudEmailsSettingUuid: string, email: string): OneDriveBackupFailedEvent
createEmailRequestedEvent(dto: {
userEmail: string
messageIdentifier: string
level: string
body: string
subject: string
sender?: string
attachments?: Array<{
filePath: string
fileName: string
attachmentFileName: string
attachmentContentType: string
}>
}): EmailRequestedEvent
createItemsSyncedEvent(dto: {
userUuid: string
extensionUrl: string
@@ -28,12 +36,6 @@ export interface DomainEventFactoryInterface {
source: 'account-deletion' | 'realtime-extensions-sync'
}): ItemsSyncedEvent
createEmailArchiveExtensionSyncedEvent(userUuid: string, extensionId: string): EmailArchiveExtensionSyncedEvent
createEmailBackupAttachmentCreatedEvent(dto: {
backupFileName: string
backupFileIndex: number
backupFilesTotal: number
email: string
}): EmailBackupAttachmentCreatedEvent
createDuplicateItemSyncedEvent(itemUuid: string, userUuid: string): DuplicateItemSyncedEvent
createItemRevisionCreationRequested(itemUuid: string, userUuid: string): ItemRevisionCreationRequestedEvent
createItemDumpedEvent(fileDumpPath: string, userUuid: string): ItemDumpedEvent

View File

@@ -50,9 +50,7 @@ describe('ExtensionsHttpService', () => {
domainEventPublisher.publish = jest.fn()
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createDropboxBackupFailedEvent = jest.fn()
domainEventFactory.createGoogleDriveBackupFailedEvent = jest.fn()
domainEventFactory.createOneDriveBackupFailedEvent = jest.fn()
domainEventFactory.createEmailRequestedEvent = jest.fn()
contentDecoder = {} as jest.Mocked<ContentDecoderInterface>
contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Dropbox' })
@@ -65,7 +63,6 @@ describe('ExtensionsHttpService', () => {
forceMute: false,
backupFilename: 'test',
authParams,
muteEmailsSettingUuid: '3-4-5',
cloudProvider: 'DROPBOX',
})
@@ -73,7 +70,6 @@ describe('ExtensionsHttpService', () => {
data: {
auth_params: authParams,
backup_filename: 'test',
settings_id: '3-4-5',
silent: false,
user_uuid: '1-2-3',
},
@@ -99,12 +95,11 @@ describe('ExtensionsHttpService', () => {
forceMute: false,
backupFilename: 'test',
authParams,
muteEmailsSettingUuid: '3-4-5',
cloudProvider: 'DROPBOX',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createDropboxBackupFailedEvent).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should send items to extensions server', async () => {
@@ -116,7 +111,6 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: '',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
expect(httpClient.request).toHaveBeenCalledWith({
@@ -124,7 +118,6 @@ describe('ExtensionsHttpService', () => {
auth_params: authParams,
backup_filename: '',
items: [item],
settings_id: '3-4-5',
silent: false,
user_uuid: '1-2-3',
},
@@ -145,14 +138,12 @@ describe('ExtensionsHttpService', () => {
forceMute: false,
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
expect(httpClient.request).toHaveBeenCalledWith({
data: {
auth_params: authParams,
backup_filename: 'backup-file',
settings_id: '3-4-5',
silent: false,
user_uuid: '1-2-3',
},
@@ -180,11 +171,10 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createDropboxBackupFailedEvent).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should publish a failed Dropbox backup event if request was sent and extensions server responded not ok', async () => {
@@ -200,11 +190,10 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createDropboxBackupFailedEvent).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should publish a failed Google Drive backup event if request was not sent successfully', async () => {
@@ -222,11 +211,10 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createGoogleDriveBackupFailedEvent).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should publish a failed One Drive backup event if request was not sent successfully', async () => {
@@ -244,11 +232,10 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createOneDriveBackupFailedEvent).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should not publish a failed backup event if emailes are force muted', async () => {
@@ -266,7 +253,6 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
@@ -289,7 +275,6 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
} catch (e) {
error = e
@@ -316,7 +301,6 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
} catch (e) {
error = e
@@ -340,11 +324,10 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createDropboxBackupFailedEvent).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should publish a failed Google Drive backup event judging by extension url if request was not sent successfully', async () => {
@@ -362,11 +345,10 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createGoogleDriveBackupFailedEvent).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should publish a failed One Drive backup event judging by extension url if request was not sent successfully', async () => {
@@ -384,11 +366,10 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createOneDriveBackupFailedEvent).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should throw an error if cannot deduce extension by judging from the url', async () => {
@@ -408,7 +389,6 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
} catch (e) {
error = e
@@ -434,7 +414,6 @@ describe('ExtensionsHttpService', () => {
items: [item],
backupFilename: 'backup-file',
authParams,
muteEmailsSettingUuid: '3-4-5',
})
} catch (e) {
error = e

View File

@@ -1,5 +1,6 @@
import { KeyParamsData } from '@standardnotes/responses'
import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { EmailLevel } from '@standardnotes/domain-core'
import { AxiosInstance } from 'axios'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
@@ -10,6 +11,9 @@ import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
import { ExtensionName } from './ExtensionName'
import { ExtensionsHttpServiceInterface } from './ExtensionsHttpServiceInterface'
import { SendItemsToExtensionsServerDTO } from './SendItemsToExtensionsServerDTO'
import { getBody as googleDriveBody, getSubject as googleDriveSubject } from '../Email/GoogleDriveBackupFailed'
import { getBody as dropboxBody, getSubject as dropboxSubject } from '../Email/DropboxBackupFailed'
import { getBody as oneDriveBody, getSubject as oneDriveSubject } from '../Email/OneDriveBackupFailed'
@injectable()
export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
@@ -29,7 +33,6 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
authParams: KeyParamsData
forceMute: boolean
userUuid: string
muteEmailsSettingUuid: string
}): Promise<void> {
let sent = false
try {
@@ -38,7 +41,6 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
auth_params: dto.authParams,
silent: dto.forceMute,
user_uuid: dto.userUuid,
settings_id: dto.muteEmailsSettingUuid,
}
const response = await this.httpClient.request({
@@ -58,13 +60,9 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
this.logger.error(`[${dto.userUuid}] Failed to send a request to extensions server: ${(error as Error).message}`)
}
if (!sent && !dto.forceMute && dto.muteEmailsSettingUuid !== undefined) {
if (!sent && !dto.forceMute) {
await this.domainEventPublisher.publish(
this.createCloudBackupFailedEventBasedOnProvider(
dto.cloudProvider,
dto.authParams.identifier as string,
dto.muteEmailsSettingUuid,
),
this.createCloudBackupFailedEventBasedOnProvider(dto.cloudProvider, dto.authParams.identifier as string),
)
}
}
@@ -77,7 +75,6 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
auth_params: dto.authParams,
silent: dto.forceMute,
user_uuid: dto.userUuid,
settings_id: dto.muteEmailsSettingUuid,
}
if (dto.items !== undefined) {
payload.items = dto.items
@@ -100,14 +97,9 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
this.logger.error(`[${dto.userUuid}] Failed to send a request to extensions server: ${(error as Error).message}`)
}
if (!sent && !dto.forceMute && dto.muteEmailsSettingUuid !== undefined) {
if (!sent && !dto.forceMute) {
await this.domainEventPublisher.publish(
await this.getBackupFailedEvent(
dto.muteEmailsSettingUuid,
dto.extensionId,
dto.userUuid,
dto.authParams.identifier as string,
),
await this.getBackupFailedEvent(dto.extensionId, dto.userUuid, dto.authParams.identifier as string),
)
}
}
@@ -115,20 +107,36 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
private createCloudBackupFailedEventBasedOnProvider(
cloudProvider: 'DROPBOX' | 'GOOGLE_DRIVE' | 'ONE_DRIVE',
email: string,
muteCloudEmailsSettingUuid: string,
): DomainEventInterface {
switch (cloudProvider) {
case 'DROPBOX':
return this.domainEventFactory.createDropboxBackupFailedEvent(muteCloudEmailsSettingUuid, email)
return this.domainEventFactory.createEmailRequestedEvent({
userEmail: email,
level: EmailLevel.LEVELS.FailedCloudBackup,
body: dropboxBody(),
messageIdentifier: 'FAILED_DROPBOX_BACKUP',
subject: dropboxSubject(),
})
case 'GOOGLE_DRIVE':
return this.domainEventFactory.createGoogleDriveBackupFailedEvent(muteCloudEmailsSettingUuid, email)
return this.domainEventFactory.createEmailRequestedEvent({
userEmail: email,
level: EmailLevel.LEVELS.FailedCloudBackup,
body: googleDriveBody(),
messageIdentifier: 'FAILED_GOOGLE_DRIVE_BACKUP',
subject: googleDriveSubject(),
})
case 'ONE_DRIVE':
return this.domainEventFactory.createOneDriveBackupFailedEvent(muteCloudEmailsSettingUuid, email)
return this.domainEventFactory.createEmailRequestedEvent({
userEmail: email,
level: EmailLevel.LEVELS.FailedCloudBackup,
body: oneDriveBody(),
messageIdentifier: 'FAILED_ONE_DRIVE_BACKUP',
subject: oneDriveSubject(),
})
}
}
private async getBackupFailedEvent(
muteCloudEmailsSettingUuid: string,
extensionId: string,
userUuid: string,
email: string,
@@ -141,11 +149,11 @@ export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
const content = this.contentDecoder.decode(extension.content)
switch (this.getExtensionName(content)) {
case ExtensionName.Dropbox:
return this.createCloudBackupFailedEventBasedOnProvider('DROPBOX', muteCloudEmailsSettingUuid, email)
return this.createCloudBackupFailedEventBasedOnProvider('DROPBOX', email)
case ExtensionName.GoogleDrive:
return this.createCloudBackupFailedEventBasedOnProvider('GOOGLE_DRIVE', muteCloudEmailsSettingUuid, email)
return this.createCloudBackupFailedEventBasedOnProvider('GOOGLE_DRIVE', email)
case ExtensionName.OneDrive:
return this.createCloudBackupFailedEventBasedOnProvider('ONE_DRIVE', muteCloudEmailsSettingUuid, email)
return this.createCloudBackupFailedEventBasedOnProvider('ONE_DRIVE', email)
}
}

View File

@@ -1,4 +1,5 @@
import { KeyParamsData } from '@standardnotes/responses'
import { Item } from '../Item/Item'
export type SendItemsToExtensionsServerDTO = {
@@ -8,6 +9,5 @@ export type SendItemsToExtensionsServerDTO = {
authParams: KeyParamsData
forceMute: boolean
userUuid: string
muteEmailsSettingUuid?: string
items?: Array<Item>
}

Some files were not shown because too many files have changed in this diff Show More