Compare commits

...

97 Commits

Author SHA1 Message Date
standardci
9ca373e208 chore(release): publish new version
- @standardnotes/analytics@2.12.27
 - @standardnotes/auth-server@1.67.3
 - @standardnotes/domain-core@1.11.0
 - @standardnotes/revisions-server@1.9.28
 - @standardnotes/scheduler-server@1.15.8
 - @standardnotes/syncing-server@1.26.7
 - @standardnotes/workspace-server@1.18.6
2022-12-15 11:26:36 +00:00
Karol Sójko
4084f2f5ec feat(domain-core): add legacy session model 2022-12-15 12:24:11 +01:00
standardci
684ffbadbc chore(release): publish new version
- @standardnotes/analytics@2.12.26
 - @standardnotes/auth-server@1.67.2
 - @standardnotes/domain-core@1.10.0
 - @standardnotes/revisions-server@1.9.27
 - @standardnotes/scheduler-server@1.15.7
 - @standardnotes/syncing-server@1.26.6
 - @standardnotes/workspace-server@1.18.5
2022-12-15 10:38:10 +00:00
Karol Sójko
1c4d4c57de feat(domain-core): add session model 2022-12-15 11:35:49 +01:00
standardci
d83111a199 chore(release): publish new version
- @standardnotes/syncing-server@1.26.5
2022-12-15 09:40:56 +00:00
Karol Sójko
f10fa839fb fix(syncing-server): revisions processing limit 2022-12-15 10:38:25 +01:00
standardci
1f20395ff3 chore(release): publish new version
- @standardnotes/syncing-server@1.26.4
2022-12-15 07:33:29 +00:00
Karol Sójko
bfe6f4255a fix(syncing-server): user uuid field name 2022-12-15 08:31:30 +01:00
standardci
b9032f3012 chore(release): publish new version
- @standardnotes/syncing-server@1.26.3
2022-12-15 06:31:07 +00:00
Karol Sójko
ce53c459e6 fix(syncing-server): select fields in query for revisions 2022-12-15 07:28:43 +01:00
standardci
6df42fb0d5 chore(release): publish new version
- @standardnotes/syncing-server@1.26.2
2022-12-14 19:08:28 +00:00
Karol Sójko
1e2b496f4f fix(syncing-server): revisions procedure logs 2022-12-14 20:06:07 +01:00
standardci
528c1b0d57 chore(release): publish new version
- @standardnotes/syncing-server@1.26.1
2022-12-14 18:58:11 +00:00
Karol Sójko
22fba8ba80 fix(syncing-server): revisions procedure logs 2022-12-14 19:56:17 +01:00
standardci
6f26261ebe chore(release): publish new version
- @standardnotes/syncing-server@1.26.0
2022-12-14 15:51:14 +00:00
Karol Sójko
4b1fe3ba91 feat(syncing-server): change revisions procedure to pagination instead of streaming 2022-12-14 16:49:21 +01:00
standardci
9f95262bd4 chore(release): publish new version
- @standardnotes/syncing-server@1.25.6
2022-12-14 09:33:24 +00:00
Karol Sójko
2ec28e541e fix(syncing-server): additional stream events handling on revisions procedure 2022-12-14 10:31:06 +01:00
standardci
4764d4b19a chore(release): publish new version
- @standardnotes/syncing-server@1.25.5
2022-12-14 09:00:42 +00:00
Karol Sójko
9b27547dae fix(syncing-server): revisions procedure with env var defined ranges 2022-12-14 09:58:41 +01:00
standardci
a96f2c9153 chore(release): publish new version
- @standardnotes/syncing-server@1.25.4
2022-12-13 11:14:35 +00:00
Karol Sójko
225e0aaf88 fix(syncing-server): logs on revisions procedure 2022-12-13 12:12:42 +01:00
standardci
f0c85910bc chore(release): publish new version
- @standardnotes/syncing-server@1.25.3
2022-12-13 11:07:23 +00:00
Karol Sójko
124c443528 fix(syncing-server): revisions ownership procedure destructured 2022-12-13 12:05:10 +01:00
standardci
37c7f8d39f chore(release): publish new version
- @standardnotes/syncing-server@1.25.2
2022-12-13 07:19:55 +00:00
Karol Sójko
c419f1ce22 fix(syncing-server): change revisions migration to notes 2022-12-13 08:17:55 +01:00
standardci
4949cdfe2f chore(release): publish new version
- @standardnotes/syncing-server@1.25.1
2022-12-13 06:03:06 +00:00
Karol Sójko
cd101b96ea fix(syncing-server): revisions procedure properties 2022-12-13 07:01:06 +01:00
standardci
40d0e4631f chore(release): publish new version
- @standardnotes/syncing-server@1.25.0
2022-12-12 19:06:24 +00:00
Karol Sójko
a55a995660 feat(syncing-server): fix streaming items for revisions update 2022-12-12 20:03:45 +01:00
standardci
1d576d48ad chore(release): publish new version
- @standardnotes/auth-server@1.67.1
 - @standardnotes/syncing-server@1.24.7
2022-12-12 13:20:47 +00:00
Karol Sójko
4ff8030f87 fix(syncing-server): revisions updating - select fields 2022-12-12 14:18:45 +01:00
Karol Sójko
c15e2e2c8f fix: user signed in email template 2022-12-12 14:18:45 +01:00
standardci
41d31a8d75 chore(release): publish new version
- @standardnotes/auth-server@1.67.0
2022-12-12 13:00:40 +00:00
Karol Sójko
10e2a26352 feat(auth): add email subscription unsubscribed event handler 2022-12-12 13:58:35 +01:00
standardci
6e547f77d0 chore(release): publish new version
- @standardnotes/revisions-server@1.9.26
2022-12-12 12:14:52 +00:00
Karol Sójko
530a426601 fix(revisions): responses to match previous response structure 2022-12-12 13:12:46 +01:00
Karol Sójko
642d6bab77 chore: fix triggers for other repos dep 2022-12-12 12:56:28 +01:00
standardci
7980af3d82 chore(release): publish new version
- @standardnotes/analytics@2.12.25
 - @standardnotes/api-gateway@1.40.2
 - @standardnotes/auth-server@1.66.9
 - @standardnotes/domain-events-infra@1.9.56
 - @standardnotes/domain-events@2.104.1
 - @standardnotes/event-store@1.6.53
 - @standardnotes/files-server@1.8.52
 - @standardnotes/revisions-server@1.9.25
 - @standardnotes/scheduler-server@1.15.6
 - @standardnotes/syncing-server@1.24.6
 - @standardnotes/websockets-server@1.4.53
 - @standardnotes/workspace-server@1.18.4
2022-12-12 11:38:37 +00:00
Karol Sójko
2980c42e88 fix(domain-events): add additional domain event services 2022-12-12 12:36:09 +01:00
standardci
b03994f9db chore(release): publish new version
- @standardnotes/analytics@2.12.24
2022-12-12 11:31:36 +00:00
Karol Sójko
41906ec2f9 fix(analytics): daily analytics report template 2022-12-12 12:29:16 +01:00
standardci
4d1e7ff2a5 chore(release): publish new version
- @standardnotes/analytics@2.12.23
 - @standardnotes/api-gateway@1.40.1
 - @standardnotes/auth-server@1.66.8
 - @standardnotes/domain-events-infra@1.9.55
 - @standardnotes/domain-events@2.104.0
 - @standardnotes/event-store@1.6.52
 - @standardnotes/files-server@1.8.51
 - @standardnotes/revisions-server@1.9.24
 - @standardnotes/scheduler-server@1.15.5
 - @standardnotes/syncing-server@1.24.5
 - @standardnotes/websockets-server@1.4.52
 - @standardnotes/workspace-server@1.18.3
2022-12-12 11:26:27 +00:00
Karol Sójko
7f18fcfc13 feat(domain-events): add event for email subscription unsubscribed 2022-12-12 12:24:31 +01:00
standardci
ff02ce0747 chore(release): publish new version
- @standardnotes/analytics@2.12.22
2022-12-12 10:31:56 +00:00
Karol Sójko
a6056600eb fix(analytics): report event publishing 2022-12-12 11:29:41 +01:00
standardci
24c94326d5 chore(release): publish new version
- @standardnotes/analytics@2.12.21
2022-12-12 09:51:01 +00:00
Karol Sójko
48c0cb5e62 fix(analytics): add debug logs for report 2022-12-12 10:49:11 +01:00
standardci
9968efe1b2 chore(release): publish new version
- @standardnotes/syncing-server@1.24.4
2022-12-12 08:49:03 +00:00
Karol Sójko
6368342149 fix(syncing-server): data integrity check on revisions fix 2022-12-12 09:46:35 +01:00
standardci
b5f73db210 chore(release): publish new version
- @standardnotes/api-gateway@1.40.0
2022-12-12 04:12:20 +00:00
Karol Sójko
22d6a02d04 feat(api-gateway): add unsubscribe from emails endpoint 2022-12-12 05:10:18 +01:00
standardci
4e0bcfcccf chore(release): publish new version
- @standardnotes/auth-server@1.66.7
2022-12-09 14:30:03 +00:00
Karol Sójko
104313c15d fix(auth): linter issue 2022-12-09 15:27:39 +01:00
standardci
814289af46 chore(release): publish new version
- @standardnotes/analytics@2.12.20
 - @standardnotes/api-gateway@1.39.24
 - @standardnotes/auth-server@1.66.6
 - @standardnotes/domain-events-infra@1.9.54
 - @standardnotes/domain-events@2.103.2
 - @standardnotes/event-store@1.6.51
 - @standardnotes/files-server@1.8.50
 - @standardnotes/revisions-server@1.9.23
 - @standardnotes/scheduler-server@1.15.4
 - @standardnotes/syncing-server@1.24.3
 - @standardnotes/websockets-server@1.4.51
 - @standardnotes/workspace-server@1.18.2
2022-12-09 14:10:16 +00:00
Karol Sójko
3096cd98d5 feat(analytics) replace daily analytics report generated event with email requested 2022-12-09 15:08:17 +01:00
standardci
45dfefbc7a chore(release): publish new version
- @standardnotes/analytics@2.12.19
 - @standardnotes/api-gateway@1.39.23
 - @standardnotes/auth-server@1.66.5
 - @standardnotes/domain-events-infra@1.9.53
 - @standardnotes/domain-events@2.103.1
 - @standardnotes/event-store@1.6.50
 - @standardnotes/files-server@1.8.49
 - @standardnotes/revisions-server@1.9.22
 - @standardnotes/scheduler-server@1.15.3
 - @standardnotes/syncing-server@1.24.2
 - @standardnotes/websockets-server@1.4.50
 - @standardnotes/workspace-server@1.18.1
2022-12-09 13:39:59 +00:00
Karol Sójko
20d92149a8 fix(domain-events): add additional styles option for sending email 2022-12-09 14:37:22 +01:00
standardci
9c01fffca5 chore(release): publish new version
- @standardnotes/analytics@2.12.18
 - @standardnotes/api-gateway@1.39.22
 - @standardnotes/auth-server@1.66.4
 - @standardnotes/domain-events-infra@1.9.52
 - @standardnotes/domain-events@2.103.0
 - @standardnotes/event-store@1.6.49
 - @standardnotes/files-server@1.8.48
 - @standardnotes/revisions-server@1.9.21
 - @standardnotes/scheduler-server@1.15.2
 - @standardnotes/syncing-server@1.24.1
 - @standardnotes/websockets-server@1.4.49
 - @standardnotes/workspace-server@1.18.0
2022-12-09 13:32:57 +00:00
Karol Sójko
61c1cfff4b feat(workspace): replace workspace invite created event with email requested 2022-12-09 14:30:58 +01:00
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
standardci
ec9e9ec387 chore(release): publish new version
- @standardnotes/analytics@2.12.6
 - @standardnotes/api-gateway@1.39.10
 - @standardnotes/auth-server@1.63.2
 - @standardnotes/domain-events-infra@1.9.40
 - @standardnotes/domain-events@2.95.0
 - @standardnotes/event-store@1.6.36
 - @standardnotes/files-server@1.8.36
 - @standardnotes/revisions-server@1.9.9
 - @standardnotes/scheduler-server@1.13.37
 - @standardnotes/syncing-server@1.20.9
 - @standardnotes/websockets-server@1.4.37
 - @standardnotes/workspace-server@1.17.36
2022-12-07 09:53:15 +00:00
Karol Sójko
fa75aa40f0 feat(domain-events): add email requested event 2022-12-07 10:51:22 +01:00
170 changed files with 3406 additions and 1854 deletions

View File

@@ -187,7 +187,7 @@ jobs:
tags: standardnotes/${{ inputs.service_name }}:${{ github.sha }}
- name: Run E2E test suite
uses: convictional/trigger-workflow-and-wait@v1.6.3
uses: convictional/trigger-workflow-and-wait@master
with:
owner: standardnotes
repo: e2e

6
.pnp.cjs generated
View File

@@ -2737,10 +2737,6 @@ const RAW_RUNTIME_STATE =
"packageLocation": "./packages/domain-core/",\
"packageDependencies": [\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.53.1"],\
["@standardnotes/predicates", "workspace:packages/predicates"],\
["@standardnotes/security", "workspace:packages/security"],\
["@types/jest", "npm:29.1.1"],\
["@types/uuid", "npm:8.3.4"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
@@ -3056,6 +3052,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"],\
@@ -3323,6 +3320,7 @@ const RAW_RUNTIME_STATE =
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/api", "npm:1.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/models", "npm:1.28.0"],\

6
.prettierrc Normal file
View File

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

View File

@@ -3,6 +3,100 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.12.27](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.26...@standardnotes/analytics@2.12.27) (2022-12-15)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.26](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.25...@standardnotes/analytics@2.12.26) (2022-12-15)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.25](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.24...@standardnotes/analytics@2.12.25) (2022-12-12)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.24](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.23...@standardnotes/analytics@2.12.24) (2022-12-12)
### Bug Fixes
* **analytics:** daily analytics report template ([41906ec](https://github.com/standardnotes/server/commit/41906ec2f9fd4d605b1c002826173e14fb534e00))
## [2.12.23](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.22...@standardnotes/analytics@2.12.23) (2022-12-12)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.22](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.21...@standardnotes/analytics@2.12.22) (2022-12-12)
### Bug Fixes
* **analytics:** report event publishing ([a605660](https://github.com/standardnotes/server/commit/a6056600eb96bf175189ad6d62870c9d736f331b))
## [2.12.21](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.20...@standardnotes/analytics@2.12.21) (2022-12-12)
### Bug Fixes
* **analytics:** add debug logs for report ([48c0cb5](https://github.com/standardnotes/server/commit/48c0cb5e62dc8af930de191deaa1eb3ff6c5a29f))
## [2.12.20](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.19...@standardnotes/analytics@2.12.20) (2022-12-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.19](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.18...@standardnotes/analytics@2.12.19) (2022-12-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.18](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.17...@standardnotes/analytics@2.12.18) (2022-12-09)
**Note:** Version bump only for package @standardnotes/analytics
## [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
## [2.12.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.4...@standardnotes/analytics@2.12.5) (2022-12-07)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -4,6 +4,7 @@ import 'newrelic'
import { Logger } from 'winston'
import { EmailLevel } from '@standardnotes/domain-core'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity'
import { Period } from '../src/Domain/Time/Period'
@@ -16,6 +17,8 @@ import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport'
import { TimerInterface } from '@standardnotes/time'
const requestReport = async (
analyticsStore: AnalyticsStoreInterface,
@@ -24,6 +27,8 @@ const requestReport = async (
domainEventPublisher: DomainEventPublisherInterface,
periodKeyGenerator: PeriodKeyGeneratorInterface,
calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue,
timer: TimerInterface,
adminEmails: string[],
): Promise<void> => {
await calculateMonthlyRecurringRevenue.execute({})
@@ -213,18 +218,29 @@ const requestReport = async (
})
}
const event = domainEventFactory.createDailyAnalyticsReportGeneratedEvent({
activityStatistics: yesterdayActivityStatistics,
activityStatisticsOverTime: analyticsOverTime,
statisticsOverTime,
statisticMeasures,
churn: {
periodKeys: monthlyPeriodKeys,
values: churnRates,
},
})
await domainEventPublisher.publish(event)
for (const adminEmail of adminEmails) {
await domainEventPublisher.publish(
domainEventFactory.createEmailRequestedEvent({
messageIdentifier: 'VERSION_ADOPTION_REPORT',
subject: getSubject(),
body: getBody(
{
activityStatistics: yesterdayActivityStatistics,
activityStatisticsOverTime: analyticsOverTime,
statisticsOverTime,
statisticMeasures,
churn: {
periodKeys: monthlyPeriodKeys,
values: churnRates,
},
},
timer,
),
level: EmailLevel.LEVELS.System,
userEmail: adminEmail,
}),
)
}
}
const container = new ContainerConfigLoader()
@@ -241,9 +257,13 @@ void container.load().then((container) => {
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator)
const timer: TimerInterface = container.get(TYPES.Timer)
const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get(
TYPES.CalculateMonthlyRecurringRevenue,
)
const adminEmails = container.get(TYPES.ADMIN_EMAILS) as string[]
logger.info(`Sending report to following admins: ${adminEmails}`)
Promise.resolve(
requestReport(
@@ -253,6 +273,8 @@ void container.load().then((container) => {
domainEventPublisher,
periodKeyGenerator,
calculateMonthlyRecurringRevenue,
timer,
adminEmails,
),
)
.then(() => {

View File

@@ -5,17 +5,17 @@ COMMAND=$1 && shift 1
case "$COMMAND" in
'start-worker' )
echo "Starting Worker..."
echo "[Docker] Starting Worker..."
yarn workspace @standardnotes/analytics worker
;;
'report' )
echo "Starting Usage Report Generation..."
echo "[Docker] Starting Usage Report Generation..."
yarn workspace @standardnotes/analytics report
;;
* )
echo "Unknown command"
echo "[Docker] Unknown command"
;;
esac

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.12.5",
"version": "2.12.27",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -40,7 +40,7 @@
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:*",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",
"@standardnotes/time": "workspace:*",

View File

@@ -130,6 +130,7 @@ export class ContainerConfigLoader {
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container.bind(TYPES.ADMIN_EMAILS).toConstantValue(env.get('ADMIN_EMAILS').split(','))
// Repositories
container

View File

@@ -11,6 +11,7 @@ const TYPES = {
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
ADMIN_EMAILS: Symbol.for('ADMIN_EMAILS'),
// Repositories
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),

View File

@@ -0,0 +1,11 @@
import { TimerInterface } from '@standardnotes/time'
import { html } from './daily-analytics-report.html'
export function getSubject(): string {
return `Daily analytics report ${new Date().toLocaleDateString('en-US')}`
}
export function getBody(data: unknown, timer: TimerInterface): string {
return html(data, timer)
}

View File

@@ -0,0 +1,966 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TimerInterface } from '@standardnotes/time'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { Period } from '../Time/Period'
const getChartUrls = (
data: any,
): {
subscriptions: string
users: string
quarterlyPerformance: string
churn: string
mrr: string
mrrMonthly: string
} => {
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionPurchased && a.period === Period.Last30Days,
)
const subscriptionRenewingOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionRenewed && a.period === Period.Last30Days,
)
const subscriptionRefundingOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionRefunded && a.period === Period.Last30Days,
)
const subscriptionCancelledOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionCancelled && a.period === Period.Last30Days,
)
const subscriptionReactivatedOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionReactivated && a.period === Period.Last30Days,
)
const subscriptionsLinerOverTimeConfig = {
type: 'line',
data: {
labels: subscriptionPurchasingOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
datasets: [
{
label: 'Subscription Purchases',
backgroundColor: 'rgb(25, 255, 140)',
borderColor: 'rgb(25, 255, 140)',
data: subscriptionPurchasingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'Subscription Renewals',
backgroundColor: 'rgb(54, 162, 235)',
borderColor: 'rgb(54, 162, 235)',
data: subscriptionRenewingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'Subscription Refunds',
backgroundColor: 'rgb(255, 221, 51)',
borderColor: 'rgb(255, 221, 51)',
data: subscriptionRefundingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'Subscription Cancels',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: subscriptionCancelledOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'Subscription Reactivations',
backgroundColor: 'rgb(221, 51, 255)',
borderColor: 'rgb(221, 51, 255)',
data: subscriptionReactivatedOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
],
},
}
const userRegistrationOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.Register && a.period === Period.Last30Days,
)
const userDeletionOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
)
const usersLinerOverTimeConfig = {
type: 'line',
data: {
labels: userRegistrationOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
datasets: [
{
label: 'User Registrations',
backgroundColor: 'rgb(25, 255, 140)',
borderColor: 'rgb(25, 255, 140)',
data: userRegistrationOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'Account Deletions',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: userDeletionOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
],
},
}
const quarters = [Period.Q1ThisYear, Period.Q2ThisYear, Period.Q3ThisYear, Period.Q4ThisYear]
const quarterlyUserRegistrations = []
const quarterlySubscriptionPurchases = []
const quarterlySubscriptionRenewals = []
for (const quarter of quarters) {
const registrations =
data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.Register && a.period === quarter,
)?.totalCount ?? 0
const purchases =
data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionPurchased && a.period === quarter,
)?.totalCount ?? 0
const renewals =
data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionRenewed && a.period === quarter,
)?.totalCount ?? 0
quarterlyUserRegistrations.push(registrations)
quarterlySubscriptionPurchases.push(purchases)
quarterlySubscriptionRenewals.push(renewals)
}
const quarterlyConfig = {
type: 'bar',
data: {
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
datasets: [
{
label: 'User Registrations',
backgroundColor: 'rgba(255, 99, 132, 0.5)',
borderColor: 'rgb(255, 99, 132)',
borderWidth: 1,
data: quarterlyUserRegistrations,
},
{
label: 'Subscription Purchases',
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgb(54, 162, 235)',
borderWidth: 1,
data: quarterlySubscriptionPurchases,
},
{
label: 'Subscription Renewals',
backgroundColor: 'rgb(25, 255, 140, 0.5)',
borderColor: 'rgb(25, 255, 140)',
borderWidth: 1,
data: quarterlySubscriptionRenewals,
},
],
},
options: {
title: {
display: true,
text: 'Quarterly Performance',
},
plugins: {
datalabels: {
anchor: 'center',
align: 'center',
color: '#666',
font: {
weight: 'normal',
},
},
},
},
}
const monthlyChurnRates = data.churn.values.map((value: { rate: number }) => +value.rate.toFixed(2))
const churnConfig = {
type: 'bar',
data: {
labels: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
datasets: [
{
label: 'Churn Percent',
backgroundColor: 'rgba(255, 99, 132, 0.5)',
borderColor: 'rgb(255, 99, 132)',
borderWidth: 1,
data: monthlyChurnRates,
},
],
},
options: {
title: {
display: true,
text: 'Monthly Churn Rate',
},
plugins: {
datalabels: {
anchor: 'center',
align: 'center',
color: '#666',
font: {
weight: 'normal',
},
},
},
},
}
const mrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'mrr' && a.period === 27,
)
const monthlyPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'monthly-plans-mrr' && a.period === 27,
)
const annualPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'annual-plans-mrr' && a.period === 27,
)
const fiveYearPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'five-year-plans-mrr' && a.period === 27,
)
const proPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'pro-plans-mrr' && a.period === 27,
)
const plusPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'plus-plans-mrr' && a.period === 27,
)
const mrrOverTimeConfig = {
type: 'line',
data: {
labels: mrrOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
datasets: [
{
label: 'MRR',
backgroundColor: 'rgb(25, 255, 140)',
borderColor: 'rgb(25, 255, 140)',
data: mrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - Monthly Plans',
backgroundColor: 'rgb(54, 162, 235)',
borderColor: 'rgb(54, 162, 235)',
data: monthlyPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - Annual Plans',
backgroundColor: 'rgb(255, 221, 51)',
borderColor: 'rgb(255, 221, 51)',
data: annualPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - Five Year Plans',
backgroundColor: 'rgb(255, 120, 120)',
borderColor: 'rgb(255, 120, 120)',
data: fiveYearPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - PRO Plans',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: proPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - PLUS Plans',
backgroundColor: 'rgb(221, 51, 255)',
borderColor: 'rgb(221, 51, 255)',
data: plusPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
],
},
}
const mrrMonthlyOverTime = data.statisticsOverTime
.find((a: { name: string; period: Period }) => a.name === 'mrr' && a.period === Period.ThisYear)
?.counts.map((count: { totalCount: number }) => +count.totalCount.toFixed(2))
const mrrMonthlyConfig = {
type: 'bar',
data: {
labels: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
datasets: [
{
label: 'MRR',
backgroundColor: 'rgba(25, 255, 140, 0.5)',
borderColor: 'rgb(25, 255, 140)',
borderWidth: 1,
data: mrrMonthlyOverTime,
},
],
},
options: {
title: {
display: true,
text: 'Monthly MRR',
},
plugins: {
datalabels: {
anchor: 'center',
align: 'center',
color: '#666',
font: {
weight: 'normal',
},
},
},
},
}
return {
subscriptions: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(
JSON.stringify(subscriptionsLinerOverTimeConfig),
)}`,
users: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(usersLinerOverTimeConfig))}`,
quarterlyPerformance: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(
JSON.stringify(quarterlyConfig),
)}`,
churn: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(churnConfig))}`,
mrr: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrOverTimeConfig))}`,
mrrMonthly: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrMonthlyConfig))}`,
}
}
export const html = (data: any, timer: TimerInterface) => {
const chartUrls = getChartUrls(data)
const successfullPaymentsActivity = data.activityStatistics.find(
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.PaymentSuccess && Period.Yesterday,
)
const failedPaymentsActivity = data.activityStatistics.find(
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.PaymentFailed && Period.Yesterday,
)
const limitedDiscountPurchasedActivity = data.activityStatistics.find(
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.LimitedDiscountOfferPurchased && Period.Yesterday,
)
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionPurchased && a.period === Period.Last30Days,
)
const subscriptionRenewingOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionRenewed && a.period === Period.Last30Days,
)
const subscriptionRefundingOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionRefunded && a.period === Period.Last30Days,
)
const subscriptionCancelledOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionCancelled && a.period === Period.Last30Days,
)
const subscriptionReactivatedOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.SubscriptionReactivated && a.period === Period.Last30Days,
)
const userRegistrationOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.Register && a.period === Period.Last30Days,
)
const userDeletionOverTime = data.activityStatisticsOverTime.find(
(a: { name: AnalyticsActivity; period: Period }) =>
a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
)
const incomeMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Income && a.period === Period.Yesterday,
)
const refundMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Refunds && a.period === Period.Yesterday,
)
const incomeYesterday = incomeMeasureYesterday?.totalValue ?? 0
const refundsYesterday = refundMeasureYesterday?.totalValue ?? 0
const revenueYesterday = incomeYesterday - refundsYesterday
const subscriptionLengthMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.Yesterday,
)
const subscriptionLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
Math.floor(subscriptionLengthMeasureYesterday?.average ?? 0),
)
const subscriptionRemainingTimePercentageMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday,
)
const subscriptionRemainingTimePercentageYesterday = Math.floor(
subscriptionRemainingTimePercentageMeasureYesterday?.average ?? 0,
)
const registrationLengthMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationLength && a.period === Period.Yesterday,
)
const registrationLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationLengthMeasureYesterday?.average ?? 0),
)
const registrationToSubscriptionMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.Yesterday,
)
const registrationToSubscriptionDurationYesterday = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationToSubscriptionMeasureYesterday?.average ?? 0),
)
const incomeMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Income && a.period === Period.ThisMonth,
)
const refundMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Refunds && a.period === Period.ThisMonth,
)
const incomeThisMonth = incomeMeasureThisMonth?.totalValue ?? 0
const refundsThisMonth = refundMeasureThisMonth?.totalValue ?? 0
const revenueThisMonth = incomeThisMonth - refundsThisMonth
const subscriptionLengthMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.ThisMonth,
)
const subscriptionLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
Math.floor(subscriptionLengthMeasureThisMonth?.average ?? 0),
)
const subscriptionRemainingTimePercentageMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth,
)
const subscriptionRemainingTimePercentageThisMonth = Math.floor(
subscriptionRemainingTimePercentageMeasureThisMonth?.average ?? 0,
)
const registrationLengthMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationLength && a.period === Period.ThisMonth,
)
const registrationLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationLengthMeasureThisMonth?.average ?? 0),
)
const registrationToSubscriptionMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.ThisMonth,
)
const registrationToSubscriptionDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationToSubscriptionMeasureThisMonth?.average ?? 0),
)
const plusSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
)
const plusSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
)
const plusSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
)
const plusSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
)
const proSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
)
const proSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
)
const proSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
)
const proSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
)
const plusSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
)
const plusSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
)
const plusSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
)
const plusSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
)
const proSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
)
const proSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
)
const proSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
)
const proSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
)
const mrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'mrr' && a.period === 27,
)
const monthlyPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'monthly-plans-mrr' && a.period === 27,
)
const annualPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'annual-plans-mrr' && a.period === 27,
)
const fiveYearPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'five-year-plans-mrr' && a.period === 27,
)
const proPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'pro-plans-mrr' && a.period === 27,
)
const plusPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'plus-plans-mrr' && a.period === 27,
)
const today = new Date()
const thisMonthPeriodKey = `${today.getFullYear().toString()}-${(today.getMonth() + 1).toString()}`
const thisMonthChurn = data.churn.values.find(
(value: { periodKey: string }) => value.periodKey === thisMonthPeriodKey,
)
return ` <div>
<p>Hello,</p>
<p>
<strong>Here are some statistics from yesterday:</strong>
</p>
<ul>
<li>
<b>Payments</b>
<ul>
<li>
Revenue: <b>$${revenueYesterday.toLocaleString('en-US')}</b> (Income: $
${incomeYesterday.toLocaleString('en-US')}, Refunds: $${refundsYesterday.toLocaleString('en-US')})
</li>
<li>
Successfull payments: <b>${successfullPaymentsActivity?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Failed payments: <b>${failedPaymentsActivity?.totalCount.toLocaleString('en-US')}</b>
</li>
</ul>
</li>
<li>
<b>MRR Breakdown</b>
<ul>
<li>
<b>Total:</b> $${mrrOverTime?.counts[mrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
</li>
<li>
<b>By Subscription Type:</b>
<ul>
<li>
<b>PLUS:</b> $
${plusPlansMrrOverTime?.counts[plusPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
</li>
<li>
<b>PRO:</b> $
${proPlansMrrOverTime?.counts[proPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
</li>
</ul>
</li>
<li>
<b>By Billing Frequency:</b>
<ul>
<li>
<b>Monthly:</b> $
${monthlyPlansMrrOverTime?.counts[monthlyPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
'en-US',
)}
</li>
<li>
<b>Annual:</b> $
${annualPlansMrrOverTime?.counts[annualPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
'en-US',
)}
</li>
<li>
<b>5-year:</b> $
${fiveYearPlansMrrOverTime?.counts[fiveYearPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
'en-US',
)}
</li>
</ul>
</li>
</ul>
</li>
<li>
<b>Income Breakdown</b>
<ul>
<li>
<b>Plus Subscription:</b>
<ul>
<li>
<b>${plusSubscriptionsInitialMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
<b>$${plusSubscriptionsInitialMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${plusSubscriptionsInitialAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${plusSubscriptionsInitialAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${plusSubscriptionsRenewingMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
<b>$${plusSubscriptionsRenewingMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${plusSubscriptionsRenewingAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${plusSubscriptionsRenewingAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
</ul>
</li>
<li>
<b>Pro Subscription:</b>
<ul>
<li>
<b>${proSubscriptionsInitialMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
<b>$${proSubscriptionsInitialMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${proSubscriptionsInitialAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${proSubscriptionsInitialAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${proSubscriptionsRenewingMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
<b>$${proSubscriptionsRenewingMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${proSubscriptionsRenewingAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${proSubscriptionsRenewingAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
</li>
</ul>
</li>
</ul>
</li>
<li>
<b>Users</b>
<ul>
<li>
Number of users registered:${' '}
<b>
${userRegistrationOverTime?.counts[userRegistrationOverTime?.counts.length - 1]?.totalCount.toLocaleString(
'en-US',
)}
</b>
</li>
<li>
Number of users unregistered:${' '}
<b>
${userDeletionOverTime?.counts[userDeletionOverTime?.counts.length - 1]?.totalCount.toLocaleString('en-US')}
</b>${' '}
(average account duration: ${registrationLengthDurationYesterday.days} days${' '}
${registrationLengthDurationYesterday.hours} hours ${registrationLengthDurationYesterday.minutes} minutes)
</li>
</ul>
</li>
<li>
<b>Subscriptions</b>
<ul>
<li>
Number of subscriptions purchased:${' '}
<b>
${subscriptionPurchasingOverTime?.counts[
subscriptionPurchasingOverTime?.counts.length - 1
]?.totalCount.toLocaleString('en-US')}
</b>${' '}
(includes <b>${limitedDiscountPurchasedActivity?.totalCount.toLocaleString('en-US')}</b> limited time
offer purchases)
</li>
<li>
Number of subscriptions renewed:${' '}
<b>
${subscriptionRenewingOverTime?.counts[
subscriptionRenewingOverTime?.counts.length - 1
]?.totalCount.toLocaleString('en-US')}
</b>
</li>
<li>
Number of subscriptions refunded:${' '}
<b>
${subscriptionRefundingOverTime?.counts[
subscriptionRefundingOverTime?.counts.length - 1
]?.totalCount.toLocaleString('en-US')}
</b>
</li>
<li>
Number of subscriptions cancelled:${' '}
<b>
${subscriptionCancelledOverTime?.counts[
subscriptionCancelledOverTime?.counts.length - 1
]?.totalCount.toLocaleString('en-US')}
</b>${' '}
(average subscription duration: ${subscriptionLengthDurationYesterday.days} days${' '}
${subscriptionLengthDurationYesterday.hours} hours ${subscriptionLengthDurationYesterday.minutes} minutes,
average remaining subscription percentage: ${subscriptionRemainingTimePercentageYesterday}%)
</li>
<li>
Number of subscriptions reactivated:${' '}
<b>
${subscriptionReactivatedOverTime?.counts[
subscriptionReactivatedOverTime?.counts.length - 1
]?.totalCount.toLocaleString('en-US')}
</b>
</li>
<li>
Average time from registration to subscription purchase:${' '}
<b>
${registrationToSubscriptionDurationYesterday.days} days${' '}
${registrationToSubscriptionDurationYesterday.hours} hours${' '}
${registrationToSubscriptionDurationYesterday.minutes} minutes
</b>
</li>
</ul>
</li>
</ul>
<p>
<strong>Here are some statistics from last 30 days:</strong>
</p>
<ul>
<li>
<b>Payments (This Month)</b>
<ul>
<li>
Revenue: <b>$${revenueThisMonth.toLocaleString('en-US')}</b>
</li>
<li>
Income: <b>$${incomeThisMonth.toLocaleString('en-US')}</b>
</li>
<li>
Refunds: <b>$${refundsThisMonth.toLocaleString('en-US')}</b>
</li>
</ul>
</li>
<li>
<b>Income Breakdown (This Month)</b>
<ul>
<li>
<b>Plus Subscription:</b>
<ul>
<li>
<b>${plusSubscriptionsInitialMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
<b>$${plusSubscriptionsInitialMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${plusSubscriptionsInitialAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${plusSubscriptionsInitialAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${plusSubscriptionsRenewingMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
<b>$${plusSubscriptionsRenewingMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${plusSubscriptionsRenewingAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${plusSubscriptionsRenewingAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
</ul>
</li>
<li>
<b>Pro Subscription:</b>
<ul>
<li>
<b>${proSubscriptionsInitialMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
<b>$${proSubscriptionsInitialMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${proSubscriptionsInitialAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${proSubscriptionsInitialAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${proSubscriptionsRenewingMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
<b>$${proSubscriptionsRenewingMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
<li>
<b>${proSubscriptionsRenewingAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
<b>$${proSubscriptionsRenewingAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
</li>
</ul>
</li>
</ul>
</li>
<li>
<b>Users</b>
<ul>
<li>
Number of users registered: <b>${userRegistrationOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Number of users unregistered: <b>${userDeletionOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Average account duration this month:${' '}
<b>
${registrationLengthDurationThisMonth.days} days ${registrationLengthDurationThisMonth.hours} hours${' '}
${registrationLengthDurationThisMonth.minutes} minutes
</b>
</li>
</ul>
</li>
<li>
<b>Subscriptions</b>
<ul>
<li>
Number of subscriptions purchased:${' '}
<b>${subscriptionPurchasingOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Number of subscriptions renewed:${' '}
<b>${subscriptionRenewingOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Number of subscriptions refunded:${' '}
<b>${subscriptionRefundingOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Number of subscriptions cancelled:${' '}
<b>${subscriptionCancelledOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Number of subscriptions reactivated:${' '}
<b>${subscriptionReactivatedOverTime?.totalCount.toLocaleString('en-US')}</b>
</li>
<li>
Average subscription duration this month:${' '}
<b>
${subscriptionLengthDurationThisMonth.days} days ${subscriptionLengthDurationThisMonth.hours} hours${' '}
${subscriptionLengthDurationThisMonth.minutes} minutes
</b>
</li>
<li>
Average subscription remaining percentage this month:${' '}
<b>${subscriptionRemainingTimePercentageThisMonth}%</b>
</li>
<li>
Average time from registration to subscription purchase this month:${' '}
<b>
${registrationToSubscriptionDurationThisMonth.days} days${' '}
${registrationToSubscriptionDurationThisMonth.hours} hours${' '}
${registrationToSubscriptionDurationThisMonth.minutes} minutes
</b>
</li>
</ul>
</li>
</ul>
<p>
<strong>Here is the MRR chart over 30 days:</strong>
</p>
<img src=${chartUrls.mrr}></img>
<p>
<strong>Here is the MRR Monthly chart this year:</strong>
</p>
<img src=${chartUrls.mrrMonthly}></img>
<p>
<strong>Here is the subscription chart over 30 days:</strong>
</p>
<img src=${chartUrls.subscriptions}></img>
<p>
<strong>Here is the users chart over 30 days:</strong>
</p>
<img src=${chartUrls.users}></img>
<p>
<strong>Here is the monthly churn rate percentage:</strong>
</p>
<p>✅ GREAT! Up to 7% 🔶 OKAY: 8-10% 🩸 BAD: 11 -15 % 🚨 TERRIBLE! 16-20%</p>
<p>Churn is calculated by the following formula:</p>
<p>
( Existing Customers Churn [${thisMonthChurn?.existingCustomersChurn}] + New Customers Churn [
${thisMonthChurn?.newCustomersChurn}] ) * 100 / Average Customers Count This Month [
${thisMonthChurn?.averageCustomersCount}]
</p>
<img src=${chartUrls.churn}></img>
<p>
<strong>Here is quarterly performance chart:</strong>
</p>
<img src=${chartUrls.quarterlyPerformance}></img>
<p>Thanks,SN</p>
</div>`
}

View File

@@ -1,6 +1,6 @@
/* istanbul ignore file */
import { DomainEventService, DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events'
import { DomainEventService, EmailRequestedEvent } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
@@ -9,55 +9,20 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
@injectable()
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
createDailyAnalyticsReportGeneratedEvent(dto: {
activityStatistics: Array<{
name: string
retention: number
totalCount: number
}>
statisticMeasures: Array<{
name: string
totalValue: number
average: number
increments: number
period: number
}>
activityStatisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
totalCount: number
}>
statisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
}>
churn: {
periodKeys: Array<string>
values: Array<{
rate: number
periodKey: string
averageCustomersCount: number
existingCustomersChurn: number
newCustomersChurn: number
}>
}
}): DailyAnalyticsReportGeneratedEvent {
createEmailRequestedEvent(dto: {
userEmail: string
messageIdentifier: string
level: string
body: string
subject: string
}): EmailRequestedEvent {
return {
type: 'DAILY_ANALYTICS_REPORT_GENERATED',
type: 'EMAIL_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: '',
userIdentifierType: 'uuid',
userIdentifier: dto.userEmail,
userIdentifierType: 'email',
},
origin: DomainEventService.Analytics,
},

View File

@@ -1,45 +1,11 @@
import { DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events'
import { EmailRequestedEvent } from '@standardnotes/domain-events'
export interface DomainEventFactoryInterface {
createDailyAnalyticsReportGeneratedEvent(dto: {
activityStatistics: Array<{
name: string
retention: number
totalCount: number
}>
statisticMeasures: Array<{
name: string
totalValue: number
average: number
increments: number
period: number
}>
activityStatisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
totalCount: number
}>
statisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
}>
churn: {
periodKeys: Array<string>
values: Array<{
rate: number
periodKey: string
averageCustomersCount: number
existingCustomersChurn: number
newCustomersChurn: number
}>
}
}): DailyAnalyticsReportGeneratedEvent
createEmailRequestedEvent(dto: {
userEmail: string
messageIdentifier: string
level: string
body: string
subject: string
}): EmailRequestedEvent
}

View File

@@ -11,6 +11,7 @@ WEB_SOCKET_SERVER_URL=http://websockets:3000
PAYMENTS_SERVER_URL=http://payments:3000
FILES_SERVER_URL=http://files:3000
REVISIONS_SERVER_URL=http://revisions:3000
EMAIL_SERVER_URL=http://email:3000
HTTP_CALL_TIMEOUT=60000

View File

@@ -3,6 +3,80 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.1...@standardnotes/api-gateway@1.40.2) (2022-12-12)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.40.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.0...@standardnotes/api-gateway@1.40.1) (2022-12-12)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.40.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.24...@standardnotes/api-gateway@1.40.0) (2022-12-12)
### Features
* **api-gateway:** add unsubscribe from emails endpoint ([22d6a02](https://github.com/standardnotes/api-gateway/commit/22d6a02d049ba3bde890c7def91e19f013ba3e22))
## [1.39.24](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.23...@standardnotes/api-gateway@1.39.24) (2022-12-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.23](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.22...@standardnotes/api-gateway@1.39.23) (2022-12-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.22](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.21...@standardnotes/api-gateway@1.39.22) (2022-12-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [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
## [1.39.9](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.8...@standardnotes/api-gateway@1.39.9) (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.9",
"version": "1.40.2",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -55,6 +55,7 @@ export class ContainerConfigLoader {
container.bind(TYPES.SYNCING_SERVER_JS_URL).toConstantValue(env.get('SYNCING_SERVER_JS_URL'))
container.bind(TYPES.AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
container.bind(TYPES.REVISIONS_SERVER_URL).toConstantValue(env.get('REVISIONS_SERVER_URL', true))
container.bind(TYPES.EMAIL_SERVER_URL).toConstantValue(env.get('EMAIL_SERVER_URL', true))
container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true))
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))

View File

@@ -8,6 +8,7 @@ const TYPES = {
PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'),
REVISIONS_SERVER_URL: Symbol.for('REVISIONS_SERVER_URL'),
EMAIL_SERVER_URL: Symbol.for('EMAIL_SERVER_URL'),
WORKSPACE_SERVER_URL: Symbol.for('WORKSPACE_SERVER_URL'),
WEB_SOCKET_SERVER_URL: Symbol.for('WEB_SOCKET_SERVER_URL'),
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),

View File

@@ -29,4 +29,14 @@ export class ActionsController extends BaseHttpController {
async methods(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'auth/methods', request.body)
}
@httpGet('/unsubscribe/:token')
async emailUnsubscribe(request: Request, response: Response): Promise<void> {
await this.httpService.callEmailServer(
request,
response,
`subscriptions/actions/unsubscribe/${request.params.token}`,
request.body,
)
}
}

View File

@@ -19,6 +19,7 @@ export class HttpService implements HttpServiceInterface {
@inject(TYPES.WORKSPACE_SERVER_URL) private workspaceServerUrl: string,
@inject(TYPES.WEB_SOCKET_SERVER_URL) private webSocketServerUrl: string,
@inject(TYPES.REVISIONS_SERVER_URL) private revisionsServerUrl: string,
@inject(TYPES.EMAIL_SERVER_URL) private emailServerUrl: string,
@inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Logger) private logger: Logger,
@@ -65,6 +66,21 @@ export class HttpService implements HttpServiceInterface {
await this.callServer(this.authServerUrl, request, response, endpoint, payload)
}
async callEmailServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void> {
if (!this.emailServerUrl) {
response.status(400).send({ message: 'Email Server not configured' })
return
}
await this.callServer(this.emailServerUrl, request, response, endpoint, payload)
}
async callWorkspaceServer(
request: Request,
response: Response,

View File

@@ -1,6 +1,12 @@
import { Request, Response } from 'express'
export interface HttpServiceInterface {
callEmailServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void>
callAuthServer(
request: Request,
response: Response,

View File

@@ -3,6 +3,114 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.67.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.2...@standardnotes/auth-server@1.67.3) (2022-12-15)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.67.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.1...@standardnotes/auth-server@1.67.2) (2022-12-15)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.67.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.0...@standardnotes/auth-server@1.67.1) (2022-12-12)
### Bug Fixes
* user signed in email template ([c15e2e2](https://github.com/standardnotes/server/commit/c15e2e2c8f3a6c177e227d25440501fa38dd3d0e))
# [1.67.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.9...@standardnotes/auth-server@1.67.0) (2022-12-12)
### Features
* **auth:** add email subscription unsubscribed event handler ([10e2a26](https://github.com/standardnotes/server/commit/10e2a263522dfa33c06940f29cb77f783f66b20c))
## [1.66.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.8...@standardnotes/auth-server@1.66.9) (2022-12-12)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.66.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.7...@standardnotes/auth-server@1.66.8) (2022-12-12)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.66.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.6...@standardnotes/auth-server@1.66.7) (2022-12-09)
### Bug Fixes
* **auth:** linter issue ([104313c](https://github.com/standardnotes/server/commit/104313c15df79f6308d23e21f65111e5bd3d9c72))
## [1.66.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.5...@standardnotes/auth-server@1.66.6) (2022-12-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.66.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.4...@standardnotes/auth-server@1.66.5) (2022-12-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.66.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.66.3...@standardnotes/auth-server@1.66.4) (2022-12-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [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
## [1.63.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.63.0...@standardnotes/auth-server@1.63.1) (2022-12-07)
### Bug Fixes

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.1",
"version": "1.67.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

@@ -193,6 +193,7 @@ import { SubscriptionInvitesController } from '../Controller/SubscriptionInvites
import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
import { ProcessUserRequest } from '../Domain/UseCase/ProcessUserRequest/ProcessUserRequest'
import { UserRequestsController } from '../Controller/UserRequestsController'
import { EmailSubscriptionUnsubscribedEventHandler } from '../Domain/Handler/EmailSubscriptionUnsubscribedEventHandler'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -560,6 +561,15 @@ export class ContainerConfigLoader {
)
}
container
.bind<EmailSubscriptionUnsubscribedEventHandler>(TYPES.EmailSubscriptionUnsubscribedEventHandler)
.toConstantValue(
new EmailSubscriptionUnsubscribedEventHandler(
container.get(TYPES.UserRepository),
container.get(TYPES.SettingService),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
@@ -582,6 +592,7 @@ export class ContainerConfigLoader {
],
['SHARED_SUBSCRIPTION_INVITATION_CREATED', container.get(TYPES.SharedSubscriptionInvitationCreatedEventHandler)],
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.PredicateVerificationRequestedEventHandler)],
['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.EmailSubscriptionUnsubscribedEventHandler)],
])
if (env.get('SQS_QUEUE_URL', true)) {

View File

@@ -138,6 +138,7 @@ const TYPES = {
UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for('UserDisabledSessionUserAgentLoggingEventHandler'),
SharedSubscriptionInvitationCreatedEventHandler: Symbol.for('SharedSubscriptionInvitationCreatedEventHandler'),
PredicateVerificationRequestedEventHandler: Symbol.for('PredicateVerificationRequestedEventHandler'),
EmailSubscriptionUnsubscribedEventHandler: Symbol.for('EmailSubscriptionUnsubscribedEventHandler'),
// Services
DeviceDetector: Symbol.for('DeviceDetector'),
SessionService: Symbol.for('SessionService'),

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

@@ -0,0 +1,109 @@
import { EmailLevel } from '@standardnotes/domain-core'
import { EmailSubscriptionUnsubscribedEvent } from '@standardnotes/domain-events'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { EmailSubscriptionUnsubscribedEventHandler } from './EmailSubscriptionUnsubscribedEventHandler'
describe('EmailSubscriptionUnsubscribedEventHandler', () => {
let userRepository: UserRepositoryInterface
let settingsService: SettingServiceInterface
let event: EmailSubscriptionUnsubscribedEvent
const createHandler = () => new EmailSubscriptionUnsubscribedEventHandler(userRepository, settingsService)
beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByEmail = jest.fn().mockReturnValue({} as jest.Mocked<User>)
settingsService = {} as jest.Mocked<SettingServiceInterface>
settingsService.createOrReplace = jest.fn()
event = {
payload: {
userEmail: 'test@test.te',
level: EmailLevel.LEVELS.Marketing,
},
} as jest.Mocked<EmailSubscriptionUnsubscribedEvent>
})
it('should not do anything if user is not found', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
await createHandler().handle(event)
expect(settingsService.createOrReplace).not.toHaveBeenCalled()
})
it('should update user marketing email settings', async () => {
await createHandler().handle(event)
expect(settingsService.createOrReplace).toHaveBeenCalledWith({
user: {},
props: {
name: 'MUTE_MARKETING_EMAILS',
unencryptedValue: 'muted',
sensitive: false,
},
})
})
it('should update user sign in email settings', async () => {
event.payload.level = EmailLevel.LEVELS.SignIn
await createHandler().handle(event)
expect(settingsService.createOrReplace).toHaveBeenCalledWith({
user: {},
props: {
name: 'MUTE_SIGN_IN_EMAILS',
unencryptedValue: 'muted',
sensitive: false,
},
})
})
it('should update user email backup email settings', async () => {
event.payload.level = EmailLevel.LEVELS.FailedEmailBackup
await createHandler().handle(event)
expect(settingsService.createOrReplace).toHaveBeenCalledWith({
user: {},
props: {
name: 'MUTE_FAILED_BACKUPS_EMAILS',
unencryptedValue: 'muted',
sensitive: false,
},
})
})
it('should update user email backup email settings', async () => {
event.payload.level = EmailLevel.LEVELS.FailedCloudBackup
await createHandler().handle(event)
expect(settingsService.createOrReplace).toHaveBeenCalledWith({
user: {},
props: {
name: 'MUTE_FAILED_CLOUD_BACKUPS_EMAILS',
unencryptedValue: 'muted',
sensitive: false,
},
})
})
it('should throw error for unrecognized level', async () => {
event.payload.level = 'foobar'
let caughtError = null
try {
await createHandler().handle(event)
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
})

View File

@@ -0,0 +1,41 @@
import { EmailLevel } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, EmailSubscriptionUnsubscribedEvent } from '@standardnotes/domain-events'
import { SettingName } from '@standardnotes/settings'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
export class EmailSubscriptionUnsubscribedEventHandler implements DomainEventHandlerInterface {
constructor(private userRepository: UserRepositoryInterface, private settingsService: SettingServiceInterface) {}
async handle(event: EmailSubscriptionUnsubscribedEvent): Promise<void> {
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
if (user === null) {
return
}
await this.settingsService.createOrReplace({
user,
props: {
name: this.getSettingNameFromLevel(event.payload.level),
unencryptedValue: 'muted',
sensitive: false,
},
})
}
private getSettingNameFromLevel(level: string): string {
switch (level) {
case EmailLevel.LEVELS.FailedCloudBackup:
return SettingName.MuteFailedCloudBackupsEmails
case EmailLevel.LEVELS.FailedEmailBackup:
return SettingName.MuteFailedBackupsEmails
case EmailLevel.LEVELS.Marketing:
return SettingName.MuteMarketingEmails
case EmailLevel.LEVELS.SignIn:
return SettingName.MuteSignInEmails
default:
throw new Error(`Unknown level: ${level}`)
}
}
}

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,7 @@ describe('CreateOfflineSubscriptionToken', () => {
domainEventPublisher.publish = jest.fn()
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createOfflineSubscriptionTokenCreatedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<OfflineSubscriptionTokenCreatedEvent>)
domainEventFactory.createEmailRequestedEvent = jest.fn().mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
timer = {} as jest.Mocked<TimerInterface>
timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(1)
@@ -71,10 +69,7 @@ describe('CreateOfflineSubscriptionToken', () => {
expiresAt: 1,
})
expect(domainEventFactory.createOfflineSubscriptionTokenCreatedEvent).toHaveBeenCalledWith(
'random-string',
'test@test.com',
)
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
@@ -91,7 +86,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 +105,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 +124,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,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.11.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.10.0...@standardnotes/domain-core@1.11.0) (2022-12-15)
### Features
* **domain-core:** add legacy session model ([4084f2f](https://github.com/standardnotes/server/commit/4084f2f5ecf8379ff69d619d3d12495c010c3980))
# [1.10.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.9.0...@standardnotes/domain-core@1.10.0) (2022-12-15)
### Features
* **domain-core:** add session model ([1c4d4c5](https://github.com/standardnotes/server/commit/1c4d4c57dea1187dc130f1ae8b7dc8ede332320f))
# [1.9.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.8.0...@standardnotes/domain-core@1.9.0) (2022-12-07)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-core",
"version": "1.9.0",
"version": "1.11.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -23,10 +23,6 @@
"test": "jest spec --coverage --passWithNoTests"
},
"dependencies": {
"@standardnotes/common": "workspace:*",
"@standardnotes/features": "^1.52.1",
"@standardnotes/predicates": "workspace:*",
"@standardnotes/security": "workspace:*",
"reflect-metadata": "^0.1.13",
"shallow-equal-object": "^1.1.1",
"uuid": "^9.0.0"

View File

@@ -0,0 +1,16 @@
import { LegacySession } from './LegacySession'
describe('LegacySession', () => {
it('should create a value object', () => {
const valueOrError = LegacySession.create('foobar')
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().accessToken).toEqual('foobar')
})
it('should not create an invalid value object', () => {
const valueOrError = LegacySession.create('')
expect(valueOrError.isFailed()).toBeTruthy()
})
})

View File

@@ -0,0 +1,22 @@
import { ValueObject } from '../Core/ValueObject'
import { Result } from '../Core/Result'
import { LegacySessionProps } from './LegacySessionProps'
import { Validator } from '../Core/Validator'
export class LegacySession extends ValueObject<LegacySessionProps> {
get accessToken(): string {
return this.props.token
}
private constructor(props: LegacySessionProps) {
super(props)
}
static create(token: string): Result<LegacySession> {
if (Validator.isNotEmpty(token).isFailed()) {
return Result.fail<LegacySession>('Could not create legacy session. Token value is empty')
}
return Result.ok<LegacySession>(new LegacySession({ token }))
}
}

View File

@@ -0,0 +1,3 @@
export interface LegacySessionProps {
token: string
}

View File

@@ -0,0 +1,26 @@
import { Session } from './Session'
import { SessionToken } from './SessionToken'
describe('Session', () => {
it('should create a session value object', () => {
const accessToken = SessionToken.create('foobar1', 1234567890).getValue()
const refreshToken = SessionToken.create('foobar2', 1234567890).getValue()
const valueOrError = Session.create(accessToken, refreshToken)
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().accessToken.value).toEqual('foobar1')
expect(valueOrError.getValue().refreshToken.value).toEqual('foobar2')
expect(valueOrError.getValue().isReadOnly()).toEqual(false)
})
it('should create a session reado-only value object', () => {
const accessToken = SessionToken.create('foobar', 1234567890).getValue()
const refreshToken = SessionToken.create('foobar', 1234567890).getValue()
const valueOrError = Session.create(accessToken, refreshToken, true)
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().isReadOnly()).toEqual(true)
})
})

View File

@@ -0,0 +1,26 @@
import { ValueObject } from '../Core/ValueObject'
import { Result } from '../Core/Result'
import { SessionProps } from './SessionProps'
import { SessionToken } from './SessionToken'
export class Session extends ValueObject<SessionProps> {
get accessToken(): SessionToken {
return this.props.accessToken
}
get refreshToken(): SessionToken {
return this.props.refreshToken
}
isReadOnly(): boolean {
return this.props.readonlyAccess || false
}
private constructor(props: SessionProps) {
super(props)
}
static create(accessToken: SessionToken, refreshToken: SessionToken, readonlyAccess?: boolean): Result<Session> {
return Result.ok<Session>(new Session({ accessToken, refreshToken, readonlyAccess }))
}
}

View File

@@ -0,0 +1,7 @@
import { SessionToken } from './SessionToken'
export interface SessionProps {
accessToken: SessionToken
refreshToken: SessionToken
readonlyAccess?: boolean
}

View File

@@ -0,0 +1,21 @@
import { SessionToken } from './SessionToken'
describe('SessionToken', () => {
it('should create a value object', () => {
const valueOrError = SessionToken.create('foobar', 1234567890)
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual('foobar')
expect(valueOrError.getValue().expiresAt).toEqual(1234567890)
})
it('should not create an invalid value object', () => {
let valueOrError = SessionToken.create('', 1234567890)
expect(valueOrError.isFailed()).toBeTruthy()
valueOrError = SessionToken.create('foobar', undefined as unknown as number)
expect(valueOrError.isFailed()).toBeTruthy()
})
})

View File

@@ -0,0 +1,29 @@
import { ValueObject } from '../Core/ValueObject'
import { Result } from '../Core/Result'
import { SessionTokenProps } from './SessionTokenProps'
import { Validator } from '../Core/Validator'
export class SessionToken extends ValueObject<SessionTokenProps> {
get value(): string {
return this.props.value
}
get expiresAt(): number {
return this.props.expiresAt
}
private constructor(props: SessionTokenProps) {
super(props)
}
static create(value: string, expiresAt: number): Result<SessionToken> {
if (Validator.isNotEmpty(value).isFailed()) {
return Result.fail<SessionToken>('Could not create session token. Token value is empty')
}
if (Validator.isNotEmpty(expiresAt).isFailed()) {
return Result.fail<SessionToken>('Could not create session token. Token expiration is empty')
}
return Result.ok<SessionToken>(new SessionToken({ value, expiresAt }))
}
}

View File

@@ -0,0 +1,4 @@
export interface SessionTokenProps {
value: string
expiresAt: number
}

View File

@@ -1,3 +1,10 @@
export * from './Auth/LegacySession'
export * from './Auth/LegacySessionProps'
export * from './Auth/Session'
export * from './Auth/SessionProps'
export * from './Auth/SessionToken'
export * from './Auth/SessionTokenProps'
export * from './Common/Dates'
export * from './Common/DatesProps'
export * from './Common/Email'

View File

@@ -3,6 +3,74 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.56](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.55...@standardnotes/domain-events-infra@1.9.56) (2022-12-12)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.55](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.54...@standardnotes/domain-events-infra@1.9.55) (2022-12-12)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.54](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.53...@standardnotes/domain-events-infra@1.9.54) (2022-12-09)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.53](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.52...@standardnotes/domain-events-infra@1.9.53) (2022-12-09)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.52](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.51...@standardnotes/domain-events-infra@1.9.52) (2022-12-09)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [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
## [1.9.39](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.38...@standardnotes/domain-events-infra@1.9.39) (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.39",
"version": "1.9.56",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,106 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.104.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.104.0...@standardnotes/domain-events@2.104.1) (2022-12-12)
### Bug Fixes
* **domain-events:** add additional domain event services ([2980c42](https://github.com/standardnotes/server/commit/2980c42e88b6be5f065c91c86bf85a706975f801))
# [2.104.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.103.2...@standardnotes/domain-events@2.104.0) (2022-12-12)
### Features
* **domain-events:** add event for email subscription unsubscribed ([7f18fcf](https://github.com/standardnotes/server/commit/7f18fcfc139911620f2ea72729357aefd0613315))
## [2.103.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.103.1...@standardnotes/domain-events@2.103.2) (2022-12-09)
**Note:** Version bump only for package @standardnotes/domain-events
## [2.103.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.103.0...@standardnotes/domain-events@2.103.1) (2022-12-09)
### Bug Fixes
* **domain-events:** add additional styles option for sending email ([20d9214](https://github.com/standardnotes/server/commit/20d92149a8c559edf6aa25932b3dbcbc00b2e878))
# [2.103.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.102.0...@standardnotes/domain-events@2.103.0) (2022-12-09)
### Features
* **workspace:** replace workspace invite created event with email requested ([61c1cff](https://github.com/standardnotes/server/commit/61c1cfff4bcee09e1f933cb3e085412b6f07cc42))
# [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
* **domain-events:** add email requested event ([fa75aa4](https://github.com/standardnotes/server/commit/fa75aa40f036dc3b9b4ed1364bffdbba6dec4da4))
## [2.94.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.94.0...@standardnotes/domain-events@2.94.1) (2022-12-07)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.94.1",
"version": "2.104.1",
"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,7 +0,0 @@
import { DomainEventInterface } from './DomainEventInterface'
import { DailyAnalyticsReportGeneratedEventPayload } from './DailyAnalyticsReportGeneratedEventPayload'
export interface DailyAnalyticsReportGeneratedEvent extends DomainEventInterface {
type: 'DAILY_ANALYTICS_REPORT_GENERATED'
payload: DailyAnalyticsReportGeneratedEventPayload
}

View File

@@ -1,41 +0,0 @@
export interface DailyAnalyticsReportGeneratedEventPayload {
activityStatistics: Array<{
name: string
retention: number
totalCount: number
}>
statisticMeasures: Array<{
name: string
totalValue: number
average: number
increments: number
period: number
}>
activityStatisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
totalCount: number
}>
statisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
}>
churn: {
periodKeys: Array<string>
values: Array<{
rate: number
averageCustomersCount: number
existingCustomersChurn: number
newCustomersChurn: number
periodKey: 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

@@ -10,4 +10,7 @@ export enum DomainEventService {
Scheduler = 'scheduler',
Workspace = 'workspace',
Analytics = 'analytics',
Revisions = 'revisions',
Email = 'email',
Settings = 'settings',
}

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

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { EmailRequestedEventPayload } from './EmailRequestedEventPayload'
export interface EmailRequestedEvent extends DomainEventInterface {
type: 'EMAIL_REQUESTED'
payload: EmailRequestedEventPayload
}

View File

@@ -0,0 +1,15 @@
export interface EmailRequestedEventPayload {
userEmail: string
messageIdentifier: string
level: string
subject: string
body: string
sender?: string
additionalStyles?: string
attachments?: Array<{
filePath: string
fileName: string
attachmentFileName: string
attachmentContentType: string
}>
}

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { EmailSubscriptionUnsubscribedEventPayload } from './EmailSubscriptionUnsubscribedEventPayload'
export interface EmailSubscriptionUnsubscribedEvent extends DomainEventInterface {
type: 'EMAIL_SUBSCRIPTION_UNSUBSCRIBED'
payload: EmailSubscriptionUnsubscribedEventPayload
}

View File

@@ -0,0 +1,4 @@
export interface EmailSubscriptionUnsubscribedEventPayload {
userEmail: string
level: 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
}

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