Compare commits

..

123 Commits

Author SHA1 Message Date
standardci
9b602ed405 chore(release): publish new version
- @standardnotes/api-gateway@1.19.6
 - @standardnotes/auth-server@1.29.0
 - @standardnotes/common@1.33.0
 - @standardnotes/domain-events-infra@1.8.11
 - @standardnotes/domain-events@2.60.5
 - @standardnotes/event-store@1.3.16
 - @standardnotes/files-server@1.6.0
 - @standardnotes/predicates@1.4.2
 - @standardnotes/scheduler-server@1.10.30
 - @standardnotes/security@1.3.3
 - @standardnotes/syncing-server@1.8.6
2022-09-19 07:45:26 +00:00
Karol Sójko
db15457ce4 feat(files): add validating remote identifiers 2022-09-19 09:43:46 +02:00
standardci
719d8558a3 chore(release): publish new version
- @standardnotes/auth-server@1.28.4
2022-09-16 10:36:18 +00:00
Karol Sójko
c207c3fc84 fix(auth): feature service spec 2022-09-16 12:34:43 +02:00
standardci
4bde4758c3 chore(release): publish new version
- @standardnotes/analytics@1.29.1
 - @standardnotes/api-gateway@1.19.5
 - @standardnotes/auth-server@1.28.3
 - @standardnotes/syncing-server@1.8.5
2022-09-16 10:19:03 +00:00
Karol Sójko
5eb957c82a fix(auth): change remaining subscription time stats to percentage 2022-09-16 12:17:34 +02:00
standardci
0b38617acf chore(release): publish new version
- @standardnotes/api-gateway@1.19.4
 - @standardnotes/auth-server@1.28.2
 - @standardnotes/domain-events-infra@1.8.10
 - @standardnotes/domain-events@2.60.4
 - @standardnotes/event-store@1.3.15
 - @standardnotes/files-server@1.5.52
 - @standardnotes/scheduler-server@1.10.29
 - @standardnotes/security@1.3.2
 - @standardnotes/syncing-server@1.8.4
2022-09-16 08:55:36 +00:00
Karol Sójko
377d32c449 fix(files): add verifying permitted operation on valet token 2022-09-16 10:52:25 +02:00
standardci
cdfb0c2603 chore(release): publish new version
- @standardnotes/auth-server@1.28.1
2022-09-15 12:19:43 +00:00
Karol Sójko
d85152429c fix(auth): missing injectable annotation 2022-09-15 14:17:56 +02:00
standardci
422e596fc7 chore(release): publish new version
- @standardnotes/api-gateway@1.19.3
2022-09-15 10:39:57 +00:00
Karol Sójko
89334c9022 fix(api-gateway): add remaining subscription time to stats 2022-09-15 12:38:28 +02:00
standardci
f5a0e88ab9 chore(release): publish new version
- @standardnotes/analytics@1.29.0
 - @standardnotes/api-gateway@1.19.2
 - @standardnotes/auth-server@1.28.0
 - @standardnotes/syncing-server@1.8.3
2022-09-15 10:23:29 +00:00
Karol Sójko
a59ba08339 feat(auth): add remaining subscription time stats 2022-09-15 12:21:59 +02:00
standardci
2641056c51 chore(release): publish new version
- @standardnotes/auth-server@1.27.0
2022-09-15 10:14:51 +00:00
Karol Sójko
5d812befc4 feat(auth): implement subscription server interface on server side 2022-09-15 12:12:50 +02:00
standardci
1c592d6f96 chore(release): publish new version
- @standardnotes/auth-server@1.26.1
2022-09-15 08:44:32 +00:00
Karol Sójko
531f13fe1f fix(auth): disallow duplicating subscription invites 2022-09-15 10:43:07 +02:00
standardci
4757cc8dae chore(release): publish new version
- @standardnotes/syncing-server@1.8.2
2022-09-15 08:27:49 +00:00
Karol Sójko
ecdfe9ecc0 fix(syncing-server): files count stats 2022-09-15 10:25:55 +02:00
standardci
d19cb08e9c chore(release): publish new version
- @standardnotes/auth-server@1.26.0
2022-09-13 13:48:14 +00:00
Karol Sójko
f45320e5ed feat(auth): add subscription sharing permission 2022-09-13 15:46:11 +02:00
standardci
93ded34de9 chore(release): publish new version
- @standardnotes/auth-server@1.25.13
2022-09-12 18:08:27 +00:00
Karol Sójko
dd13e2eaf7 fix(auth): add debug logs for canceling shared subscription invitations 2022-09-12 20:06:36 +02:00
standardci
1405c6f260 chore(release): publish new version
- @standardnotes/auth-server@1.25.12
2022-09-12 12:26:19 +00:00
Karol Sójko
0dab31f993 fix(auth): allow canceling shared subscription invitation before it was accepted 2022-09-12 14:24:52 +02:00
standardci
8070c70152 chore(release): publish new version
- @standardnotes/api-gateway@1.19.1
 - @standardnotes/auth-server@1.25.11
 - @standardnotes/common@1.32.0
 - @standardnotes/domain-events-infra@1.8.9
 - @standardnotes/domain-events@2.60.3
 - @standardnotes/event-store@1.3.14
 - @standardnotes/files-server@1.5.51
 - @standardnotes/predicates@1.4.1
 - @standardnotes/scheduler-server@1.10.28
 - @standardnotes/security@1.3.1
 - @standardnotes/syncing-server@1.8.1
2022-09-09 12:04:24 +00:00
Karol Sójko
c3ebb321cf feat(common): add either and only types 2022-09-09 14:02:25 +02:00
standardci
e54deb594a chore(release): publish new version
- @standardnotes/event-store@1.3.13
2022-09-09 10:13:14 +00:00
Karol Sójko
432d071ec8 fix(event-store): add missing event subscriptions 2022-09-09 12:11:45 +02:00
standardci
b9c06f1f5d chore(release): publish new version
- @standardnotes/analytics@1.28.0
 - @standardnotes/api-gateway@1.19.0
 - @standardnotes/auth-server@1.25.10
 - @standardnotes/syncing-server@1.8.0
2022-09-09 09:46:00 +00:00
Karol Sójko
52cc6462a6 feat(syncing-server): add tracking files count in stats 2022-09-09 11:44:02 +02:00
standardci
35c2afef67 chore(release): publish new version
- @standardnotes/analytics@1.27.0
 - @standardnotes/api-gateway@1.18.0
 - @standardnotes/auth-server@1.25.9
 - @standardnotes/syncing-server@1.7.1
2022-09-09 09:02:40 +00:00
Karol Sójko
339c86fca0 fix(api-gateway): add general activity breakdown to yesterdays report stats 2022-09-09 11:01:06 +02:00
Karol Sójko
0afd3de977 feat(api-gateway): add tracking general activity for free and paid users breakdown 2022-09-09 10:59:46 +02:00
standardci
e699569d46 chore(release): publish new version
- @standardnotes/api-gateway@1.17.4
2022-09-09 08:51:10 +00:00
Karol Sójko
ced852d9db fix(api-gateway): add notes count statistics to report 2022-09-09 10:49:15 +02:00
standardci
a63612613e chore(release): publish new version
- @standardnotes/analytics@1.26.0
 - @standardnotes/api-gateway@1.17.3
 - @standardnotes/auth-server@1.25.8
 - @standardnotes/syncing-server@1.7.0
2022-09-09 08:44:23 +00:00
Karol Sójko
c9ec7b492a feat(syncing-server): add statistics for notes count for free and paid users 2022-09-09 10:42:12 +02:00
standardci
bf8ffc07ee chore(release): publish new version
- @standardnotes/event-store@1.3.12
2022-09-08 14:11:09 +00:00
Karol Sójko
73e1ea7f93 fix(event-store): add listening to refund processed event 2022-09-08 16:08:57 +02:00
standardci
5979b99398 chore(release): publish new version
- @standardnotes/api-gateway@1.17.2
 - @standardnotes/auth-server@1.25.7
 - @standardnotes/domain-events-infra@1.8.8
 - @standardnotes/domain-events@2.60.2
 - @standardnotes/event-store@1.3.11
 - @standardnotes/files-server@1.5.50
 - @standardnotes/scheduler-server@1.10.27
 - @standardnotes/syncing-server@1.6.70
2022-09-08 13:27:38 +00:00
Karol Sójko
50ddb918cc fix(api-gateway): retention data structure to include both period keys 2022-09-08 15:25:41 +02:00
standardci
6b19eb8876 chore(release): publish new version
- @standardnotes/api-gateway@1.17.1
 - @standardnotes/auth-server@1.25.6
 - @standardnotes/domain-events-infra@1.8.7
 - @standardnotes/domain-events@2.60.1
 - @standardnotes/event-store@1.3.10
 - @standardnotes/files-server@1.5.49
 - @standardnotes/scheduler-server@1.10.26
 - @standardnotes/syncing-server@1.6.69
2022-09-08 13:13:13 +00:00
Karol Sójko
47be0841fc fix(api-gateway): retention data structure 2022-09-08 15:11:12 +02:00
standardci
99c7bb70fc chore(release): publish new version
- @standardnotes/api-gateway@1.17.0
 - @standardnotes/auth-server@1.25.5
 - @standardnotes/domain-events-infra@1.8.6
 - @standardnotes/domain-events@2.60.0
 - @standardnotes/event-store@1.3.9
 - @standardnotes/files-server@1.5.48
 - @standardnotes/scheduler-server@1.10.25
 - @standardnotes/syncing-server@1.6.68
2022-09-08 09:54:52 +00:00
Karol Sójko
f139bb0036 feat(api-gateway): add registration-to-activity retention analytics to report 2022-09-08 11:52:44 +02:00
standardci
23f592ca24 chore(release): publish new version
- @standardnotes/api-gateway@1.16.8
 - @standardnotes/auth-server@1.25.4
 - @standardnotes/domain-events-infra@1.8.5
 - @standardnotes/domain-events@2.59.3
 - @standardnotes/event-store@1.3.8
 - @standardnotes/files-server@1.5.47
 - @standardnotes/scheduler-server@1.10.24
 - @standardnotes/syncing-server@1.6.67
2022-09-08 08:33:27 +00:00
Karol Sójko
fe4821d4f7 Revert "fix(domain-events): add boolean for sending emails on refund processed"
This reverts commit d7e6758089.
2022-09-08 10:32:00 +02:00
standardci
c338d4fec5 chore(release): publish new version
- @standardnotes/api-gateway@1.16.7
 - @standardnotes/auth-server@1.25.3
 - @standardnotes/domain-events-infra@1.8.4
 - @standardnotes/domain-events@2.59.2
 - @standardnotes/event-store@1.3.7
 - @standardnotes/files-server@1.5.46
 - @standardnotes/scheduler-server@1.10.23
 - @standardnotes/syncing-server@1.6.66
2022-09-08 08:05:47 +00:00
Karol Sójko
d7e6758089 fix(domain-events): add boolean for sending emails on refund processed 2022-09-08 10:03:41 +02:00
standardci
0ad62636b9 chore(release): publish new version
- @standardnotes/analytics@1.25.0
 - @standardnotes/api-gateway@1.16.6
 - @standardnotes/auth-server@1.25.2
 - @standardnotes/syncing-server@1.6.65
2022-09-07 14:24:28 +00:00
Karol Sójko
f872c7dfe9 feat(analytics): add discrete period key generation for last 7 days 2022-09-07 16:22:40 +02:00
standardci
c5fdd59eb1 chore(release): publish new version
- @standardnotes/analytics@1.24.0
 - @standardnotes/api-gateway@1.16.5
 - @standardnotes/auth-server@1.25.1
 - @standardnotes/syncing-server@1.6.64
2022-09-07 14:18:45 +00:00
Karol Sójko
7132dc3ac0 feat(analytics): add calculation retention for two activities 2022-09-07 16:16:27 +02:00
standardci
956d5be959 chore(release): publish new version
- @standardnotes/api-gateway@1.16.4
2022-09-07 13:43:04 +00:00
Karol Sójko
936591d40b fix(api-gateway): add registration-to-subscription time to analytics report 2022-09-07 15:41:02 +02:00
standardci
686e4f8ddf chore(release): publish new version
- @standardnotes/analytics@1.23.0
 - @standardnotes/api-gateway@1.16.3
 - @standardnotes/auth-server@1.25.0
 - @standardnotes/syncing-server@1.6.63
2022-09-07 12:36:46 +00:00
Karol Sójko
b61825235e feat(auth): add measuring registration to subscription time statistics 2022-09-07 14:34:45 +02:00
standardci
8157f324a0 chore(release): publish new version
- @standardnotes/auth-server@1.24.4
2022-09-07 08:37:22 +00:00
Karol Sójko
132b617aaa fix(auth): forbid users on shared subscription to send out invitations 2022-09-07 10:35:06 +02:00
standardci
25b1f3e9ea chore(release): publish new version
- @standardnotes/api-gateway@1.16.2
 - @standardnotes/auth-server@1.24.3
 - @standardnotes/domain-events-infra@1.8.3
 - @standardnotes/domain-events@2.59.1
 - @standardnotes/event-store@1.3.6
 - @standardnotes/files-server@1.5.45
 - @standardnotes/scheduler-server@1.10.22
 - @standardnotes/syncing-server@1.6.62
2022-09-06 10:30:52 +00:00
Karol Sójko
f94c8fc26e fix(api-gateway): period types on analytics report 2022-09-06 12:28:48 +02:00
Karol Sójko
d149f46cf6 fix(api-gateway): include period key in statistics measures 2022-09-06 12:16:07 +02:00
standardci
6a24ba5d56 chore(release): publish new version
- @standardnotes/api-gateway@1.16.1
 - @standardnotes/auth-server@1.24.2
 - @standardnotes/event-store@1.3.5
 - @standardnotes/files-server@1.5.44
 - @standardnotes/scheduler-server@1.10.21
 - @standardnotes/syncing-server@1.6.61
 - @standardnotes/time@1.11.0
2022-09-06 10:10:20 +00:00
Karol Sójko
db8333a858 feat(time): add converting microseconds to time structure 2022-09-06 12:08:39 +02:00
standardci
3af254d7c7 chore(release): publish new version
- @standardnotes/api-gateway@1.16.0
 - @standardnotes/auth-server@1.24.1
 - @standardnotes/domain-events-infra@1.8.2
 - @standardnotes/domain-events@2.59.0
 - @standardnotes/event-store@1.3.4
 - @standardnotes/files-server@1.5.43
 - @standardnotes/scheduler-server@1.10.20
 - @standardnotes/syncing-server@1.6.60
2022-09-06 09:28:23 +00:00
Karol Sójko
8151bb108a feat(api-gateway): add statistics measures to report generation 2022-09-06 11:26:27 +02:00
Karol Sójko
3b18769c2d chore(deps): upgrade uuid 2022-09-06 11:26:27 +02:00
standardci
2883cac6d4 chore(release): publish new version
- @standardnotes/analytics@1.22.0
 - @standardnotes/api-gateway@1.15.12
 - @standardnotes/auth-server@1.24.0
 - @standardnotes/domain-events-infra@1.8.1
 - @standardnotes/domain-events@2.58.0
 - @standardnotes/event-store@1.3.3
 - @standardnotes/files-server@1.5.42
 - @standardnotes/scheduler-server@1.10.19
 - @standardnotes/syncing-server@1.6.59
2022-09-06 08:56:45 +00:00
Karol Sójko
d7ae2f0625 feat(auth): add statistics for refunds and account deletions 2022-09-06 10:54:50 +02:00
standardci
318f6d0986 chore(release): publish new version
- @standardnotes/auth-server@1.23.2
2022-09-06 07:42:31 +00:00
Karol Sójko
2ca430f40c fix(auth): add debug logs for subscription canceling 2022-09-06 09:40:57 +02:00
standardci
fd65060a8e chore(release): publish new version
- @standardnotes/analytics@1.21.1
 - @standardnotes/api-gateway@1.15.11
 - @standardnotes/auth-server@1.23.1
 - @standardnotes/syncing-server@1.6.58
2022-09-06 07:36:07 +00:00
Karol Sójko
cb81f819ba fix(analytics): increment by float instead of integer on measures 2022-09-06 09:34:13 +02:00
standardci
61c7040e4b chore(release): publish new version
- @standardnotes/analytics@1.21.0
 - @standardnotes/api-gateway@1.15.10
 - @standardnotes/auth-server@1.23.0
 - @standardnotes/syncing-server@1.6.57
2022-09-05 19:14:47 +00:00
Karol Sójko
fa10827443 feat(auth): add measuring subscription length 2022-09-05 21:13:07 +02:00
standardci
bcee779e74 chore(release): publish new version
- @standardnotes/auth-server@1.22.1
2022-09-05 15:31:41 +00:00
Karol Sójko
34315c91d7 fix(auth): StatisticsStore binding 2022-09-05 17:29:34 +02:00
standardci
8d3bf6c4a5 chore(release): publish new version
- @standardnotes/analytics@1.20.0
 - @standardnotes/api-gateway@1.15.9
 - @standardnotes/auth-server@1.22.0
 - @standardnotes/common@1.31.0
 - @standardnotes/domain-events-infra@1.8.0
 - @standardnotes/domain-events@2.57.0
 - @standardnotes/event-store@1.3.2
 - @standardnotes/files-server@1.5.41
 - @standardnotes/predicates@1.4.0
 - @standardnotes/scheduler-server@1.10.18
 - @standardnotes/security@1.3.0
 - @standardnotes/sncrypto-node@1.11.0
 - @standardnotes/syncing-server@1.6.56
 - @standardnotes/time@1.10.0
2022-09-05 14:57:25 +00:00
Karol Sójko
0c176b70f8 feat(auth): add keeping stats on payments 2022-09-05 16:55:53 +02:00
standardci
87a5854357 chore(release): publish new version
- @standardnotes/api-gateway@1.15.8
 - @standardnotes/auth-server@1.21.5
 - @standardnotes/domain-events-infra@1.7.37
 - @standardnotes/domain-events@2.56.0
 - @standardnotes/event-store@1.3.1
 - @standardnotes/files-server@1.5.40
 - @standardnotes/scheduler-server@1.10.17
 - @standardnotes/syncing-server@1.6.55
2022-09-05 14:47:52 +00:00
Karol Sójko
9c2d51d718 feat(domain-events): add amount of dollars to payment success event 2022-09-05 16:46:20 +02:00
standardci
e618f046ea chore(release): publish new version
- @standardnotes/analytics@1.19.0
 - @standardnotes/api-gateway@1.15.7
 - @standardnotes/auth-server@1.21.4
 - @standardnotes/syncing-server@1.6.54
2022-09-05 14:41:15 +00:00
Karol Sójko
a36cb925ff feat(analytics): add statistics measurements tracking 2022-09-05 16:39:44 +02:00
standardci
9e2aea2793 chore(release): publish new version
- @standardnotes/event-store@1.3.0
2022-09-02 09:12:05 +00:00
Karol Sójko
ef1e2bb5ed feat(event-store): add listening to subscription reverts 2022-09-02 11:10:33 +02:00
standardci
6a457281ea chore(release): publish new version
- @standardnotes/api-gateway@1.15.6
 - @standardnotes/auth-server@1.21.3
 - @standardnotes/domain-events-infra@1.7.36
 - @standardnotes/domain-events@2.55.1
 - @standardnotes/event-store@1.2.3
 - @standardnotes/files-server@1.5.39
 - @standardnotes/scheduler-server@1.10.16
 - @standardnotes/syncing-server@1.6.53
2022-09-01 10:11:46 +00:00
Karol Sójko
41c512798d fix(domain-events): add admin-panel as event source option 2022-09-01 12:10:00 +02:00
standardci
ffa0f51305 chore(release): publish new version
- @standardnotes/api-gateway@1.15.5
 - @standardnotes/auth-server@1.21.2
 - @standardnotes/domain-events-infra@1.7.35
 - @standardnotes/domain-events@2.55.0
 - @standardnotes/event-store@1.2.2
 - @standardnotes/files-server@1.5.38
 - @standardnotes/scheduler-server@1.10.15
 - @standardnotes/syncing-server@1.6.52
2022-09-01 09:02:44 +00:00
Karol Sójko
e0cec9e24a feat(domain-events): add subscription revert requested event 2022-09-01 11:00:45 +02:00
standardci
f6b359a772 chore(release): publish new version
- @standardnotes/scheduler-server@1.10.14
2022-08-30 12:53:27 +00:00
Karol Sójko
648eb89c7c fix(scheduler): linter issues 2022-08-30 14:51:54 +02:00
standardci
ba22e085b8 chore(release): publish new version
- @standardnotes/scheduler-server@1.10.13
2022-08-30 12:46:35 +00:00
Karol Sójko
35373db1d3 fix(scheduler): skip sending exit survery when a subscription is replaced 2022-08-30 14:44:54 +02:00
standardci
932cfa7200 chore(release): publish new version
- @standardnotes/api-gateway@1.15.4
 - @standardnotes/auth-server@1.21.1
 - @standardnotes/domain-events-infra@1.7.34
 - @standardnotes/domain-events@2.54.2
 - @standardnotes/event-store@1.2.1
 - @standardnotes/files-server@1.5.37
 - @standardnotes/scheduler-server@1.10.12
 - @standardnotes/syncing-server@1.6.51
2022-08-30 12:35:32 +00:00
Karol Sójko
932ef933fc fix(domain-events): add replaced boolean to subscription canceled event 2022-08-30 14:33:59 +02:00
standardci
4f1293525c chore(release): publish new version
- @standardnotes/event-store@1.2.0
2022-08-30 08:02:20 +00:00
Karol Sójko
dd6bec8a0c feat(event-store): add account claim events subscription 2022-08-30 10:00:48 +02:00
standardci
1abca64765 chore(release): publish new version
- @standardnotes/scheduler-server@1.10.11
2022-08-29 12:40:41 +00:00
Karol Sójko
dbe55d89ec fix(scheduler): change logs severity to reduce noise 2022-08-29 14:39:04 +02:00
Karol Sójko
dcb3ad661c chore(setup): node version setting up on workflows 2022-08-29 14:20:39 +02:00
standardci
1e1f6cb4a3 chore(release): publish new version
- @standardnotes/files-server@1.5.36
2022-08-29 11:59:12 +00:00
Karol Sójko
83d96fd71d chore(deps): upgrade ts-node 2022-08-29 13:57:43 +02:00
standardci
7dc4670028 chore(release): publish new version
- @standardnotes/auth-server@1.21.0
2022-08-29 11:20:16 +00:00
Karol Sójko
dc88e2413b feat(auth): retain user agent, api version and received at on revoked sessions 2022-08-29 13:18:05 +02:00
standardci
b7f7c3f164 chore(release): publish new version
- @standardnotes/api-gateway@1.15.3
2022-08-22 08:37:14 +00:00
Karol Sójko
f7def38e20 fix(api-gateway): add error logs on missing connection id for websockets 2022-08-22 10:35:38 +02:00
Karol Sójko
cf49e1ff74 chore(deps): remove reviewers assignment from dependabot to avoid noise 2022-08-17 10:06:22 +02:00
standardci
38de2d6b30 chore(release): publish new version
- @standardnotes/auth-server@1.20.1
 - @standardnotes/scheduler-server@1.10.10
2022-08-17 06:53:26 +00:00
Karol Sójko
4b3de264ef fix(scheduler): change discount from 10% to 20% on a limited time offer 2022-08-17 08:51:35 +02:00
standardci
4bb785c7f0 chore(release): publish new version
- @standardnotes/analytics@1.18.1
 - @standardnotes/api-gateway@1.15.2
 - @standardnotes/auth-server@1.20.0
 - @standardnotes/domain-events-infra@1.7.33
 - @standardnotes/domain-events@2.54.1
 - @standardnotes/event-store@1.1.33
 - @standardnotes/files-server@1.5.35
 - @standardnotes/scheduler-server@1.10.9
 - @standardnotes/syncing-server@1.6.50
2022-08-15 16:37:33 +00:00
Karol Sójko
2fb904d2cb fix(domain-events): missing exports 2022-08-15 18:35:52 +02:00
Karol Sójko
ee79347e27 fix(api-gateway): add payment success events to report 2022-08-15 18:27:44 +02:00
Karol Sójko
3477c81d37 fix(event-store): add payment events handling 2022-08-15 18:26:41 +02:00
Karol Sójko
930789316c fix(analytics): add payment success activity 2022-08-15 18:24:53 +02:00
Karol Sójko
01a08eae58 feat(auth): add payment success event handler 2022-08-15 18:24:13 +02:00
standardci
d73c9833ab chore(release): publish new version
- @standardnotes/api-gateway@1.15.1
 - @standardnotes/auth-server@1.19.1
 - @standardnotes/domain-events-infra@1.7.32
 - @standardnotes/domain-events@2.54.0
 - @standardnotes/event-store@1.1.32
 - @standardnotes/files-server@1.5.34
 - @standardnotes/scheduler-server@1.10.8
 - @standardnotes/syncing-server@1.6.49
2022-08-15 15:03:28 +00:00
Karol Sójko
1841597405 feat(domain-events): add payment success event 2022-08-15 17:01:32 +02:00
standardci
8003e5ce43 chore(release): publish new version
- @standardnotes/api-gateway@1.15.0
2022-08-15 12:39:40 +00:00
Karol Sójko
d0023a6c92 feat(api-gateway): add gathering analytics for failed payments 2022-08-15 14:38:04 +02:00
standardci
a9293f6ce1 chore(release): publish new version
- @standardnotes/analytics@1.18.0
 - @standardnotes/api-gateway@1.14.3
 - @standardnotes/auth-server@1.19.0
 - @standardnotes/syncing-server@1.6.48
2022-08-15 11:12:04 +00:00
Karol Sójko
58c5b586a9 feat(auth): add payment failed event handler 2022-08-15 13:10:36 +02:00
159 changed files with 3306 additions and 439 deletions

View File

@@ -9,134 +9,83 @@ updates:
directory: "/"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/analytics"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/api-gateway"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/auth"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/common"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/domain-events"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/domain-events-infra"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/event-store"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/files"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/predicates"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/scheduler"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/security"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/settings"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/sncrypto-node"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/syncing-server"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "npm"
directory: "/packages/time"
schedule:
interval: "daily"
reviewers:
- "moughxyz"
- "karolsojko"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
reviewers:
- "moughxyz"
- "karolsojko"

View File

@@ -60,6 +60,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build locally
run: yarn build
- name: Configure AWS credentials
@@ -90,6 +95,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build locally
run: yarn build
- name: Login to Docker Hub

View File

@@ -63,6 +63,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build locally
run: yarn build
- name: Configure AWS credentials
@@ -93,6 +98,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build locally
run: yarn build
- name: Login to Docker Hub

View File

@@ -32,6 +32,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build locally
run: yarn build
- name: Configure AWS credentials
@@ -62,6 +67,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build locally
run: yarn build
- name: Login to Docker Hub

View File

@@ -63,6 +63,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build locally
run: yarn build
- name: Configure AWS credentials
@@ -93,6 +98,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build locally
run: yarn build
- name: Login to Docker Hub

View File

@@ -11,6 +11,11 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: ESLint
run: yarn lint
- name: Build

View File

@@ -32,6 +32,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build locally
run: yarn build
- name: Configure AWS credentials
@@ -62,6 +67,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build locally
run: yarn build
- name: Login to Docker Hub

View File

@@ -14,9 +14,11 @@ jobs:
with:
ref: main
token: ${{ secrets.CI_PAT_TOKEN }}
- uses: actions/setup-node@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Setup git config
run: |
git config --global user.name "standardci"
@@ -43,4 +45,4 @@ jobs:
commit-message: "${{ 'chore(deps): upgrade snjs' }}"
delete-branch: true
committer: standardci <ci@standardnotes.com>
author: standardci <ci@standardnotes.com>
author: standardci <ci@standardnotes.com>

View File

@@ -63,6 +63,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build locally
run: yarn build
- name: Configure AWS credentials
@@ -93,6 +98,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build locally
run: yarn build
- name: Login to Docker Hub

183
.pnp.cjs generated
View File

@@ -124,7 +124,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["newrelic", "npm:9.0.0"],\
["npm-check-updates", "npm:16.0.1"],\
["prettier", "npm:2.7.1"],\
["ts-node", "virtual:c0eab07e71af57f5501e97e7ca7a2a4f4965035bd2455ad124a8b09fa55780657c55fe3df41019fa6c2c44487c897668c842a0939e380b3c1f13b3756d128543#npm:10.8.2"],\
["ts-node", "virtual:8859b278716fedf3e7458b5628625f7e35678c418626878559a0b816445001b7e24c55546f4677ba4c20b521aa0cf52cc33ac07deff171e383ada6eeab69933f#npm:10.9.1"],\
["typescript", "patch:typescript@npm%3A4.7.4#~builtin<compat/typescript>::version=4.7.4&hash=7ad353"]\
],\
"linkType": "SOFT"\
@@ -2484,16 +2484,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/api", [\
["npm:1.1.19", {\
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.1.19-6a6d650ec9-cca168245a.zip/node_modules/@standardnotes/api/",\
["npm:1.7.2", {\
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.7.2-e68e7d4e63-bdfc414e6d.zip/node_modules/@standardnotes/api/",\
"packageDependencies": [\
["@standardnotes/api", "npm:1.1.19"],\
["@standardnotes/auth", "npm:3.19.4"],\
["@standardnotes/api", "npm:1.7.2"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/encryption", "npm:1.12.0"],\
["@standardnotes/responses", "npm:1.6.39"],\
["@standardnotes/services", "npm:1.15.0"],\
["@standardnotes/utils", "npm:1.6.12"]\
["@standardnotes/encryption", "npm:1.15.2"],\
["@standardnotes/models", "npm:1.18.2"],\
["@standardnotes/responses", "npm:1.10.1"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/utils", "npm:1.9.0"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
@@ -2506,6 +2507,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
["@sentry/node", "npm:7.5.0"],\
["@standardnotes/analytics", "workspace:packages/analytics"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/security", "workspace:packages/security"],\
@@ -2561,7 +2563,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
["@sentry/node", "npm:7.5.0"],\
["@standardnotes/analytics", "workspace:packages/analytics"],\
["@standardnotes/api", "npm:1.1.19"],\
["@standardnotes/api", "npm:1.7.2"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
@@ -2607,7 +2609,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["ts-jest", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:28.0.5"],\
["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.7"],\
["ua-parser-js", "npm:1.0.2"],\
["uuid", "npm:8.3.2"],\
["uuid", "npm:9.0.0"],\
["winston", "npm:3.8.1"]\
],\
"linkType": "SOFT"\
@@ -2686,16 +2688,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/encryption", [\
["npm:1.12.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.12.0-eb2342c675-1a28653b1e.zip/node_modules/@standardnotes/encryption/",\
["npm:1.15.2", {\
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.15.2-ef86a8281d-6e8336f1e7.zip/node_modules/@standardnotes/encryption/",\
"packageDependencies": [\
["@standardnotes/encryption", "npm:1.12.0"],\
["@standardnotes/encryption", "npm:1.15.2"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/models", "npm:1.14.0"],\
["@standardnotes/responses", "npm:1.6.39"],\
["@standardnotes/services", "npm:1.15.0"],\
["@standardnotes/sncrypto-common", "npm:1.9.0"],\
["@standardnotes/utils", "npm:1.6.12"],\
["@standardnotes/models", "npm:1.18.2"],\
["@standardnotes/responses", "npm:1.10.1"],\
["@standardnotes/sncrypto-common", "npm:1.11.1"],\
["@standardnotes/utils", "npm:1.9.0"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
@@ -2741,6 +2742,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.52.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.52.0-8c1adf7881-3e6014272f.zip/node_modules/@standardnotes/features/",\
"packageDependencies": [\
["@standardnotes/features", "npm:1.52.0"],\
["@standardnotes/auth", "npm:3.19.4"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/security", "workspace:packages/security"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/files-server", [\
@@ -2788,22 +2800,22 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["prettyjson", "npm:1.2.5"],\
["reflect-metadata", "npm:0.1.13"],\
["ts-jest", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:28.0.5"],\
["ts-node", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:10.8.2"],\
["uuid", "npm:8.3.2"],\
["ts-node", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:10.9.1"],\
["uuid", "npm:9.0.0"],\
["winston", "npm:3.8.1"]\
],\
"linkType": "SOFT"\
}]\
]],\
["@standardnotes/models", [\
["npm:1.14.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.14.0-6f064d99e7-bfb9d517b6.zip/node_modules/@standardnotes/models/",\
["npm:1.18.2", {\
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.18.2-56f35bb72d-88180a93e5.zip/node_modules/@standardnotes/models/",\
"packageDependencies": [\
["@standardnotes/models", "npm:1.14.0"],\
["@standardnotes/models", "npm:1.18.2"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.50.0"],\
["@standardnotes/responses", "npm:1.6.39"],\
["@standardnotes/utils", "npm:1.6.12"],\
["@standardnotes/features", "npm:1.52.0"],\
["@standardnotes/responses", "npm:1.10.1"],\
["@standardnotes/utils", "npm:1.9.0"],\
["lodash", "npm:4.17.21"],\
["reflect-metadata", "npm:0.1.13"]\
],\
@@ -2839,6 +2851,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/responses", [\
["npm:1.10.1", {\
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.10.1-9f82fff6c1-b84fb3f71c.zip/node_modules/@standardnotes/responses/",\
"packageDependencies": [\
["@standardnotes/responses", "npm:1.10.1"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.52.0"],\
["@standardnotes/security", "workspace:packages/security"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.6.39", {\
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.6.39-395f4c2d65-0ea1d4d5b8.zip/node_modules/@standardnotes/responses/",\
"packageDependencies": [\
@@ -2866,11 +2889,12 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@types/jest", "npm:28.1.4"],\
["@types/newrelic", "npm:7.0.3"],\
["@types/node", "npm:18.0.3"],\
["@typescript-eslint/eslint-plugin", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:5.30.5"],\
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.30.5"],\
["aws-sdk", "npm:2.1168.0"],\
["dayjs", "npm:1.11.3"],\
["dotenv", "npm:16.0.1"],\
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
["eslint", "npm:8.19.0"],\
["eslint-plugin-prettier", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.2.1"],\
["inversify", "npm:6.0.1"],\
["ioredis", "npm:5.2.0"],\
["jest", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:28.1.2"],\
@@ -2924,27 +2948,12 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["newrelic", "npm:9.0.0"],\
["npm-check-updates", "npm:16.0.1"],\
["prettier", "npm:2.7.1"],\
["ts-node", "virtual:c0eab07e71af57f5501e97e7ca7a2a4f4965035bd2455ad124a8b09fa55780657c55fe3df41019fa6c2c44487c897668c842a0939e380b3c1f13b3756d128543#npm:10.8.2"],\
["ts-node", "virtual:8859b278716fedf3e7458b5628625f7e35678c418626878559a0b816445001b7e24c55546f4677ba4c20b521aa0cf52cc33ac07deff171e383ada6eeab69933f#npm:10.9.1"],\
["typescript", "patch:typescript@npm%3A4.7.4#~builtin<compat/typescript>::version=4.7.4&hash=7ad353"]\
],\
"linkType": "SOFT"\
}]\
]],\
["@standardnotes/services", [\
["npm:1.15.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-services-npm-1.15.0-acab3bc6a3-1028a5b4c1.zip/node_modules/@standardnotes/services/",\
"packageDependencies": [\
["@standardnotes/services", "npm:1.15.0"],\
["@standardnotes/auth", "npm:3.19.4"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/models", "npm:1.14.0"],\
["@standardnotes/responses", "npm:1.6.39"],\
["@standardnotes/utils", "npm:1.6.12"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/settings", [\
["workspace:packages/settings", {\
"packageLocation": "./packages/settings/",\
@@ -2958,6 +2967,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["@standardnotes/sncrypto-common", [\
["npm:1.11.1", {\
"packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.11.1-58d12d6912-69d698abb7.zip/node_modules/@standardnotes/sncrypto-common/",\
"packageDependencies": [\
["@standardnotes/sncrypto-common", "npm:1.11.1"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.9.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.9.0-48773f745a-42252d7198.zip/node_modules/@standardnotes/sncrypto-common/",\
"packageDependencies": [\
@@ -3035,7 +3052,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["ts-jest", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:28.0.5"],\
["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.7"],\
["ua-parser-js", "npm:1.0.2"],\
["uuid", "npm:8.3.2"],\
["uuid", "npm:9.0.0"],\
["winston", "npm:3.8.1"]\
],\
"linkType": "SOFT"\
@@ -3069,6 +3086,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["lodash", "npm:4.17.21"]\
],\
"linkType": "HARD"\
}],\
["npm:1.9.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.9.0-da939553f6-4591aff48d.zip/node_modules/@standardnotes/utils/",\
"packageDependencies": [\
["@standardnotes/utils", "npm:1.9.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["dompurify", "npm:2.4.0"],\
["lodash", "npm:4.17.21"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
]],\
["@szmarczak/http-timer", [\
@@ -5842,6 +5870,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["dompurify", "npm:2.3.8"]\
],\
"linkType": "HARD"\
}],\
["npm:2.4.0", {\
"packageLocation": "./.yarn/cache/dompurify-npm-2.4.0-0ffecf22ef-c93ea73cf8.zip/node_modules/dompurify/",\
"packageDependencies": [\
["dompurify", "npm:2.4.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["dot-prop", [\
@@ -12187,10 +12222,53 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
],\
"linkType": "SOFT"\
}],\
["virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:10.8.2", {\
"packageLocation": "./.yarn/__virtual__/ts-node-virtual-8a01a45377/0/cache/ts-node-npm-10.8.2-f3c0c9eaee-1eede939be.zip/node_modules/ts-node/",\
["npm:10.9.1", {\
"packageLocation": "./.yarn/cache/ts-node-npm-10.9.1-6c268be7f4-090adff130.zip/node_modules/ts-node/",\
"packageDependencies": [\
["ts-node", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:10.8.2"],\
["ts-node", "npm:10.9.1"]\
],\
"linkType": "SOFT"\
}],\
["virtual:8859b278716fedf3e7458b5628625f7e35678c418626878559a0b816445001b7e24c55546f4677ba4c20b521aa0cf52cc33ac07deff171e383ada6eeab69933f#npm:10.9.1", {\
"packageLocation": "./.yarn/__virtual__/ts-node-virtual-ac01688ebc/0/cache/ts-node-npm-10.9.1-6c268be7f4-090adff130.zip/node_modules/ts-node/",\
"packageDependencies": [\
["ts-node", "virtual:8859b278716fedf3e7458b5628625f7e35678c418626878559a0b816445001b7e24c55546f4677ba4c20b521aa0cf52cc33ac07deff171e383ada6eeab69933f#npm:10.9.1"],\
["@cspotcode/source-map-support", "npm:0.8.1"],\
["@swc/core", null],\
["@swc/wasm", null],\
["@tsconfig/node10", "npm:1.0.9"],\
["@tsconfig/node12", "npm:1.0.11"],\
["@tsconfig/node14", "npm:1.0.3"],\
["@tsconfig/node16", "npm:1.0.3"],\
["@types/node", "npm:18.0.3"],\
["@types/swc__core", null],\
["@types/swc__wasm", null],\
["@types/typescript", null],\
["acorn", "npm:8.7.1"],\
["acorn-walk", "npm:8.2.0"],\
["arg", "npm:4.1.3"],\
["create-require", "npm:1.1.1"],\
["diff", "npm:4.0.2"],\
["make-error", "npm:1.3.6"],\
["typescript", "patch:typescript@npm%3A4.7.4#~builtin<compat/typescript>::version=4.7.4&hash=7ad353"],\
["v8-compile-cache-lib", "npm:3.0.1"],\
["yn", "npm:3.1.1"]\
],\
"packagePeers": [\
"@swc/core",\
"@swc/wasm",\
"@types/node",\
"@types/swc__core",\
"@types/swc__wasm",\
"@types/typescript",\
"typescript"\
],\
"linkType": "HARD"\
}],\
["virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:10.9.1", {\
"packageLocation": "./.yarn/__virtual__/ts-node-virtual-c4e9951caa/0/cache/ts-node-npm-10.9.1-6c268be7f4-090adff130.zip/node_modules/ts-node/",\
"packageDependencies": [\
["ts-node", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:10.9.1"],\
["@cspotcode/source-map-support", "npm:0.8.1"],\
["@swc/core", null],\
["@swc/wasm", null],\
@@ -12735,6 +12813,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["uuid", "npm:8.3.2"]\
],\
"linkType": "HARD"\
}],\
["npm:9.0.0", {\
"packageLocation": "./.yarn/cache/uuid-npm-9.0.0-46c41e3e43-8dd2c83c43.zip/node_modules/uuid/",\
"packageDependencies": [\
["uuid", "npm:9.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["v8-compile-cache", [\

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -60,7 +60,7 @@
"ini": "^3.0.0",
"npm-check-updates": "^16.0.1",
"prettier": "^2.7.1",
"ts-node": "^10.8.1",
"ts-node": "^10.9.1",
"typescript": "^4.7.4"
},
"packageManager": "yarn@3.2.1",

View File

@@ -3,6 +3,96 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.29.0...@standardnotes/analytics@1.29.1) (2022-09-16)
### Bug Fixes
* **auth:** change remaining subscription time stats to percentage ([5eb957c](https://github.com/standardnotes/server/commit/5eb957c82a8cc5fdcb6815e2cd30e49cd2b1e8ac))
# [1.29.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.28.0...@standardnotes/analytics@1.29.0) (2022-09-15)
### Features
* **auth:** add remaining subscription time stats ([a59ba08](https://github.com/standardnotes/server/commit/a59ba083397c75960af0e8a102b617bf5baa287f))
# [1.28.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.27.0...@standardnotes/analytics@1.28.0) (2022-09-09)
### Features
* **syncing-server:** add tracking files count in stats ([52cc646](https://github.com/standardnotes/server/commit/52cc6462a66dae3bd6c05f551d4ba661c8a9b8c8))
# [1.27.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.26.0...@standardnotes/analytics@1.27.0) (2022-09-09)
### Features
* **api-gateway:** add tracking general activity for free and paid users breakdown ([0afd3de](https://github.com/standardnotes/server/commit/0afd3de9779e2abe10deede24626a3cbe6b15e6c))
# [1.26.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.25.0...@standardnotes/analytics@1.26.0) (2022-09-09)
### Features
* **syncing-server:** add statistics for notes count for free and paid users ([c9ec7b4](https://github.com/standardnotes/server/commit/c9ec7b492aea1911e441ed8ad9a155f871be2ef7))
# [1.25.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.24.0...@standardnotes/analytics@1.25.0) (2022-09-07)
### Features
* **analytics:** add discrete period key generation for last 7 days ([f872c7d](https://github.com/standardnotes/server/commit/f872c7dfe9f120f40dd0c28a9e0f5749eb251643))
# [1.24.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.23.0...@standardnotes/analytics@1.24.0) (2022-09-07)
### Features
* **analytics:** add calculation retention for two activities ([7132dc3](https://github.com/standardnotes/server/commit/7132dc3ac0cf878d2c326243747343e8a6746e2f))
# [1.23.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.22.0...@standardnotes/analytics@1.23.0) (2022-09-07)
### Features
* **auth:** add measuring registration to subscription time statistics ([b618252](https://github.com/standardnotes/server/commit/b61825235eebaf5eddb55cbda173176ca43c0099))
# [1.22.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.21.1...@standardnotes/analytics@1.22.0) (2022-09-06)
### Features
* **auth:** add statistics for refunds and account deletions ([d7ae2f0](https://github.com/standardnotes/server/commit/d7ae2f06255b19eb5d3403a4989610390064754e))
## [1.21.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.21.0...@standardnotes/analytics@1.21.1) (2022-09-06)
### Bug Fixes
* **analytics:** increment by float instead of integer on measures ([cb81f81](https://github.com/standardnotes/server/commit/cb81f819ba30a45f27ec344480b5ef22e5a0a50d))
# [1.21.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.20.0...@standardnotes/analytics@1.21.0) (2022-09-05)
### Features
* **auth:** add measuring subscription length ([fa10827](https://github.com/standardnotes/server/commit/fa108274430d8dff1016ddcba5bbcb2778eb781b))
# [1.20.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.19.0...@standardnotes/analytics@1.20.0) (2022-09-05)
### Features
* **auth:** add keeping stats on payments ([0c176b7](https://github.com/standardnotes/server/commit/0c176b70f8281e1e490224b9c7ab85f272a3d4e9))
# [1.19.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.18.1...@standardnotes/analytics@1.19.0) (2022-09-05)
### Features
* **analytics:** add statistics measurements tracking ([a36cb92](https://github.com/standardnotes/server/commit/a36cb925ff3bd8396a53f58c3e954549e904d694))
## [1.18.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.18.0...@standardnotes/analytics@1.18.1) (2022-08-15)
### Bug Fixes
* **analytics:** add payment success activity ([9307893](https://github.com/standardnotes/server/commit/930789316c2eec8227f26e75d4917796168f2d08))
# [1.18.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.17.2...@standardnotes/analytics@1.18.0) (2022-08-15)
### Features
* **auth:** add payment failed event handler ([58c5b58](https://github.com/standardnotes/server/commit/58c5b586a904cf1fd179cc28783a6ae7da688063))
## [1.17.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.17.1...@standardnotes/analytics@1.17.2) (2022-08-15)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "1.17.2",
"version": "1.29.1",
"engines": {
"node": ">=14.0.0 <17.0.0"
},
@@ -23,7 +23,7 @@
"prebuild": "yarn clean",
"build": "tsc -p tsconfig.json",
"lint": "eslint . --ext .ts",
"test:unit": "jest spec --coverage"
"test": "jest spec --coverage"
},
"devDependencies": {
"@types/ioredis": "^4.28.10",

View File

@@ -1,6 +1,9 @@
export enum AnalyticsActivity {
GeneralActivity = 'general-activity',
GeneralActivityFreeUsers = 'general-activity-free-users',
GeneralActivityPaidUsers = 'general-activity-paid-users',
EditingItems = 'editing-items',
CheckingIntegrity = 'checking-integrity',
Login = 'login',
Register = 'register',
DeleteAccount = 'DeleteAccount',
@@ -11,4 +14,6 @@ export enum AnalyticsActivity {
EmailUnbackedUpData = 'email-unbacked-up-data',
EmailBackup = 'email-backup',
LimitedDiscountOfferPurchased = 'limited-discount-offer-purchased',
PaymentFailed = 'payment-failed',
PaymentSuccess = 'payment-success',
}

View File

@@ -6,6 +6,12 @@ export interface AnalyticsStoreInterface {
markActivity(activities: AnalyticsActivity[], analyticsId: number, periods: Period[]): Promise<void>
wasActivityDone(activity: AnalyticsActivity, analyticsId: number, period: Period): Promise<boolean>
calculateActivityRetention(activity: AnalyticsActivity, firstPeriod: Period, secondPeriod: Period): Promise<number>
calculateActivitiesRetention(parameters: {
firstActivity: AnalyticsActivity
firstActivityPeriodKey: string
secondActivity: AnalyticsActivity
secondActivityPeriodKey: string
}): Promise<number>
calculateActivityTotalCount(activity: AnalyticsActivity, period: Period): Promise<number>
calculateActivityChangesTotalCount(
activity: AnalyticsActivity,

View File

@@ -0,0 +1,11 @@
export enum StatisticsMeasure {
Income = 'income',
SubscriptionLength = 'subscription-length',
RegistrationLength = 'registration-length',
RegistrationToSubscriptionTime = 'registration-to-subscription-time',
RemainingSubscriptionTimePercentage = 'remaining-subscription-time-percentage',
Refunds = 'refunds',
NotesCountFreeUsers = 'notes-count-free-users',
NotesCountPaidUsers = 'notes-count-paid-users',
FilesCount = 'files-count',
}

View File

@@ -1,3 +1,6 @@
import { Period } from '../Time/Period'
import { StatisticsMeasure } from './StatisticsMeasure'
export interface StatisticsStoreInterface {
incrementSNJSVersionUsage(snjsVersion: string): Promise<void>
incrementApplicationVersionUsage(applicationVersion: string): Promise<void>
@@ -5,4 +8,7 @@ export interface StatisticsStoreInterface {
getYesterdaySNJSUsage(): Promise<Array<{ version: string; count: number }>>
getYesterdayApplicationUsage(): Promise<Array<{ version: string; count: number }>>
getYesterdayOutOfSyncIncidents(): Promise<number>
incrementMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void>
getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise<number>
getMeasureTotal(measure: StatisticsMeasure, period: Period): Promise<number>
}

View File

@@ -8,6 +8,7 @@ export enum Period {
ThisMonth,
LastMonth,
Last30Days,
Last7Days,
Q1ThisYear,
Q2ThisYear,
Q3ThisYear,

View File

@@ -48,6 +48,18 @@ describe('PeriodKeyGenerator', () => {
])
})
it('should generate period keys for last 7 days', () => {
expect(createGenerator().getDiscretePeriodKeys(Period.Last7Days)).toEqual([
'2022-5-17',
'2022-5-18',
'2022-5-19',
'2022-5-20',
'2022-5-21',
'2022-5-22',
'2022-5-23',
])
})
it('should generate period keys for Q1', () => {
expect(createGenerator().getDiscretePeriodKeys(Period.Q1ThisYear)).toEqual(['2022-1', '2022-2', '2022-3'])
})

View File

@@ -11,6 +11,12 @@ export class PeriodKeyGenerator implements PeriodKeyGeneratorInterface {
periodKeys.unshift(this.getDailyKey(this.getDateNDaysBefore(i)))
}
return periodKeys
case Period.Last7Days:
for (let i = 1; i <= 7; i++) {
periodKeys.unshift(this.getDailyKey(this.getDateNDaysBefore(i)))
}
return periodKeys
case Period.Q1ThisYear:
return this.generateMonthlyKeysRange(0, 3)

View File

@@ -1,5 +1,6 @@
export * from './Analytics/AnalyticsActivity'
export * from './Analytics/AnalyticsStoreInterface'
export * from './Statistics/StatisticsMeasure'
export * from './Statistics/StatisticsStoreInterface'
export * from './Time/Period'
export * from './Time/PeriodKeyGenerator'

View File

@@ -125,7 +125,7 @@ describe('RedisAnalyticsStore', () => {
expect(redisClient.bitop).toHaveBeenCalledWith(
'AND',
'bitmap:action:editing-items:timespan:period-key-period-key',
'bitmap:action:editing-items-editing-items:timespan:period-key',
'bitmap:action:editing-items:timespan:period-key',
'bitmap:action:editing-items:timespan:period-key',
)

View File

@@ -95,21 +95,19 @@ export class RedisAnalyticsStore implements AnalyticsStoreInterface {
return bitValue === 1
}
async calculateActivityRetention(
activity: AnalyticsActivity,
firstPeriod: Period,
secondPeriod: Period,
): Promise<number> {
const initialPeriodKey = this.periodKeyGenerator.getPeriodKey(firstPeriod)
const subsequentPeriodKey = this.periodKeyGenerator.getPeriodKey(secondPeriod)
const diffKey = `bitmap:action:${activity}:timespan:${initialPeriodKey}-${subsequentPeriodKey}`
async calculateActivitiesRetention(parameters: {
firstActivity: AnalyticsActivity
firstActivityPeriodKey: string
secondActivity: AnalyticsActivity
secondActivityPeriodKey: string
}): Promise<number> {
const diffKey = `bitmap:action:${parameters.firstActivity}-${parameters.secondActivity}:timespan:${parameters.secondActivityPeriodKey}`
await this.redisClient.bitop(
'AND',
diffKey,
`bitmap:action:${activity}:timespan:${initialPeriodKey}`,
`bitmap:action:${activity}:timespan:${subsequentPeriodKey}`,
`bitmap:action:${parameters.firstActivity}:timespan:${parameters.firstActivityPeriodKey}`,
`bitmap:action:${parameters.secondActivity}:timespan:${parameters.secondActivityPeriodKey}`,
)
await this.redisClient.expire(diffKey, 3600)
@@ -117,12 +115,25 @@ export class RedisAnalyticsStore implements AnalyticsStoreInterface {
const retainedTotalInActivity = await this.redisClient.bitcount(diffKey)
const initialTotalInActivity = await this.redisClient.bitcount(
`bitmap:action:${activity}:timespan:${initialPeriodKey}`,
`bitmap:action:${parameters.firstActivity}:timespan:${parameters.firstActivityPeriodKey}`,
)
return Math.ceil((retainedTotalInActivity * 100) / initialTotalInActivity)
}
async calculateActivityRetention(
activity: AnalyticsActivity,
firstPeriod: Period,
secondPeriod: Period,
): Promise<number> {
return this.calculateActivitiesRetention({
firstActivity: activity,
firstActivityPeriodKey: this.periodKeyGenerator.getPeriodKey(firstPeriod),
secondActivity: activity,
secondActivityPeriodKey: this.periodKeyGenerator.getPeriodKey(secondPeriod),
})
}
async calculateActivityTotalCount(activity: AnalyticsActivity, period: Period): Promise<number> {
return this.redisClient.bitcount(
`bitmap:action:${activity}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,

View File

@@ -1,5 +1,7 @@
import * as IORedis from 'ioredis'
import { PeriodKeyGeneratorInterface } from '../../Domain'
import { Period, PeriodKeyGeneratorInterface } from '../../Domain'
import { StatisticsMeasure } from '../../Domain/Statistics/StatisticsMeasure'
import { RedisStatisticsStore } from './RedisStatisticsStore'
@@ -13,6 +15,7 @@ describe('RedisStatisticsStore', () => {
beforeEach(() => {
pipeline = {} as jest.Mocked<IORedis.Pipeline>
pipeline.incr = jest.fn()
pipeline.incrbyfloat = jest.fn()
pipeline.setbit = jest.fn()
pipeline.exec = jest.fn()
@@ -88,4 +91,30 @@ describe('RedisStatisticsStore', () => {
expect(pipeline.incr).toHaveBeenCalled()
expect(pipeline.exec).toHaveBeenCalled()
})
it('should increment measure by a value', async () => {
await createStore().incrementMeasure(StatisticsMeasure.Income, 2, [Period.Today, Period.ThisMonth])
expect(pipeline.incr).toHaveBeenCalledTimes(2)
expect(pipeline.incrbyfloat).toHaveBeenCalledTimes(2)
expect(pipeline.exec).toHaveBeenCalled()
})
it('should count a measurement average', async () => {
redisClient.get = jest.fn().mockReturnValueOnce('5').mockReturnValueOnce('2')
expect(await createStore().getMeasureAverage(StatisticsMeasure.Income, Period.Today)).toEqual(2 / 5)
})
it('should count a measurement average - 0 increments', async () => {
redisClient.get = jest.fn().mockReturnValueOnce(null).mockReturnValueOnce(null)
expect(await createStore().getMeasureAverage(StatisticsMeasure.Income, Period.Today)).toEqual(0)
})
it('should count a measurement average - 0 total value', async () => {
redisClient.get = jest.fn().mockReturnValueOnce(5).mockReturnValueOnce(null)
expect(await createStore().getMeasureAverage(StatisticsMeasure.Income, Period.Today)).toEqual(0)
})
})

View File

@@ -1,12 +1,49 @@
import * as IORedis from 'ioredis'
import { Period, PeriodKeyGeneratorInterface } from '../../Domain'
import { StatisticsMeasure } from '../../Domain/Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../../Domain/Statistics/StatisticsStoreInterface'
export class RedisStatisticsStore implements StatisticsStoreInterface {
constructor(private periodKeyGenerator: PeriodKeyGeneratorInterface, private redisClient: IORedis.Redis) {}
async getMeasureTotal(measure: StatisticsMeasure, period: Period): Promise<number> {
const totalValue = await this.redisClient.get(
`count:measure:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,
)
if (totalValue === null) {
return 0
}
return +totalValue
}
async incrementMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void> {
const pipeline = this.redisClient.pipeline()
for (const period of periods) {
pipeline.incrbyfloat(`count:measure:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`, value)
pipeline.incr(`count:increments:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`)
}
await pipeline.exec()
}
async getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise<number> {
const increments = await this.redisClient.get(
`count:increments:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,
)
if (increments === null) {
return 0
}
const totalValue = await this.getMeasureTotal(measure, period)
return totalValue / +increments
}
async getYesterdayOutOfSyncIncidents(): Promise<number> {
const count = await this.redisClient.get(
`count:action:out-of-sync:timespan:${this.periodKeyGenerator.getPeriodKey(Period.Yesterday)}`,

View File

@@ -3,6 +3,183 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.19.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.5...@standardnotes/api-gateway@1.19.6) (2022-09-19)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.19.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.4...@standardnotes/api-gateway@1.19.5) (2022-09-16)
### Bug Fixes
* **auth:** change remaining subscription time stats to percentage ([5eb957c](https://github.com/standardnotes/api-gateway/commit/5eb957c82a8cc5fdcb6815e2cd30e49cd2b1e8ac))
## [1.19.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.3...@standardnotes/api-gateway@1.19.4) (2022-09-16)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.19.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.2...@standardnotes/api-gateway@1.19.3) (2022-09-15)
### Bug Fixes
* **api-gateway:** add remaining subscription time to stats ([89334c9](https://github.com/standardnotes/api-gateway/commit/89334c90221045308d83fce9e97c146185d21389))
## [1.19.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.1...@standardnotes/api-gateway@1.19.2) (2022-09-15)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.19.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.0...@standardnotes/api-gateway@1.19.1) (2022-09-09)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.19.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.18.0...@standardnotes/api-gateway@1.19.0) (2022-09-09)
### Features
* **syncing-server:** add tracking files count in stats ([52cc646](https://github.com/standardnotes/api-gateway/commit/52cc6462a66dae3bd6c05f551d4ba661c8a9b8c8))
# [1.18.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.17.4...@standardnotes/api-gateway@1.18.0) (2022-09-09)
### Bug Fixes
* **api-gateway:** add general activity breakdown to yesterdays report stats ([339c86f](https://github.com/standardnotes/api-gateway/commit/339c86fca073b02054260417b7519c08874e1e4e))
### Features
* **api-gateway:** add tracking general activity for free and paid users breakdown ([0afd3de](https://github.com/standardnotes/api-gateway/commit/0afd3de9779e2abe10deede24626a3cbe6b15e6c))
## [1.17.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.17.3...@standardnotes/api-gateway@1.17.4) (2022-09-09)
### Bug Fixes
* **api-gateway:** add notes count statistics to report ([ced852d](https://github.com/standardnotes/api-gateway/commit/ced852d9dbf8cab4c235b94a834968a5fc5e7d36))
## [1.17.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.17.2...@standardnotes/api-gateway@1.17.3) (2022-09-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.17.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.17.1...@standardnotes/api-gateway@1.17.2) (2022-09-08)
### Bug Fixes
* **api-gateway:** retention data structure to include both period keys ([50ddb91](https://github.com/standardnotes/api-gateway/commit/50ddb918ccc52bee4caad82504cb899bc5936150))
## [1.17.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.17.0...@standardnotes/api-gateway@1.17.1) (2022-09-08)
### Bug Fixes
* **api-gateway:** retention data structure ([47be084](https://github.com/standardnotes/api-gateway/commit/47be0841fc6d5fa00892e775bb3a40f404a6382b))
# [1.17.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.8...@standardnotes/api-gateway@1.17.0) (2022-09-08)
### Features
* **api-gateway:** add registration-to-activity retention analytics to report ([f139bb0](https://github.com/standardnotes/api-gateway/commit/f139bb003669bb41f98ad4bb59a036c489f43606))
## [1.16.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.7...@standardnotes/api-gateway@1.16.8) (2022-09-08)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.16.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.6...@standardnotes/api-gateway@1.16.7) (2022-09-08)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.16.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.5...@standardnotes/api-gateway@1.16.6) (2022-09-07)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.16.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.4...@standardnotes/api-gateway@1.16.5) (2022-09-07)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.16.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.3...@standardnotes/api-gateway@1.16.4) (2022-09-07)
### Bug Fixes
* **api-gateway:** add registration-to-subscription time to analytics report ([936591d](https://github.com/standardnotes/api-gateway/commit/936591d40b5f5beb5c0a824c92cdfa20fff51c97))
## [1.16.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.2...@standardnotes/api-gateway@1.16.3) (2022-09-07)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.16.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.1...@standardnotes/api-gateway@1.16.2) (2022-09-06)
### Bug Fixes
* **api-gateway:** include period key in statistics measures ([d149f46](https://github.com/standardnotes/api-gateway/commit/d149f46cf6456201dd8690977f64ed32a75f3459))
* **api-gateway:** period types on analytics report ([f94c8fc](https://github.com/standardnotes/api-gateway/commit/f94c8fc26e684a07101cc5282ebb9cda3c8c6961))
## [1.16.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.16.0...@standardnotes/api-gateway@1.16.1) (2022-09-06)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.16.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.12...@standardnotes/api-gateway@1.16.0) (2022-09-06)
### Features
* **api-gateway:** add statistics measures to report generation ([8151bb1](https://github.com/standardnotes/api-gateway/commit/8151bb108affb2b5cfa1ab365f99a9f0170a7795))
## [1.15.12](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.11...@standardnotes/api-gateway@1.15.12) (2022-09-06)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.15.11](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.10...@standardnotes/api-gateway@1.15.11) (2022-09-06)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.15.10](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.9...@standardnotes/api-gateway@1.15.10) (2022-09-05)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.15.9](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.8...@standardnotes/api-gateway@1.15.9) (2022-09-05)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.15.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.7...@standardnotes/api-gateway@1.15.8) (2022-09-05)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.15.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.6...@standardnotes/api-gateway@1.15.7) (2022-09-05)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.15.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.5...@standardnotes/api-gateway@1.15.6) (2022-09-01)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.15.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.4...@standardnotes/api-gateway@1.15.5) (2022-09-01)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.15.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.3...@standardnotes/api-gateway@1.15.4) (2022-08-30)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.15.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.2...@standardnotes/api-gateway@1.15.3) (2022-08-22)
### Bug Fixes
* **api-gateway:** add error logs on missing connection id for websockets ([f7def38](https://github.com/standardnotes/api-gateway/commit/f7def38e20f87ae24ebc736a41bc7cac53b0c61f))
## [1.15.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.1...@standardnotes/api-gateway@1.15.2) (2022-08-15)
### Bug Fixes
* **api-gateway:** add payment success events to report ([ee79347](https://github.com/standardnotes/api-gateway/commit/ee79347e27f5887def2cda57091a7c0a40570d33))
## [1.15.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.15.0...@standardnotes/api-gateway@1.15.1) (2022-08-15)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.15.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.14.3...@standardnotes/api-gateway@1.15.0) (2022-08-15)
### Features
* **api-gateway:** add gathering analytics for failed payments ([d0023a6](https://github.com/standardnotes/api-gateway/commit/d0023a6c92756c81b8daa9089d38141b6cd4fe48))
## [1.14.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.14.2...@standardnotes/api-gateway@1.14.3) (2022-08-15)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.14.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.14.1...@standardnotes/api-gateway@1.14.2) (2022-08-15)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -12,12 +12,20 @@ import {
DailyAnalyticsReportGeneratedEvent,
DomainEventService,
} from '@standardnotes/domain-events'
import { AnalyticsActivity, AnalyticsStoreInterface, Period, StatisticsStoreInterface } from '@standardnotes/analytics'
import {
AnalyticsActivity,
AnalyticsStoreInterface,
Period,
PeriodKeyGeneratorInterface,
StatisticsMeasure,
StatisticsStoreInterface,
} from '@standardnotes/analytics'
const requestReport = async (
analyticsStore: AnalyticsStoreInterface,
statisticsStore: StatisticsStoreInterface,
domainEventPublisher: DomainEventPublisherInterface,
periodKeyGenerator: PeriodKeyGeneratorInterface,
): Promise<void> => {
const analyticsOverTime = []
@@ -60,9 +68,12 @@ const requestReport = async (
const yesterdayActivityStatistics = []
const yesterdayActivityNames = [
AnalyticsActivity.EditingItems,
AnalyticsActivity.LimitedDiscountOfferPurchased,
AnalyticsActivity.GeneralActivity,
AnalyticsActivity.GeneralActivityFreeUsers,
AnalyticsActivity.GeneralActivityPaidUsers,
AnalyticsActivity.PaymentFailed,
AnalyticsActivity.PaymentSuccess,
]
for (const activityName of yesterdayActivityNames) {
@@ -77,6 +88,48 @@ const requestReport = async (
})
}
const statisticMeasureNames = [
StatisticsMeasure.Income,
StatisticsMeasure.Refunds,
StatisticsMeasure.RegistrationLength,
StatisticsMeasure.SubscriptionLength,
StatisticsMeasure.RegistrationToSubscriptionTime,
StatisticsMeasure.RemainingSubscriptionTimePercentage,
StatisticsMeasure.NotesCountFreeUsers,
StatisticsMeasure.NotesCountPaidUsers,
StatisticsMeasure.FilesCount,
]
const statisticMeasures = []
for (const statisticMeasureName of statisticMeasureNames) {
for (const period of [Period.Yesterday, Period.ThisMonth]) {
statisticMeasures.push({
name: statisticMeasureName,
period,
totalValue: await statisticsStore.getMeasureTotal(statisticMeasureName, period),
average: await statisticsStore.getMeasureAverage(statisticMeasureName, period),
})
}
}
const periodKeys = periodKeyGenerator.getDiscretePeriodKeys(Period.Last7Days)
const retentionOverDays = []
for (let i = 0; i < periodKeys.length; i++) {
for (let j = 0; j < periodKeys.length - i; j++) {
const dailyRetention = await analyticsStore.calculateActivitiesRetention({
firstActivity: AnalyticsActivity.Register,
firstActivityPeriodKey: periodKeys[i],
secondActivity: AnalyticsActivity.GeneralActivity,
secondActivityPeriodKey: periodKeys[i + j],
})
retentionOverDays.push({
firstPeriodKey: periodKeys[i],
secondPeriodKey: periodKeys[i + j],
value: dailyRetention,
})
}
}
const event: DailyAnalyticsReportGeneratedEvent = {
type: 'DAILY_ANALYTICS_REPORT_GENERATED',
createdAt: new Date(),
@@ -93,6 +146,17 @@ const requestReport = async (
outOfSyncIncidents: await statisticsStore.getYesterdayOutOfSyncIncidents(),
activityStatistics: yesterdayActivityStatistics,
activityStatisticsOverTime: analyticsOverTime,
statisticMeasures,
retentionStatistics: [
{
firstActivity: AnalyticsActivity.Register,
secondActivity: AnalyticsActivity.GeneralActivity,
retention: {
periodKeys,
values: retentionOverDays,
},
},
],
},
}
@@ -111,8 +175,9 @@ void container.load().then((container) => {
const analyticsStore: AnalyticsStoreInterface = container.get(TYPES.AnalyticsStore)
const statisticsStore: StatisticsStoreInterface = container.get(TYPES.StatisticsStore)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator)
Promise.resolve(requestReport(analyticsStore, statisticsStore, domainEventPublisher))
Promise.resolve(requestReport(analyticsStore, statisticsStore, domainEventPublisher, periodKeyGenerator))
.then(() => {
logger.info('Usage report generation complete')

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.14.2",
"version": "1.19.6",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -25,6 +25,7 @@
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.3.0",
"@standardnotes/analytics": "workspace:*",
"@standardnotes/common": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",
"@standardnotes/security": "workspace:*",

View File

@@ -6,6 +6,7 @@ import * as AWS from 'aws-sdk'
import {
AnalyticsStoreInterface,
PeriodKeyGenerator,
PeriodKeyGeneratorInterface,
RedisAnalyticsStore,
RedisStatisticsStore,
StatisticsStoreInterface,
@@ -91,13 +92,13 @@ export class ContainerConfigLoader {
// Services
container.bind<HttpServiceInterface>(TYPES.HTTPService).to(HttpService)
const periodKeyGenerator = new PeriodKeyGenerator()
container.bind<PeriodKeyGeneratorInterface>(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator())
container
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
.toConstantValue(new RedisAnalyticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
.toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
container
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
.toConstantValue(new RedisStatisticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
container.bind<CrossServiceTokenCacheInterface>(TYPES.CrossServiceTokenCache).to(RedisCrossServiceTokenCache)
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())

View File

@@ -26,6 +26,7 @@ const TYPES = {
StatisticsStore: Symbol.for('StatisticsStore'),
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
Timer: Symbol.for('Timer'),
PeriodKeyGenerator: Symbol.for('PeriodKeyGenerator'),
}
export default TYPES

View File

@@ -1,4 +1,5 @@
import { CrossServiceTokenData } from '@standardnotes/security'
import { RoleName } from '@standardnotes/common'
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
import { TimerInterface } from '@standardnotes/time'
import { NextFunction, Request, Response } from 'express'
@@ -75,9 +76,20 @@ export class AuthMiddleware extends BaseMiddleware {
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
await this.analyticsStore.markActivity([AnalyticsActivity.GeneralActivity], decodedToken.analyticsId as number, [
Period.Today,
])
response.locals.freeUser =
decodedToken.roles.length === 1 &&
decodedToken.roles.find((role) => role.name === RoleName.CoreUser) !== undefined
await this.analyticsStore.markActivity(
[
AnalyticsActivity.GeneralActivity,
response.locals.freeUser
? AnalyticsActivity.GeneralActivityFreeUsers
: AnalyticsActivity.GeneralActivityPaidUsers,
],
decodedToken.analyticsId as number,
[Period.Today],
)
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({

View File

@@ -1,18 +1,25 @@
import { Request, Response } from 'express'
import { inject } from 'inversify'
import { BaseHttpController, controller, httpDelete, httpPost } from 'inversify-express-utils'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
@controller('/v1/sockets')
export class WebSocketsController extends BaseHttpController {
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
constructor(
@inject(TYPES.HTTPService) private httpService: HttpServiceInterface,
@inject(TYPES.Logger) private logger: Logger,
) {
super()
}
@httpPost('/', TYPES.AuthMiddleware)
async createWebSocketConnection(request: Request, response: Response): Promise<void> {
if (!request.headers.connectionid) {
this.logger.error('Could not create a websocket connection. Missing connection id header.')
response.status(400).send('Missing connection id in the request')
return
@@ -24,6 +31,8 @@ export class WebSocketsController extends BaseHttpController {
@httpDelete('/')
async deleteWebSocketConnection(request: Request, response: Response): Promise<void> {
if (!request.headers.connectionid) {
this.logger.error('Could not delete a websocket connection. Missing connection id header.')
response.status(400).send('Missing connection id in the request')
return

View File

@@ -3,6 +3,222 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.29.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.4...@standardnotes/auth-server@1.29.0) (2022-09-19)
### Features
* **files:** add validating remote identifiers ([db15457](https://github.com/standardnotes/server/commit/db15457ce4eb533ec822cf93c3ed83eafe9e64d5))
## [1.28.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.3...@standardnotes/auth-server@1.28.4) (2022-09-16)
### Bug Fixes
* **auth:** feature service spec ([c207c3f](https://github.com/standardnotes/server/commit/c207c3fc8442eec9b8c3150f09ecccfdd6a5ed50))
## [1.28.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.2...@standardnotes/auth-server@1.28.3) (2022-09-16)
### Bug Fixes
* **auth:** change remaining subscription time stats to percentage ([5eb957c](https://github.com/standardnotes/server/commit/5eb957c82a8cc5fdcb6815e2cd30e49cd2b1e8ac))
## [1.28.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.1...@standardnotes/auth-server@1.28.2) (2022-09-16)
### Bug Fixes
* **files:** add verifying permitted operation on valet token ([377d32c](https://github.com/standardnotes/server/commit/377d32c4498305f0f59ff59e7357f0d2f10ce3a2))
## [1.28.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.0...@standardnotes/auth-server@1.28.1) (2022-09-15)
### Bug Fixes
* **auth:** missing injectable annotation ([d851524](https://github.com/standardnotes/server/commit/d85152429ca379d3d0314a9864cc46ebee541958))
# [1.28.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.27.0...@standardnotes/auth-server@1.28.0) (2022-09-15)
### Features
* **auth:** add remaining subscription time stats ([a59ba08](https://github.com/standardnotes/server/commit/a59ba083397c75960af0e8a102b617bf5baa287f))
# [1.27.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.26.1...@standardnotes/auth-server@1.27.0) (2022-09-15)
### Features
* **auth:** implement subscription server interface on server side ([5d812be](https://github.com/standardnotes/server/commit/5d812befc4733954919eef0d3718ae6f8eb81654))
## [1.26.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.26.0...@standardnotes/auth-server@1.26.1) (2022-09-15)
### Bug Fixes
* **auth:** disallow duplicating subscription invites ([531f13f](https://github.com/standardnotes/server/commit/531f13fe1f4bdfb8d27f5e3c07ec0b15d36ad413))
# [1.26.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.13...@standardnotes/auth-server@1.26.0) (2022-09-13)
### Features
* **auth:** add subscription sharing permission ([f45320e](https://github.com/standardnotes/server/commit/f45320e5ed8948a432029586c05284f4d640de5b))
## [1.25.13](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.12...@standardnotes/auth-server@1.25.13) (2022-09-12)
### Bug Fixes
* **auth:** add debug logs for canceling shared subscription invitations ([dd13e2e](https://github.com/standardnotes/server/commit/dd13e2eaf74de56a3c8c30c236c32c6dc0c560f2))
## [1.25.12](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.11...@standardnotes/auth-server@1.25.12) (2022-09-12)
### Bug Fixes
* **auth:** allow canceling shared subscription invitation before it was accepted ([0dab31f](https://github.com/standardnotes/server/commit/0dab31f9936bfd5081a87eef9701a268b8dec88c))
## [1.25.11](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.10...@standardnotes/auth-server@1.25.11) (2022-09-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.10](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.9...@standardnotes/auth-server@1.25.10) (2022-09-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.8...@standardnotes/auth-server@1.25.9) (2022-09-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.7...@standardnotes/auth-server@1.25.8) (2022-09-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.6...@standardnotes/auth-server@1.25.7) (2022-09-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.5...@standardnotes/auth-server@1.25.6) (2022-09-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.4...@standardnotes/auth-server@1.25.5) (2022-09-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.3...@standardnotes/auth-server@1.25.4) (2022-09-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.2...@standardnotes/auth-server@1.25.3) (2022-09-08)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.1...@standardnotes/auth-server@1.25.2) (2022-09-07)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.25.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.25.0...@standardnotes/auth-server@1.25.1) (2022-09-07)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.25.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.24.4...@standardnotes/auth-server@1.25.0) (2022-09-07)
### Features
* **auth:** add measuring registration to subscription time statistics ([b618252](https://github.com/standardnotes/server/commit/b61825235eebaf5eddb55cbda173176ca43c0099))
## [1.24.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.24.3...@standardnotes/auth-server@1.24.4) (2022-09-07)
### Bug Fixes
* **auth:** forbid users on shared subscription to send out invitations ([132b617](https://github.com/standardnotes/server/commit/132b617aaa8a703877fd7e8d23711fb1ec234524))
## [1.24.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.24.2...@standardnotes/auth-server@1.24.3) (2022-09-06)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.24.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.24.1...@standardnotes/auth-server@1.24.2) (2022-09-06)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.24.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.24.0...@standardnotes/auth-server@1.24.1) (2022-09-06)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.24.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.23.2...@standardnotes/auth-server@1.24.0) (2022-09-06)
### Features
* **auth:** add statistics for refunds and account deletions ([d7ae2f0](https://github.com/standardnotes/server/commit/d7ae2f06255b19eb5d3403a4989610390064754e))
## [1.23.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.23.1...@standardnotes/auth-server@1.23.2) (2022-09-06)
### Bug Fixes
* **auth:** add debug logs for subscription canceling ([2ca430f](https://github.com/standardnotes/server/commit/2ca430f40ce6a8d56aafa27e9c2d0b0dd561c650))
## [1.23.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.23.0...@standardnotes/auth-server@1.23.1) (2022-09-06)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.23.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.22.1...@standardnotes/auth-server@1.23.0) (2022-09-05)
### Features
* **auth:** add measuring subscription length ([fa10827](https://github.com/standardnotes/server/commit/fa108274430d8dff1016ddcba5bbcb2778eb781b))
## [1.22.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.22.0...@standardnotes/auth-server@1.22.1) (2022-09-05)
### Bug Fixes
* **auth:** StatisticsStore binding ([34315c9](https://github.com/standardnotes/server/commit/34315c91d7428bbe8297e50972aa7823e2a983b2))
# [1.22.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.21.5...@standardnotes/auth-server@1.22.0) (2022-09-05)
### Features
* **auth:** add keeping stats on payments ([0c176b7](https://github.com/standardnotes/server/commit/0c176b70f8281e1e490224b9c7ab85f272a3d4e9))
## [1.21.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.21.4...@standardnotes/auth-server@1.21.5) (2022-09-05)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.21.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.21.3...@standardnotes/auth-server@1.21.4) (2022-09-05)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.21.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.21.2...@standardnotes/auth-server@1.21.3) (2022-09-01)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.21.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.21.1...@standardnotes/auth-server@1.21.2) (2022-09-01)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.21.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.21.0...@standardnotes/auth-server@1.21.1) (2022-08-30)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.21.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.20.1...@standardnotes/auth-server@1.21.0) (2022-08-29)
### Features
* **auth:** retain user agent, api version and received at on revoked sessions ([dc88e24](https://github.com/standardnotes/server/commit/dc88e2413b3be254b265d2874174eaac39d628ee))
## [1.20.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.20.0...@standardnotes/auth-server@1.20.1) (2022-08-17)
### Bug Fixes
* **scheduler:** change discount from 10% to 20% on a limited time offer ([4b3de26](https://github.com/standardnotes/server/commit/4b3de264efc4fffb2603181c158cddb25c4ed4a9))
# [1.20.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.19.1...@standardnotes/auth-server@1.20.0) (2022-08-15)
### Features
* **auth:** add payment success event handler ([01a08ea](https://github.com/standardnotes/server/commit/01a08eae582e070ec844f5e05f34260447b7d4c6))
## [1.19.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.19.0...@standardnotes/auth-server@1.19.1) (2022-08-15)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.19.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.18.4...@standardnotes/auth-server@1.19.0) (2022-08-15)
### Features
* **auth:** add payment failed event handler ([58c5b58](https://github.com/standardnotes/server/commit/58c5b586a904cf1fd179cc28783a6ae7da688063))
## [1.18.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.18.3...@standardnotes/auth-server@1.18.4) (2022-08-15)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -17,10 +17,10 @@ import '../src/Controller/SubscriptionTokensController'
import '../src/Controller/OfflineController'
import '../src/Controller/ValetTokenController'
import '../src/Controller/ListedController'
import '../src/Controller/SubscriptionInvitesController'
import '../src/Controller/SubscriptionSettingsController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController'
import * as cors from 'cors'
import { urlencoded, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'

View File

@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class revokedSessionData1661771230400 implements MigrationInterface {
name = 'revokedSessionData1661771230400'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `revoked_sessions` ADD `received_at` datetime NULL')
await queryRunner.query('ALTER TABLE `revoked_sessions` ADD `user_agent` text NULL')
await queryRunner.query('ALTER TABLE `revoked_sessions` ADD `api_version` varchar(255) NULL')
}
public async down(): Promise<void> {
return
}
}

View File

@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addSubscriptionSharingPermission1663073954000 implements MigrationInterface {
name = 'addSubscriptionSharingPermission1663073954000'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'INSERT INTO `permissions` (uuid, name) VALUES ("3aeaf12e-380f-4f21-97b9-d862d63874f6", "server:subscription-sharing")',
)
// Pro User Permissions
await queryRunner.query(
'INSERT INTO `role_permissions` (role_uuid, permission_uuid) VALUES \
("8047edbb-a10a-4ff8-8d53-c2cae600a8e8", "3aeaf12e-380f-4f21-97b9-d862d63874f6") \
',
)
}
public async down(): Promise<void> {
return
}
}

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addRenewedAtColumn1663321030000 implements MigrationInterface {
name = 'addRenewedAtColumn1663321030000'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `user_subscriptions` ADD `renewed_at` bigint NULL')
}
public async down(): Promise<void> {
return
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.18.4",
"version": "1.29.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -34,7 +34,7 @@
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.3.0",
"@standardnotes/analytics": "workspace:*",
"@standardnotes/api": "^1.1.19",
"@standardnotes/api": "^1.7.2",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",
@@ -63,7 +63,7 @@
"reflect-metadata": "0.1.13",
"typeorm": "^0.3.6",
"ua-parser-js": "1.0.2",
"uuid": "8.3.2",
"uuid": "^9.0.0",
"winston": "^3.8.1"
},
"devDependencies": {

View File

@@ -9,7 +9,13 @@ import {
} from '@standardnotes/domain-events'
import { TimerInterface, Timer } from '@standardnotes/time'
import { UAParser } from 'ua-parser-js'
import { AnalyticsStoreInterface, PeriodKeyGenerator, RedisAnalyticsStore } from '@standardnotes/analytics'
import {
AnalyticsStoreInterface,
PeriodKeyGenerator,
RedisAnalyticsStore,
RedisStatisticsStore,
StatisticsStoreInterface,
} from '@standardnotes/analytics'
import { Env } from './Env'
import TYPES from './Types'
@@ -124,7 +130,14 @@ import { RedisOfflineSubscriptionTokenRepository } from '../Infra/Redis/RedisOff
import { CreateOfflineSubscriptionToken } from '../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
import { AuthenticateOfflineSubscriptionToken } from '../Domain/UseCase/AuthenticateOfflineSubscriptionToken/AuthenticateOfflineSubscriptionToken'
import { SubscriptionCancelledEventHandler } from '../Domain/Handler/SubscriptionCancelledEventHandler'
import { ContentDecoder, ContentDecoderInterface, ProtocolVersion } from '@standardnotes/common'
import {
ContentDecoder,
ContentDecoderInterface,
ProtocolVersion,
Uuid,
UuidValidator,
ValidatorInterface,
} from '@standardnotes/common'
import { GetUserOfflineSubscription } from '../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription'
import { ApiGatewayOfflineAuthMiddleware } from '../Controller/ApiGatewayOfflineAuthMiddleware'
import { UserEmailChangedEventHandler } from '../Domain/Handler/UserEmailChangedEventHandler'
@@ -191,6 +204,10 @@ import { AuthController } from '../Controller/AuthController'
import { VerifyPredicate } from '../Domain/UseCase/VerifyPredicate/VerifyPredicate'
import { PredicateVerificationRequestedEventHandler } from '../Domain/Handler/PredicateVerificationRequestedEventHandler'
import { MuteMarketingEmails } from '../Domain/UseCase/MuteMarketingEmails/MuteMarketingEmails'
import { PaymentFailedEventHandler } from '../Domain/Handler/PaymentFailedEventHandler'
import { PaymentSuccessEventHandler } from '../Domain/Handler/PaymentSuccessEventHandler'
import { RefundProcessedEventHandler } from '../Domain/Handler/RefundProcessedEventHandler'
import { SubscriptionInvitesController } from '../Controller/SubscriptionInvitesController'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -253,6 +270,7 @@ export class ContainerConfigLoader {
// Controller
container.bind<AuthController>(TYPES.AuthController).to(AuthController)
container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
// Repositories
container.bind<SessionRepositoryInterface>(TYPES.SessionRepository).to(MySQLSessionRepository)
@@ -478,6 +496,9 @@ export class ContainerConfigLoader {
container
.bind<PredicateVerificationRequestedEventHandler>(TYPES.PredicateVerificationRequestedEventHandler)
.to(PredicateVerificationRequestedEventHandler)
container.bind<PaymentFailedEventHandler>(TYPES.PaymentFailedEventHandler).to(PaymentFailedEventHandler)
container.bind<PaymentSuccessEventHandler>(TYPES.PaymentSuccessEventHandler).to(PaymentSuccessEventHandler)
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
// Services
container.bind<UAParser>(TYPES.DeviceDetector).toConstantValue(new UAParser())
@@ -538,9 +559,14 @@ export class ContainerConfigLoader {
.bind<SelectorInterface<boolean>>(TYPES.BooleanSelector)
.toConstantValue(new DeterministicSelector<boolean>())
container.bind<UserSubscriptionServiceInterface>(TYPES.UserSubscriptionService).to(UserSubscriptionService)
const periodKeyGenerator = new PeriodKeyGenerator()
container
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
.toConstantValue(new RedisAnalyticsStore(new PeriodKeyGenerator(), container.get(TYPES.Redis)))
.toConstantValue(new RedisAnalyticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
container
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
.toConstantValue(new RedisStatisticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
container.bind<ValidatorInterface<Uuid>>(TYPES.UuidValidator).to(UuidValidator)
if (env.get('SNS_TOPIC_ARN', true)) {
container
@@ -576,6 +602,9 @@ export class ContainerConfigLoader {
],
['SHARED_SUBSCRIPTION_INVITATION_CREATED', container.get(TYPES.SharedSubscriptionInvitationCreatedEventHandler)],
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.PredicateVerificationRequestedEventHandler)],
['PAYMENT_FAILED', container.get(TYPES.PaymentFailedEventHandler)],
['PAYMENT_SUCCESS', container.get(TYPES.PaymentSuccessEventHandler)],
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
])
if (env.get('SQS_QUEUE_URL', true)) {

View File

@@ -5,6 +5,7 @@ const TYPES = {
SQS: Symbol.for('SQS'),
// Controller
AuthController: Symbol.for('AuthController'),
SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'),
// Repositories
UserRepository: Symbol.for('UserRepository'),
SessionRepository: Symbol.for('SessionRepository'),
@@ -143,6 +144,9 @@ const TYPES = {
UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for('UserDisabledSessionUserAgentLoggingEventHandler'),
SharedSubscriptionInvitationCreatedEventHandler: Symbol.for('SharedSubscriptionInvitationCreatedEventHandler'),
PredicateVerificationRequestedEventHandler: Symbol.for('PredicateVerificationRequestedEventHandler'),
PaymentFailedEventHandler: Symbol.for('PaymentFailedEventHandler'),
PaymentSuccessEventHandler: Symbol.for('PaymentSuccessEventHandler'),
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
// Services
DeviceDetector: Symbol.for('DeviceDetector'),
SessionService: Symbol.for('SessionService'),
@@ -184,6 +188,8 @@ const TYPES = {
BooleanSelector: Symbol.for('BooleanSelector'),
UserSubscriptionService: Symbol.for('UserSubscriptionService'),
AnalyticsStore: Symbol.for('AnalyticsStore'),
StatisticsStore: Symbol.for('StatisticsStore'),
UuidValidator: Symbol.for('UuidValidator'),
}
export default TYPES

View File

@@ -1,16 +1,13 @@
import 'reflect-metadata'
import * as express from 'express'
import { SubscriptionInvitesController } from './SubscriptionInvitesController'
import { results } from 'inversify-express-utils'
import { User } from '../Domain/User/User'
import { InviteToSharedSubscription } from '../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
import { AcceptSharedSubscriptionInvitation } from '../Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation'
import { DeclineSharedSubscriptionInvitation } from '../Domain/UseCase/DeclineSharedSubscriptionInvitation/DeclineSharedSubscriptionInvitation'
import { CancelSharedSubscriptionInvitation } from '../Domain/UseCase/CancelSharedSubscriptionInvitation/CancelSharedSubscriptionInvitation'
import { RoleName } from '@standardnotes/common'
import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
import { ApiVersion } from '@standardnotes/api'
describe('SubscriptionInvitesController', () => {
let inviteToSharedSubscription: InviteToSharedSubscription
@@ -19,8 +16,6 @@ describe('SubscriptionInvitesController', () => {
let cancelSharedSubscriptionInvitation: CancelSharedSubscriptionInvitation
let listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations
let request: express.Request
let response: express.Response
let user: User
const createController = () =>
@@ -51,25 +46,6 @@ describe('SubscriptionInvitesController', () => {
listSharedSubscriptionInvitations = {} as jest.Mocked<ListSharedSubscriptionInvitations>
listSharedSubscriptionInvitations.execute = jest.fn()
request = {
headers: {},
body: {},
params: {},
} as jest.Mocked<express.Request>
response = {
locals: {},
} as jest.Mocked<express.Response>
response.locals.user = {
email: 'test@test.te',
}
response.locals.roles = [
{
uuid: '1-2-3',
name: RoleName.CoreUser,
},
]
})
it('should get invitations to subscription sharing', async () => {
@@ -77,128 +53,127 @@ describe('SubscriptionInvitesController', () => {
invitations: [],
})
const httpResponse = <results.JsonResult>await createController().listInvites(request, response)
const result = await httpResponse.executeAsync()
const result = await createController().listInvites({ api: ApiVersion.v0, inviterEmail: 'test@test.te' })
expect(listSharedSubscriptionInvitations.execute).toHaveBeenCalledWith({
inviterEmail: 'test@test.te',
})
expect(result.statusCode).toEqual(200)
expect(result.status).toEqual(200)
})
it('should cancel invitation to subscription sharing', async () => {
request.params.inviteUuid = '1-2-3'
cancelSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
success: true,
})
const httpResponse = <results.JsonResult>await createController().cancelSubscriptionSharing(request, response)
const result = await httpResponse.executeAsync()
const result = await createController().cancelInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
inviterEmail: 'test@test.te',
})
expect(cancelSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
sharedSubscriptionInvitationUuid: '1-2-3',
inviterEmail: 'test@test.te',
})
expect(result.statusCode).toEqual(200)
expect(result.status).toEqual(200)
})
it('should not cancel invitation to subscription sharing if the workflow fails', async () => {
request.params.inviteUuid = '1-2-3'
cancelSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
success: false,
})
const httpResponse = <results.JsonResult>await createController().cancelSubscriptionSharing(request, response)
const result = await httpResponse.executeAsync()
const result = await createController().cancelInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(result.statusCode).toEqual(400)
expect(result.status).toEqual(400)
})
it('should decline invitation to subscription sharing', async () => {
request.params.inviteUuid = '1-2-3'
declineSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
success: true,
})
const httpResponse = <results.JsonResult>await createController().declineInvite(request)
const result = await httpResponse.executeAsync()
const result = await createController().declineInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(declineSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
sharedSubscriptionInvitationUuid: '1-2-3',
})
expect(result.statusCode).toEqual(200)
expect(result.status).toEqual(200)
})
it('should not decline invitation to subscription sharing if the workflow fails', async () => {
request.params.inviteUuid = '1-2-3'
declineSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
success: false,
})
const httpResponse = <results.JsonResult>await createController().declineInvite(request)
const result = await httpResponse.executeAsync()
const result = await createController().declineInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(declineSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
sharedSubscriptionInvitationUuid: '1-2-3',
})
expect(result.statusCode).toEqual(400)
expect(result.status).toEqual(400)
})
it('should accept invitation to subscription sharing', async () => {
request.params.inviteUuid = '1-2-3'
acceptSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
success: true,
})
const httpResponse = <results.JsonResult>await createController().acceptInvite(request)
const result = await httpResponse.executeAsync()
const result = await createController().acceptInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(acceptSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
sharedSubscriptionInvitationUuid: '1-2-3',
})
expect(result.statusCode).toEqual(200)
expect(result.status).toEqual(200)
})
it('should not accept invitation to subscription sharing if the workflow fails', async () => {
request.params.inviteUuid = '1-2-3'
acceptSharedSubscriptionInvitation.execute = jest.fn().mockReturnValue({
success: false,
})
const httpResponse = <results.JsonResult>await createController().acceptInvite(request)
const result = await httpResponse.executeAsync()
const result = await createController().acceptInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(acceptSharedSubscriptionInvitation.execute).toHaveBeenCalledWith({
sharedSubscriptionInvitationUuid: '1-2-3',
})
expect(result.statusCode).toEqual(400)
expect(result.status).toEqual(400)
})
it('should invite to user subscription', async () => {
request.body.identifier = 'invitee@test.te'
response.locals.user = {
uuid: '1-2-3',
email: 'test@test.te',
}
inviteToSharedSubscription.execute = jest.fn().mockReturnValue({
success: true,
})
const httpResponse = <results.JsonResult>await createController().inviteToSubscriptionSharing(request, response)
const result = await httpResponse.executeAsync()
const result = await createController().invite({
api: ApiVersion.v0,
identifier: 'invitee@test.te',
inviterUuid: '1-2-3',
inviterEmail: 'test@test.te',
inviterRoles: ['CORE_USER'],
})
expect(inviteToSharedSubscription.execute).toHaveBeenCalledWith({
inviterEmail: 'test@test.te',
@@ -207,37 +182,36 @@ describe('SubscriptionInvitesController', () => {
inviterRoles: ['CORE_USER'],
})
expect(result.statusCode).toEqual(200)
expect(result.status).toEqual(200)
})
it('should not invite to user subscription if the identifier is missing in request', async () => {
response.locals.user = {
uuid: '1-2-3',
email: 'test@test.te',
}
const httpResponse = <results.JsonResult>await createController().inviteToSubscriptionSharing(request, response)
const result = await httpResponse.executeAsync()
const result = await createController().invite({
api: ApiVersion.v0,
identifier: '',
inviterUuid: '1-2-3',
inviterEmail: 'test@test.te',
inviterRoles: ['CORE_USER'],
})
expect(inviteToSharedSubscription.execute).not.toHaveBeenCalled()
expect(result.statusCode).toEqual(400)
expect(result.status).toEqual(400)
})
it('should not invite to user subscription if the workflow does not run', async () => {
request.body.identifier = 'invitee@test.te'
response.locals.user = {
uuid: '1-2-3',
email: 'test@test.te',
}
inviteToSharedSubscription.execute = jest.fn().mockReturnValue({
success: false,
})
const httpResponse = <results.JsonResult>await createController().inviteToSubscriptionSharing(request, response)
const result = await httpResponse.executeAsync()
const result = await createController().invite({
api: ApiVersion.v0,
identifier: 'invitee@test.te',
inviterUuid: '1-2-3',
inviterEmail: 'test@test.te',
inviterRoles: ['CORE_USER'],
})
expect(result.statusCode).toEqual(400)
expect(result.status).toEqual(400)
})
})

View File

@@ -1,15 +1,19 @@
import { Role } from '@standardnotes/security'
import { Request, Response } from 'express'
import { inject } from 'inversify'
import {
BaseHttpController,
controller,
httpDelete,
httpGet,
httpPost,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
results,
} from 'inversify-express-utils'
HttpStatusCode,
SubscriptionInviteAcceptRequestParams,
SubscriptionInviteAcceptResponse,
SubscriptionInviteCancelRequestParams,
SubscriptionInviteCancelResponse,
SubscriptionInviteDeclineRequestParams,
SubscriptionInviteDeclineResponse,
SubscriptionInviteListRequestParams,
SubscriptionInviteListResponse,
SubscriptionInviteRequestParams,
SubscriptionInviteResponse,
SubscriptionServerInterface,
} from '@standardnotes/api'
import { RoleName } from '@standardnotes/common'
import { inject, injectable } from 'inversify'
import TYPES from '../Bootstrap/Types'
import { AcceptSharedSubscriptionInvitation } from '../Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation'
@@ -18,8 +22,8 @@ import { DeclineSharedSubscriptionInvitation } from '../Domain/UseCase/DeclineSh
import { InviteToSharedSubscription } from '../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
@controller('/subscription-invites')
export class SubscriptionInvitesController extends BaseHttpController {
@injectable()
export class SubscriptionInvitesController implements SubscriptionServerInterface {
constructor(
@inject(TYPES.InviteToSharedSubscription) private inviteToSharedSubscription: InviteToSharedSubscription,
@inject(TYPES.AcceptSharedSubscriptionInvitation)
@@ -30,75 +34,103 @@ export class SubscriptionInvitesController extends BaseHttpController {
private cancelSharedSubscriptionInvitation: CancelSharedSubscriptionInvitation,
@inject(TYPES.ListSharedSubscriptionInvitations)
private listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations,
) {
super()
}
) {}
@httpGet('/:inviteUuid/accept')
async acceptInvite(request: Request): Promise<results.JsonResult> {
async acceptInvite(params: SubscriptionInviteAcceptRequestParams): Promise<SubscriptionInviteAcceptResponse> {
const result = await this.acceptSharedSubscriptionInvitation.execute({
sharedSubscriptionInvitationUuid: request.params.inviteUuid,
sharedSubscriptionInvitationUuid: params.inviteUuid,
})
if (result.success) {
return this.json(result)
return {
status: HttpStatusCode.Success,
data: result,
}
}
return this.json(result, 400)
return {
status: HttpStatusCode.BadRequest,
data: result,
}
}
@httpGet('/:inviteUuid/decline')
async declineInvite(request: Request): Promise<results.JsonResult> {
async declineInvite(params: SubscriptionInviteDeclineRequestParams): Promise<SubscriptionInviteDeclineResponse> {
const result = await this.declineSharedSubscriptionInvitation.execute({
sharedSubscriptionInvitationUuid: request.params.inviteUuid,
sharedSubscriptionInvitationUuid: params.inviteUuid,
})
if (result.success) {
return this.json(result)
return {
status: HttpStatusCode.Success,
data: result,
}
}
return this.json(result, 400)
return {
status: HttpStatusCode.BadRequest,
data: result,
}
}
@httpPost('/', TYPES.ApiGatewayAuthMiddleware)
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
if (!request.body.identifier) {
return this.json({ error: { message: 'Missing invitee identifier' } }, 400)
async invite(params: SubscriptionInviteRequestParams): Promise<SubscriptionInviteResponse> {
if (!params.identifier) {
return {
status: HttpStatusCode.BadRequest,
data: {
error: {
message: 'Missing invitee identifier',
},
},
}
}
const result = await this.inviteToSharedSubscription.execute({
inviterEmail: response.locals.user.email,
inviterUuid: response.locals.user.uuid,
inviteeIdentifier: request.body.identifier,
inviterRoles: response.locals.roles.map((role: Role) => role.name),
inviterEmail: params.inviterEmail as string,
inviterUuid: params.inviterUuid as string,
inviteeIdentifier: params.identifier,
inviterRoles: params.inviterRoles as RoleName[],
})
if (result.success) {
return this.json(result)
return {
status: HttpStatusCode.Success,
data: result,
}
}
return this.json(result, 400)
return {
status: HttpStatusCode.BadRequest,
data: result,
}
}
@httpDelete('/:inviteUuid', TYPES.ApiGatewayAuthMiddleware)
async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
async cancelInvite(params: SubscriptionInviteCancelRequestParams): Promise<SubscriptionInviteCancelResponse> {
const result = await this.cancelSharedSubscriptionInvitation.execute({
sharedSubscriptionInvitationUuid: request.params.inviteUuid,
inviterEmail: response.locals.user.email,
sharedSubscriptionInvitationUuid: params.inviteUuid,
inviterEmail: params.inviterEmail as string,
})
if (result.success) {
return this.json(result)
return {
status: HttpStatusCode.Success,
data: result,
}
}
return this.json(result, 400)
return {
status: HttpStatusCode.BadRequest,
data: result,
}
}
@httpGet('/', TYPES.ApiGatewayAuthMiddleware)
async listInvites(_request: Request, response: Response): Promise<results.JsonResult> {
async listInvites(params: SubscriptionInviteListRequestParams): Promise<SubscriptionInviteListResponse> {
const result = await this.listSharedSubscriptionInvitations.execute({
inviterEmail: response.locals.user.email,
inviterEmail: params.inviterEmail as string,
})
return this.json(result)
return {
status: HttpStatusCode.Success,
data: result,
}
}
}

View File

@@ -4,18 +4,23 @@ import { Request, Response } from 'express'
import { results } from 'inversify-express-utils'
import { ValetTokenController } from './ValetTokenController'
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
import { Uuid, ValidatorInterface } from '@standardnotes/common'
describe('ValetTokenController', () => {
let createValetToken: CreateValetToken
let uuidValidator: ValidatorInterface<Uuid>
let request: Request
let response: Response
const createController = () => new ValetTokenController(createValetToken)
const createController = () => new ValetTokenController(createValetToken, uuidValidator)
beforeEach(() => {
createValetToken = {} as jest.Mocked<CreateValetToken>
createValetToken.execute = jest.fn().mockReturnValue({ success: true, valetToken: 'foobar' })
uuidValidator = {} as jest.Mocked<ValidatorInterface<Uuid>>
uuidValidator.validate = jest.fn().mockReturnValue(true)
request = {
body: {
operation: 'write',
@@ -42,6 +47,17 @@ describe('ValetTokenController', () => {
expect(await result.content.readAsStringAsync()).toEqual('{"success":true,"valetToken":"foobar"}')
})
it('should not create a valet token if the remote resource identifier is not a valid uuid', async () => {
uuidValidator.validate = jest.fn().mockReturnValue(false)
const httpResponse = <results.JsonResult>await createController().create(request, response)
const result = await httpResponse.executeAsync()
expect(createValetToken.execute).not.toHaveBeenCalled()
expect(result.statusCode).toEqual(400)
})
it('should create a read valet token for read only access session', async () => {
response.locals.readOnlyAccess = true
request.body.operation = 'read'

View File

@@ -11,11 +11,15 @@ import { CreateValetTokenPayload } from '@standardnotes/responses'
import TYPES from '../Bootstrap/Types'
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
import { ErrorTag } from '@standardnotes/common'
import { ErrorTag, Uuid, ValidatorInterface } from '@standardnotes/common'
import { ValetTokenOperation } from '@standardnotes/security'
@controller('/valet-tokens', TYPES.ApiGatewayAuthMiddleware)
export class ValetTokenController extends BaseHttpController {
constructor(@inject(TYPES.CreateValetToken) private createValetKey: CreateValetToken) {
constructor(
@inject(TYPES.CreateValetToken) private createValetKey: CreateValetToken,
@inject(TYPES.UuidValidator) private uuidValitor: ValidatorInterface<Uuid>,
) {
super()
}
@@ -35,9 +39,23 @@ export class ValetTokenController extends BaseHttpController {
)
}
for (const resource of payload.resources) {
if (!this.uuidValitor.validate(resource.remoteIdentifier)) {
return this.json(
{
error: {
tag: ErrorTag.ParametersInvalid,
message: 'Invalid remote resource identifier.',
},
},
400,
)
}
}
const createValetKeyResponse = await this.createValetKey.execute({
userUuid: response.locals.user.uuid,
operation: payload.operation,
operation: payload.operation as ValetTokenOperation,
resources: payload.resources,
})

View File

@@ -82,6 +82,7 @@ describe('FeatureService', () => {
uuid: 'subscription-1-1-1',
createdAt: 111,
updatedAt: 222,
renewedAt: null,
planName: SubscriptionName.PlusPlan,
endsAt: 555,
user: Promise.resolve(user),
@@ -95,6 +96,7 @@ describe('FeatureService', () => {
uuid: 'subscription-2-2-2',
createdAt: 222,
updatedAt: 333,
renewedAt: null,
planName: SubscriptionName.ProPlan,
endsAt: 777,
user: Promise.resolve(user),
@@ -108,6 +110,7 @@ describe('FeatureService', () => {
uuid: 'subscription-3-3-3-canceled',
createdAt: 111,
updatedAt: 222,
renewedAt: null,
planName: SubscriptionName.PlusPlan,
endsAt: 333,
user: Promise.resolve(user),
@@ -121,6 +124,7 @@ describe('FeatureService', () => {
uuid: 'subscription-4-4-4-canceled',
createdAt: 111,
updatedAt: 222,
renewedAt: null,
planName: SubscriptionName.PlusPlan,
endsAt: 333,
user: Promise.resolve(user),
@@ -240,6 +244,7 @@ describe('FeatureService', () => {
uuid: 'subscription-1-1-1',
createdAt: 111,
updatedAt: 222,
renewedAt: null,
planName: 'non existing plan name' as SubscriptionName,
endsAt: 555,
user: Promise.resolve(user),

View File

@@ -12,7 +12,8 @@ import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { AnalyticsStoreInterface } from '@standardnotes/analytics'
import { AnalyticsStoreInterface, StatisticsStoreInterface } from '@standardnotes/analytics'
import { TimerInterface } from '@standardnotes/time'
describe('AccountDeletionRequestedEventHandler', () => {
let userRepository: UserRepositoryInterface
@@ -27,6 +28,8 @@ describe('AccountDeletionRequestedEventHandler', () => {
let event: AccountDeletionRequestedEvent
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let timer: TimerInterface
const createHandler = () =>
new AccountDeletionRequestedEventHandler(
@@ -36,6 +39,8 @@ describe('AccountDeletionRequestedEventHandler', () => {
revokedSessionRepository,
getUserAnalyticsId,
analyticsStore,
statisticsStore,
timer,
logger,
)
@@ -87,6 +92,13 @@ describe('AccountDeletionRequestedEventHandler', () => {
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
logger.warn = jest.fn()
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.incrementMeasure = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(100)
})
it('should remove a user', async () => {

View File

@@ -1,5 +1,12 @@
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
import {
AnalyticsActivity,
AnalyticsStoreInterface,
Period,
StatisticsMeasure,
StatisticsStoreInterface,
} from '@standardnotes/analytics'
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
@@ -18,6 +25,8 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
@inject(TYPES.RevokedSessionRepository) private revokedSessionRepository: RevokedSessionRepositoryInterface,
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
@@ -39,6 +48,14 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
Period.ThisMonth,
])
const registrationLength =
this.timer.getTimestampInMicroseconds() - this.timer.convertDateToMicroseconds(user.createdAt)
await this.statisticsStore.incrementMeasure(StatisticsMeasure.RegistrationLength, registrationLength, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
await this.userRepository.remove(user)
this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)

View File

@@ -0,0 +1,51 @@
import 'reflect-metadata'
import { PaymentFailedEvent } from '@standardnotes/domain-events'
import { AnalyticsStoreInterface } from '@standardnotes/analytics'
import { PaymentFailedEventHandler } from './PaymentFailedEventHandler'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { User } from '../User/User'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
describe('PaymentFailedEventHandler', () => {
let userRepository: UserRepositoryInterface
let event: PaymentFailedEvent
let user: User
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
const createHandler = () => new PaymentFailedEventHandler(userRepository, getUserAnalyticsId, analyticsStore)
beforeEach(() => {
user = {} as jest.Mocked<User>
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
event = {} as jest.Mocked<PaymentFailedEvent>
event.payload = {
userEmail: 'test@test.com',
}
})
it('should mark payment failed for analytics', async () => {
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalled()
})
it('should not mark payment failed for analytics if user is not found', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
await createHandler().handle(event)
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,30 @@
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
import { DomainEventHandlerInterface, PaymentFailedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
@injectable()
export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
) {}
async handle(event: PaymentFailedEvent): Promise<void> {
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
if (user === null) {
return
}
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
await this.analyticsStore.markActivity([AnalyticsActivity.PaymentFailed], analyticsId, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
}
}

View File

@@ -0,0 +1,58 @@
import 'reflect-metadata'
import { PaymentSuccessEvent } from '@standardnotes/domain-events'
import { AnalyticsStoreInterface, StatisticsStoreInterface } from '@standardnotes/analytics'
import { PaymentSuccessEventHandler } from './PaymentSuccessEventHandler'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { User } from '../User/User'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
describe('PaymentSuccessEventHandler', () => {
let userRepository: UserRepositoryInterface
let event: PaymentSuccessEvent
let user: User
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
const createHandler = () =>
new PaymentSuccessEventHandler(userRepository, getUserAnalyticsId, analyticsStore, statisticsStore)
beforeEach(() => {
user = {} as jest.Mocked<User>
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.incrementMeasure = jest.fn()
event = {} as jest.Mocked<PaymentSuccessEvent>
event.payload = {
userEmail: 'test@test.com',
amount: 12.45,
}
})
it('should mark payment failed for analytics', async () => {
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalled()
expect(statisticsStore.incrementMeasure).toHaveBeenCalled()
})
it('should not mark payment failed for analytics if user is not found', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
await createHandler().handle(event)
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,43 @@
import {
AnalyticsActivity,
AnalyticsStoreInterface,
Period,
StatisticsMeasure,
StatisticsStoreInterface,
} from '@standardnotes/analytics'
import { DomainEventHandlerInterface, PaymentSuccessEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
@injectable()
export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
) {}
async handle(event: PaymentSuccessEvent): Promise<void> {
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
if (user === null) {
return
}
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
await this.analyticsStore.markActivity([AnalyticsActivity.PaymentSuccess], analyticsId, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
await this.statisticsStore.incrementMeasure(StatisticsMeasure.Income, event.payload.amount, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
}
}

View File

@@ -0,0 +1,34 @@
import 'reflect-metadata'
import { RefundProcessedEvent } from '@standardnotes/domain-events'
import { Period, StatisticsMeasure, StatisticsStoreInterface } from '@standardnotes/analytics'
import { RefundProcessedEventHandler } from './RefundProcessedEventHandler'
describe('RefundProcessedEventHandler', () => {
let event: RefundProcessedEvent
let statisticsStore: StatisticsStoreInterface
const createHandler = () => new RefundProcessedEventHandler(statisticsStore)
beforeEach(() => {
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.incrementMeasure = jest.fn()
event = {} as jest.Mocked<RefundProcessedEvent>
event.payload = {
userEmail: 'test@test.com',
amount: 12.45,
}
})
it('should mark refunds for statistics', async () => {
await createHandler().handle(event)
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith(StatisticsMeasure.Refunds, 12.45, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
})
})

View File

@@ -0,0 +1,18 @@
import { Period, StatisticsMeasure, StatisticsStoreInterface } from '@standardnotes/analytics'
import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
@injectable()
export class RefundProcessedEventHandler implements DomainEventHandlerInterface {
constructor(@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface) {}
async handle(event: RefundProcessedEvent): Promise<void> {
await this.statisticsStore.incrementMeasure(StatisticsMeasure.Refunds, event.payload.amount, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
}
}

View File

@@ -8,10 +8,11 @@ import * as dayjs from 'dayjs'
import { SubscriptionCancelledEventHandler } from './SubscriptionCancelledEventHandler'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { AnalyticsStoreInterface } from '@standardnotes/analytics'
import { AnalyticsStoreInterface, Period, StatisticsMeasure, StatisticsStoreInterface } from '@standardnotes/analytics'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { User } from '../User/User'
import { UserSubscription } from '../Subscription/UserSubscription'
describe('SubscriptionCancelledEventHandler', () => {
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
@@ -20,6 +21,7 @@ describe('SubscriptionCancelledEventHandler', () => {
let userRepository: UserRepositoryInterface
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let timestamp: number
const createHandler = () =>
@@ -29,6 +31,7 @@ describe('SubscriptionCancelledEventHandler', () => {
userRepository,
getUserAnalyticsId,
analyticsStore,
statisticsStore,
)
beforeEach(() => {
@@ -43,8 +46,16 @@ describe('SubscriptionCancelledEventHandler', () => {
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.incrementMeasure = jest.fn()
const userSubscription = {
createdAt: 1642395451515000,
} as jest.Mocked<UserSubscription>
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
userSubscriptionRepository.updateCancelled = jest.fn()
userSubscriptionRepository.findBySubscriptionId = jest.fn().mockReturnValue([userSubscription])
offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
offlineUserSubscriptionRepository.updateCancelled = jest.fn()
@@ -59,14 +70,21 @@ describe('SubscriptionCancelledEventHandler', () => {
subscriptionName: SubscriptionName.ProPlan,
timestamp,
offline: false,
replaced: false,
}
})
it('should update subscription cancelled', async () => {
event.payload.timestamp = 1642395451516000
await createHandler().handle(event)
expect(userSubscriptionRepository.updateCancelled).toHaveBeenCalledWith(1, true, timestamp)
expect(userSubscriptionRepository.updateCancelled).toHaveBeenCalledWith(1, true, 1642395451516000)
expect(analyticsStore.markActivity).toHaveBeenCalled()
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith(StatisticsMeasure.SubscriptionLength, 1000, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
})
it('should update subscription cancelled - user not found', async () => {

View File

@@ -1,12 +1,19 @@
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import {
AnalyticsActivity,
AnalyticsStoreInterface,
Period,
StatisticsMeasure,
StatisticsStoreInterface,
} from '@standardnotes/analytics'
import TYPES from '../../Bootstrap/Types'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
import { UserSubscription } from '../Subscription/UserSubscription'
@injectable()
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
@@ -17,16 +24,9 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
) {}
async handle(event: SubscriptionCancelledEvent): Promise<void> {
if (event.payload.offline) {
await this.updateOfflineSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
return
}
await this.updateSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
if (user !== null) {
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
@@ -35,7 +35,38 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
Period.ThisWeek,
Period.ThisMonth,
])
const subscriptions = await this.userSubscriptionRepository.findBySubscriptionId(event.payload.subscriptionId)
if (subscriptions.length !== 0) {
const lastSubscription = subscriptions.shift() as UserSubscription
const subscriptionLength = event.payload.timestamp - lastSubscription.createdAt
await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
const lastPurchaseTime = lastSubscription.renewedAt ?? lastSubscription.updatedAt
const remainingSubscriptionTime = lastSubscription.endsAt - event.payload.timestamp
const totalSubscriptionTime = lastSubscription.endsAt - lastPurchaseTime
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
await this.statisticsStore.incrementMeasure(
StatisticsMeasure.RemainingSubscriptionTimePercentage,
remainingSubscriptionPercentage,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
}
}
if (event.payload.offline) {
await this.updateOfflineSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
return
}
await this.updateSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
}
private async updateSubscriptionCancelled(subscriptionId: number, timestamp: number): Promise<void> {

View File

@@ -16,9 +16,10 @@ import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/Offl
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
import { AnalyticsStoreInterface, Period, StatisticsStoreInterface } from '@standardnotes/analytics'
import { AnalyticsEntity } from '../Analytics/AnalyticsEntity'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { TimerInterface } from '@standardnotes/time'
describe('SubscriptionPurchasedEventHandler', () => {
let userRepository: UserRepositoryInterface
@@ -35,6 +36,8 @@ describe('SubscriptionPurchasedEventHandler', () => {
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let timestamp: number
let statisticsStore: StatisticsStoreInterface
let timer: TimerInterface
const createHandler = () =>
new SubscriptionPurchasedEventHandler(
@@ -45,6 +48,8 @@ describe('SubscriptionPurchasedEventHandler', () => {
subscriptionSettingService,
getUserAnalyticsId,
analyticsStore,
statisticsStore,
timer,
logger,
)
@@ -66,7 +71,14 @@ describe('SubscriptionPurchasedEventHandler', () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
userRepository.save = jest.fn().mockReturnValue(user)
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.incrementMeasure = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(1)
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(0)
userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
offlineUserSubscription = {} as jest.Mocked<OfflineUserSubscription>
@@ -146,6 +158,15 @@ describe('SubscriptionPurchasedEventHandler', () => {
updatedAt: expect.any(Number),
cancelled: false,
})
expect(statisticsStore.incrementMeasure).toHaveBeenCalled()
})
it("should not measure registration to subscription time if this is not user's first subscription", async () => {
userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(1)
await createHandler().handle(event)
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
})
it('should update analytics on limited discount offer purchasing', async () => {

View File

@@ -13,8 +13,15 @@ import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
import {
AnalyticsActivity,
AnalyticsStoreInterface,
Period,
StatisticsMeasure,
StatisticsStoreInterface,
} from '@standardnotes/analytics'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { TimerInterface } from '@standardnotes/time'
@injectable()
export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInterface {
@@ -27,6 +34,8 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
@inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface,
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
@@ -52,6 +61,8 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
return
}
const previousSubscriptionCount = await this.userSubscriptionRepository.countByUserUuid(user.uuid)
const userSubscription = await this.createSubscription(
event.payload.subscriptionId,
event.payload.subscriptionName,
@@ -74,12 +85,20 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
Period.ThisMonth,
])
const limitedDiscountPurchased = event.payload.discountCode === 'limited-10'
const limitedDiscountPurchased = ['limited-10', 'limited-20'].includes(event.payload.discountCode as string)
if (limitedDiscountPurchased) {
await this.analyticsStore.markActivity([AnalyticsActivity.LimitedDiscountOfferPurchased], analyticsId, [
Period.Today,
])
}
if (previousSubscriptionCount === 0) {
await this.statisticsStore.incrementMeasure(
StatisticsMeasure.RegistrationToSubscriptionTime,
event.payload.timestamp - this.timer.convertDateToMicroseconds(user.createdAt),
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
}
}
private async addUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {

View File

@@ -4,17 +4,23 @@ import { UserDisabledSessionUserAgentLoggingEvent } from '@standardnotes/domain-
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
import { UserDisabledSessionUserAgentLoggingEventHandler } from './UserDisabledSessionUserAgentLoggingEventHandler'
import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface'
describe('UserDisabledSessionUserAgentLoggingEventHandler', () => {
let sessionRepository: SessionRepositoryInterface
let revokedSessionRepository: RevokedSessionRepositoryInterface
let event: UserDisabledSessionUserAgentLoggingEvent
const createHandler = () => new UserDisabledSessionUserAgentLoggingEventHandler(sessionRepository)
const createHandler = () =>
new UserDisabledSessionUserAgentLoggingEventHandler(sessionRepository, revokedSessionRepository)
beforeEach(() => {
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
sessionRepository.clearUserAgentByUserUuid = jest.fn()
revokedSessionRepository = {} as jest.Mocked<RevokedSessionRepositoryInterface>
revokedSessionRepository.clearUserAgentByUserUuid = jest.fn()
event = {} as jest.Mocked<UserDisabledSessionUserAgentLoggingEvent>
event.payload = {
userUuid: '1-2-3',
@@ -26,5 +32,6 @@ describe('UserDisabledSessionUserAgentLoggingEventHandler', () => {
await createHandler().handle(event)
expect(sessionRepository.clearUserAgentByUserUuid).toHaveBeenCalledWith('1-2-3')
expect(revokedSessionRepository.clearUserAgentByUserUuid).toHaveBeenCalledWith('1-2-3')
})
})

View File

@@ -2,13 +2,18 @@ import { DomainEventHandlerInterface, UserDisabledSessionUserAgentLoggingEvent }
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface'
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
@injectable()
export class UserDisabledSessionUserAgentLoggingEventHandler implements DomainEventHandlerInterface {
constructor(@inject(TYPES.SessionRepository) private sessionRepository: SessionRepositoryInterface) {}
constructor(
@inject(TYPES.SessionRepository) private sessionRepository: SessionRepositoryInterface,
@inject(TYPES.SessionRepository) private revokedSessionRepository: RevokedSessionRepositoryInterface,
) {}
async handle(event: UserDisabledSessionUserAgentLoggingEvent): Promise<void> {
await this.sessionRepository.clearUserAgentByUserUuid(event.payload.userUuid)
await this.revokedSessionRepository.clearUserAgentByUserUuid(event.payload.userUuid)
}
}

View File

@@ -36,4 +36,25 @@ export class RevokedSession {
)
@JoinColumn({ name: 'user_uuid', referencedColumnName: 'uuid' })
declare user: Promise<User>
@Column({
name: 'received_at',
type: 'datetime',
nullable: true,
})
declare receivedAt: Date
@Column({
name: 'user_agent',
type: 'text',
nullable: true,
})
declare userAgent: string | null
@Column({
name: 'api_version',
length: 255,
nullable: true,
})
declare apiVersion: string
}

View File

@@ -1,3 +1,4 @@
import { Uuid } from '@standardnotes/common'
import { RevokedSession } from './RevokedSession'
export interface RevokedSessionRepositoryInterface {
@@ -5,4 +6,5 @@ export interface RevokedSessionRepositoryInterface {
findAllByUserUuid(userUuid: string): Promise<Array<RevokedSession>>
save(revokedSession: RevokedSession): Promise<RevokedSession>
remove(revokedSession: RevokedSession): Promise<RevokedSession>
clearUserAgentByUserUuid(userUuid: Uuid): Promise<void>
}

View File

@@ -2,6 +2,7 @@ import 'reflect-metadata'
import * as winston from 'winston'
import { TimerInterface } from '@standardnotes/time'
import { ApiVersion } from '../Api/ApiVersion'
import { Session } from './Session'
import { SessionRepositoryInterface } from './SessionRepositoryInterface'
import { SessionService } from './SessionService'
@@ -47,6 +48,7 @@ describe('SessionService', () => {
session.uuid = '2e1e43'
session.userUuid = '1-2-3'
session.userAgent = 'Chrome'
session.apiVersion = ApiVersion.v20200115
session.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
session.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
@@ -80,6 +82,7 @@ describe('SessionService', () => {
timer = {} as jest.Mocked<TimerInterface>
timer.convertStringDateToMilliseconds = jest.fn().mockReturnValue(123)
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
deviceDetector = {} as jest.Mocked<UAParser>
deviceDetector.setUA = jest.fn().mockReturnThis()
@@ -111,6 +114,7 @@ describe('SessionService', () => {
expect(revokedSessionRepository.save).toHaveBeenCalledWith({
uuid: '2e1e43',
received: true,
receivedAt: new Date(1),
})
})
@@ -476,6 +480,8 @@ describe('SessionService', () => {
expect(revokedSessionRepository.save).toHaveBeenCalledWith({
uuid: '2e1e43',
userUuid: '1-2-3',
userAgent: 'Chrome',
apiVersion: '20200115',
createdAt: expect.any(Date),
})
})

View File

@@ -203,6 +203,7 @@ export class SessionService implements SessionServiceInterface {
async markRevokedSessionAsReceived(revokedSession: RevokedSession): Promise<RevokedSession> {
revokedSession.received = true
revokedSession.receivedAt = this.timer.getUTCDate()
return this.revokedSessionRepository.save(revokedSession)
}
@@ -224,7 +225,9 @@ export class SessionService implements SessionServiceInterface {
const revokedSession = new RevokedSession()
revokedSession.uuid = session.uuid
revokedSession.userUuid = session.userUuid
revokedSession.createdAt = dayjs.utc().toDate()
revokedSession.createdAt = this.timer.getUTCDate()
revokedSession.apiVersion = session.apiVersion
revokedSession.userAgent = session.userAgent
return this.revokedSessionRepository.save(revokedSession)
}
@@ -246,8 +249,8 @@ export class SessionService implements SessionServiceInterface {
}
session.userUuid = dto.user.uuid
session.apiVersion = dto.apiVersion
session.createdAt = dayjs.utc().toDate()
session.updatedAt = dayjs.utc().toDate()
session.createdAt = this.timer.getUTCDate()
session.updatedAt = this.timer.getUTCDate()
session.readonlyAccess = dto.readonlyAccess
return session

View File

@@ -7,5 +7,9 @@ export interface SharedSubscriptionInvitationRepositoryInterface {
findOneByUuidAndStatus(uuid: Uuid, status: InvitationStatus): Promise<SharedSubscriptionInvitation | null>
findOneByUuid(uuid: Uuid): Promise<SharedSubscriptionInvitation | null>
findByInviterEmail(inviterEmail: string): Promise<SharedSubscriptionInvitation[]>
findOneByInviteeAndInviterEmail(
inviteeEmail: string,
inviterEmail: string,
): Promise<SharedSubscriptionInvitation | null>
countByInviterEmailAndStatus(inviterEmail: Uuid, statuses: InvitationStatus[]): Promise<number>
}

View File

@@ -34,6 +34,13 @@ export class UserSubscription {
@Index('updated_at')
declare updatedAt: number
@Column({
name: 'renewed_at',
type: 'bigint',
nullable: true,
})
declare renewedAt: number | null
@Column({
type: 'tinyint',
width: 1,

View File

@@ -4,6 +4,7 @@ import { UserSubscriptionType } from './UserSubscriptionType'
export interface UserSubscriptionRepositoryInterface {
findOneByUuid(uuid: Uuid): Promise<UserSubscription | null>
countByUserUuid(userUuid: Uuid): Promise<number>
findOneByUserUuid(userUuid: Uuid): Promise<UserSubscription | null>
findOneByUserUuidAndSubscriptionId(userUuid: Uuid, subscriptionId: number): Promise<UserSubscription | null>
findBySubscriptionIdAndType(subscriptionId: number, type: UserSubscriptionType): Promise<UserSubscription[]>

View File

@@ -16,6 +16,7 @@ import { DomainEventPublisherInterface, SharedSubscriptionInvitationCanceledEven
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { InviterIdentifierType } from '../../SharedSubscription/InviterIdentifierType'
import { InviteeIdentifierType } from '../../SharedSubscription/InviteeIdentifierType'
import { Logger } from 'winston'
describe('CancelSharedSubscriptionInvitation', () => {
let sharedSubscriptionInvitationRepository: SharedSubscriptionInvitationRepositoryInterface
@@ -28,6 +29,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
let invitation: SharedSubscriptionInvitation
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let logger: Logger
const createUseCase = () =>
new CancelSharedSubscriptionInvitation(
@@ -38,6 +40,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
domainEventPublisher,
domainEventFactory,
timer,
logger,
)
beforeEach(() => {
@@ -60,6 +63,9 @@ describe('CancelSharedSubscriptionInvitation', () => {
inviteeIdentifierType: InviteeIdentifierType.Email,
} as jest.Mocked<SharedSubscriptionInvitation>
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
sharedSubscriptionInvitationRepository = {} as jest.Mocked<SharedSubscriptionInvitationRepositoryInterface>
sharedSubscriptionInvitationRepository.findOneByUuid = jest.fn().mockReturnValue(invitation)
sharedSubscriptionInvitationRepository.save = jest.fn()
@@ -126,7 +132,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
})
})
it('should cancel a shared subscription invitation without subscription removal is subscription is not found', async () => {
it('should cancel a shared subscription invitation without subscription removal if subscription is not found', async () => {
userSubscriptionRepository.findOneByUserUuidAndSubscriptionId = jest.fn().mockReturnValue(null)
expect(
@@ -175,7 +181,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
})
})
it('should not cancel a shared subscription invitation if invitee is not found', async () => {
it('should cancel a shared subscription invitation without subscription removal if invitee is not found', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({
@@ -183,20 +189,21 @@ describe('CancelSharedSubscriptionInvitation', () => {
inviterEmail: 'test@test.te',
}),
).toEqual({
success: false,
success: true,
})
})
it('should not cancel a shared subscription invitation if invitee is not found', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({
sharedSubscriptionInvitationUuid: '1-2-3',
inviterEmail: 'test@test.te',
}),
).toEqual({
success: false,
expect(sharedSubscriptionInvitationRepository.save).toHaveBeenCalledWith({
status: 'canceled',
subscriptionId: 3,
updatedAt: 1,
inviterIdentifier: 'test@test.te',
uuid: '1-2-3',
inviterIdentifierType: 'email',
inviteeIdentifier: 'invitee@test.te',
inviteeIdentifierType: 'email',
})
expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.removeUserRole).not.toHaveBeenCalled()
})
it('should not cancel a shared subscription invitation if inviter subscription is not found', async () => {

View File

@@ -2,6 +2,7 @@ import { SubscriptionName } from '@standardnotes/common'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
@@ -29,6 +30,7 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
async execute(dto: CancelSharedSubscriptionInvitationDTO): Promise<CancelSharedSubscriptionInvitationResponse> {
@@ -36,29 +38,34 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
dto.sharedSubscriptionInvitationUuid,
)
if (sharedSubscriptionInvitation === null) {
this.logger.debug(
`Could not find a shared subscription invitation with uuid ${dto.sharedSubscriptionInvitationUuid}`,
)
return {
success: false,
}
}
if (dto.inviterEmail !== sharedSubscriptionInvitation.inviterIdentifier) {
this.logger.debug(
`Subscription belongs to a different inviter (${sharedSubscriptionInvitation.inviterIdentifier}). Modifier: ${dto.inviterEmail}`,
)
return {
success: false,
}
}
const invitee = await this.userRepository.findOneByEmail(sharedSubscriptionInvitation.inviteeIdentifier)
if (invitee === null) {
return {
success: false,
}
}
const inviterUserSubscriptions = await this.userSubscriptionRepository.findBySubscriptionIdAndType(
sharedSubscriptionInvitation.subscriptionId,
UserSubscriptionType.Regular,
)
if (inviterUserSubscriptions.length !== 1) {
if (inviterUserSubscriptions.length === 0) {
this.logger.debug(`Could not find a regular subscription with id ${sharedSubscriptionInvitation.subscriptionId}`)
return {
success: false,
}
@@ -70,20 +77,22 @@ export class CancelSharedSubscriptionInvitation implements UseCaseInterface {
await this.sharedSubscriptionInvitationRepository.save(sharedSubscriptionInvitation)
await this.removeSharedSubscription(sharedSubscriptionInvitation.subscriptionId, invitee)
if (invitee !== null) {
await this.removeSharedSubscription(sharedSubscriptionInvitation.subscriptionId, invitee)
await this.roleService.removeUserRole(invitee, inviterUserSubscription.planName as SubscriptionName)
await this.roleService.removeUserRole(invitee, inviterUserSubscription.planName as SubscriptionName)
await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedSubscriptionInvitationCanceledEvent({
inviteeIdentifier: invitee.uuid,
inviteeIdentifierType: InviteeIdentifierType.Uuid,
inviterEmail: sharedSubscriptionInvitation.inviterIdentifier,
inviterSubscriptionId: sharedSubscriptionInvitation.subscriptionId,
inviterSubscriptionUuid: inviterUserSubscription.uuid,
sharedSubscriptionInvitationUuid: sharedSubscriptionInvitation.uuid,
}),
)
await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedSubscriptionInvitationCanceledEvent({
inviteeIdentifier: invitee.uuid,
inviteeIdentifierType: InviteeIdentifierType.Uuid,
inviterEmail: sharedSubscriptionInvitation.inviterIdentifier,
inviterSubscriptionId: sharedSubscriptionInvitation.subscriptionId,
inviterSubscriptionUuid: inviterUserSubscription.uuid,
sharedSubscriptionInvitationUuid: sharedSubscriptionInvitation.uuid,
}),
)
}
return {
success: true,

View File

@@ -1,6 +1,6 @@
import 'reflect-metadata'
import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security'
import { TokenEncoderInterface, ValetTokenData, ValetTokenOperation } from '@standardnotes/security'
import { CreateValetToken } from './CreateValetToken'
import { TimerInterface } from '@standardnotes/time'
import { UserSubscription } from '../../Subscription/UserSubscription'
@@ -70,7 +70,7 @@ describe('CreateValetToken', () => {
it('should create a read valet token', async () => {
const response = await createUseCase().execute({
operation: 'read',
operation: ValetTokenOperation.Read,
userUuid: '1-2-3',
resources: [
{
@@ -92,7 +92,7 @@ describe('CreateValetToken', () => {
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
const response = await createUseCase().execute({
operation: 'read',
operation: ValetTokenOperation.Read,
userUuid: '1-2-3',
resources: [
{
@@ -117,7 +117,7 @@ describe('CreateValetToken', () => {
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(150)
const response = await createUseCase().execute({
operation: 'read',
operation: ValetTokenOperation.Read,
userUuid: '1-2-3',
resources: [
{
@@ -135,7 +135,7 @@ describe('CreateValetToken', () => {
it('should not create a write valet token if unencrypted file size has not been provided for a resource', async () => {
const response = await createUseCase().execute({
operation: 'write',
operation: ValetTokenOperation.Write,
resources: [
{
remoteIdentifier: '2-3-4',
@@ -152,7 +152,7 @@ describe('CreateValetToken', () => {
it('should create a write valet token', async () => {
const response = await createUseCase().execute({
operation: 'write',
operation: ValetTokenOperation.Write,
resources: [
{
remoteIdentifier: '2-3-4',
@@ -192,7 +192,7 @@ describe('CreateValetToken', () => {
.mockReturnValue({ regularSubscription, sharedSubscription })
const response = await createUseCase().execute({
operation: 'write',
operation: ValetTokenOperation.Write,
resources: [
{
remoteIdentifier: '2-3-4',
@@ -232,7 +232,7 @@ describe('CreateValetToken', () => {
.mockReturnValue({ regularSubscription: null, sharedSubscription })
const response = await createUseCase().execute({
operation: 'write',
operation: ValetTokenOperation.Write,
resources: [
{
remoteIdentifier: '2-3-4',
@@ -252,7 +252,7 @@ describe('CreateValetToken', () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
const response = await createUseCase().execute({
operation: 'write',
operation: ValetTokenOperation.Write,
userUuid: '1-2-3',
resources: [
{

View File

@@ -1,5 +1,10 @@
import { CreateValetTokenPayload } from '@standardnotes/responses'
import { ValetTokenOperation } from '@standardnotes/security'
export type CreateValetTokenDTO = CreateValetTokenPayload & {
export type CreateValetTokenDTO = {
operation: ValetTokenOperation
resources: Array<{
remoteIdentifier: string
unencryptedFileSize?: number
}>
userUuid: string
}

View File

@@ -9,6 +9,8 @@ import { InviteToSharedSubscription } from './InviteToSharedSubscription'
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { RoleName } from '@standardnotes/common'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
import { SharedSubscriptionInvitation } from '../../SharedSubscription/SharedSubscriptionInvitation'
describe('InviteToSharedSubscription', () => {
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
@@ -28,9 +30,10 @@ describe('InviteToSharedSubscription', () => {
beforeEach(() => {
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
userSubscriptionRepository.findOneByUserUuid = jest
.fn()
.mockReturnValue({ subscriptionId: 2 } as jest.Mocked<UserSubscription>)
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue({
subscriptionId: 2,
subscriptionType: UserSubscriptionType.Regular,
} as jest.Mocked<UserSubscription>)
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
@@ -38,6 +41,7 @@ describe('InviteToSharedSubscription', () => {
sharedSubscriptionInvitationRepository = {} as jest.Mocked<SharedSubscriptionInvitationRepositoryInterface>
sharedSubscriptionInvitationRepository.save = jest.fn().mockImplementation((same) => ({ ...same, uuid: '1-2-3' }))
sharedSubscriptionInvitationRepository.countByInviterEmailAndStatus = jest.fn().mockReturnValue(2)
sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail = jest.fn().mockReturnValue(null)
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
@@ -159,4 +163,46 @@ describe('InviteToSharedSubscription', () => {
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
it('should not create an inivitation for sharing the subscription if the inviter is on a shared subscription', async () => {
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue({
subscriptionId: 2,
subscriptionType: UserSubscriptionType.Shared,
} as jest.Mocked<UserSubscription>)
await createUseCase().execute({
inviteeIdentifier: 'invitee@test.te',
inviterUuid: '1-2-3',
inviterEmail: 'inviter@test.te',
inviterRoles: [RoleName.ProUser],
})
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(domainEventFactory.createSharedSubscriptionInvitationCreatedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should not create an invitation if it already exists', async () => {
sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail = jest
.fn()
.mockReturnValue({} as jest.Mocked<SharedSubscriptionInvitation>)
expect(
await createUseCase().execute({
inviteeIdentifier: 'invitee@test.te',
inviterUuid: '1-2-3',
inviterEmail: 'inviter@test.te',
inviterRoles: [RoleName.ProUser],
}),
).toEqual({
success: false,
})
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(domainEventFactory.createSharedSubscriptionInvitationCreatedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
})

View File

@@ -11,6 +11,7 @@ import { InviterIdentifierType } from '../../SharedSubscription/InviterIdentifie
import { SharedSubscriptionInvitation } from '../../SharedSubscription/SharedSubscriptionInvitation'
import { SharedSubscriptionInvitationRepositoryInterface } from '../../SharedSubscription/SharedSubscriptionInvitationRepositoryInterface'
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
import { UseCaseInterface } from '../UseCaseInterface'
import { InviteToSharedSubscriptionDTO } from './InviteToSharedSubscriptionDTO'
@@ -35,6 +36,13 @@ export class InviteToSharedSubscription implements UseCaseInterface {
}
}
const inviterUserSubscription = await this.userSubscriptionRepository.findOneByUserUuid(dto.inviterUuid)
if (inviterUserSubscription === null || inviterUserSubscription.subscriptionType === UserSubscriptionType.Shared) {
return {
success: false,
}
}
const numberOfUsedInvites = await this.sharedSubscriptionInvitationRepository.countByInviterEmailAndStatus(
dto.inviterEmail,
[InvitationStatus.Sent, InvitationStatus.Accepted],
@@ -45,8 +53,11 @@ export class InviteToSharedSubscription implements UseCaseInterface {
}
}
const inviterUserSubscription = await this.userSubscriptionRepository.findOneByUserUuid(dto.inviterUuid)
if (inviterUserSubscription === null) {
const existingInvitation = await this.sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail(
dto.inviteeIdentifier,
dto.inviterEmail,
)
if (existingInvitation !== null) {
return {
success: false,
}

View File

@@ -0,0 +1,77 @@
import { ApiVersion } from '@standardnotes/api'
import { Role } from '@standardnotes/security'
import { Request, Response } from 'express'
import { inject } from 'inversify'
import {
BaseHttpController,
controller,
httpDelete,
httpGet,
httpPost,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
results,
} from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { SubscriptionInvitesController } from '../../Controller/SubscriptionInvitesController'
@controller('/subscription-invites')
export class InversifyExpressSubscriptionInvitesController extends BaseHttpController {
constructor(
@inject(TYPES.SubscriptionInvitesController) private subscriptionInvitesController: SubscriptionInvitesController,
) {
super()
}
@httpGet('/:inviteUuid/accept')
async acceptInvite(request: Request): Promise<results.JsonResult> {
const response = await this.subscriptionInvitesController.acceptInvite({
api: request.query.api as ApiVersion,
inviteUuid: request.params.inviteUuid,
})
return this.json(response.data, response.status)
}
@httpGet('/:inviteUuid/decline')
async declineInvite(request: Request): Promise<results.JsonResult> {
const response = await this.subscriptionInvitesController.declineInvite({
api: request.query.api as ApiVersion,
inviteUuid: request.params.inviteUuid,
})
return this.json(response.data, response.status)
}
@httpPost('/', TYPES.ApiGatewayAuthMiddleware)
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.invite({
...request.body,
inviterEmail: response.locals.user.email,
inviterUuid: response.locals.user.uuid,
inviterRoles: response.locals.roles.map((role: Role) => role.name),
})
return this.json(result.data, result.status)
}
@httpDelete('/:inviteUuid', TYPES.ApiGatewayAuthMiddleware)
async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.cancelInvite({
...request.body,
inviteUuid: request.params.inviteUuid,
inviterEmail: response.locals.user.email,
})
return this.json(result.data, result.status)
}
@httpGet('/', TYPES.ApiGatewayAuthMiddleware)
async listInvites(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.subscriptionInvitesController.listInvites({
...request.body,
inviterEmail: response.locals.user.email,
})
return this.json(result.data, result.status)
}
}

View File

@@ -1,6 +1,6 @@
import 'reflect-metadata'
import { Repository, SelectQueryBuilder } from 'typeorm'
import { Repository, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm'
import { RevokedSession } from '../../Domain/Session/RevokedSession'
@@ -9,12 +9,14 @@ import { MySQLRevokedSessionRepository } from './MySQLRevokedSessionRepository'
describe('MySQLRevokedSessionRepository', () => {
let ormRepository: Repository<RevokedSession>
let queryBuilder: SelectQueryBuilder<RevokedSession>
let updateQueryBuilder: UpdateQueryBuilder<RevokedSession>
let session: RevokedSession
const createRepository = () => new MySQLRevokedSessionRepository(ormRepository)
beforeEach(() => {
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<RevokedSession>>
updateQueryBuilder = {} as jest.Mocked<UpdateQueryBuilder<RevokedSession>>
session = {} as jest.Mocked<RevokedSession>
@@ -36,6 +38,24 @@ describe('MySQLRevokedSessionRepository', () => {
expect(ormRepository.remove).toHaveBeenCalledWith(session)
})
it('should clear user agent data on all user sessions', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
updateQueryBuilder.update = jest.fn().mockReturnThis()
updateQueryBuilder.set = jest.fn().mockReturnThis()
updateQueryBuilder.where = jest.fn().mockReturnThis()
updateQueryBuilder.execute = jest.fn()
await createRepository().clearUserAgentByUserUuid('1-2-3')
expect(updateQueryBuilder.update).toHaveBeenCalled()
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
userAgent: null,
})
expect(updateQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :userUuid', { userUuid: '1-2-3' })
expect(updateQueryBuilder.execute).toHaveBeenCalled()
})
it('should find one session by id', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(session)

View File

@@ -20,6 +20,17 @@ export class MySQLRevokedSessionRepository implements RevokedSessionRepositoryIn
return this.ormRepository.remove(revokedSession)
}
async clearUserAgentByUserUuid(userUuid: string): Promise<void> {
await this.ormRepository
.createQueryBuilder('revoked_session')
.update()
.set({
userAgent: null,
})
.where('user_uuid = :userUuid', { userUuid })
.execute()
}
async findAllByUserUuid(userUuid: string): Promise<RevokedSession[]> {
return this.ormRepository
.createQueryBuilder('revoked_session')

View File

@@ -70,6 +70,23 @@ describe('MySQLSharedSubscriptionInvitationRepository', () => {
expect(result).toEqual(invitation)
})
it('should find one invitation by invitee and inviter email', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(invitation)
const result = await createRepository().findOneByInviteeAndInviterEmail('invitee@test.te', 'inviter@test.te')
expect(queryBuilder.where).toHaveBeenCalledWith(
'invitation.inviter_identifier = :inviterEmail AND invitation.invitee_identifier = :inviteeEmail',
{
inviterEmail: 'inviter@test.te',
inviteeEmail: 'invitee@test.te',
},
)
expect(result).toEqual(invitation)
})
it('should find one invitation by uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(invitation)

View File

@@ -13,6 +13,19 @@ export class MySQLSharedSubscriptionInvitationRepository implements SharedSubscr
private ormRepository: Repository<SharedSubscriptionInvitation>,
) {}
async findOneByInviteeAndInviterEmail(
inviteeEmail: string,
inviterEmail: string,
): Promise<SharedSubscriptionInvitation | null> {
return this.ormRepository
.createQueryBuilder('invitation')
.where('invitation.inviter_identifier = :inviterEmail AND invitation.invitee_identifier = :inviteeEmail', {
inviterEmail,
inviteeEmail,
})
.getOne()
}
async save(sharedSubscriptionInvitation: SharedSubscriptionInvitation): Promise<SharedSubscriptionInvitation> {
return this.ormRepository.save(sharedSubscriptionInvitation)
}

View File

@@ -75,6 +75,21 @@ describe('MySQLUserSubscriptionRepository', () => {
expect(result).toEqual(subscription)
})
it('should count by user uuid', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.getCount = jest.fn().mockReturnValue(2)
const result = await createRepository().countByUserUuid('123')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
user_uuid: '123',
})
expect(selectQueryBuilder.getCount).toHaveBeenCalled()
expect(result).toEqual(2)
})
it('should find one, longest lasting subscription by user uuid if there are no ucanceled ones', async () => {
subscription.cancelled = true
@@ -123,7 +138,8 @@ describe('MySQLUserSubscriptionRepository', () => {
expect(updateQueryBuilder.update).toHaveBeenCalled()
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
updatedAt: expect.any(Number),
updatedAt: 1000,
renewedAt: 1000,
endsAt: 1000,
})
expect(updateQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
@@ -157,6 +173,7 @@ describe('MySQLUserSubscriptionRepository', () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([subscription])
const result = await createRepository().findBySubscriptionId(123)
@@ -164,6 +181,7 @@ describe('MySQLUserSubscriptionRepository', () => {
expect(selectQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
subscriptionId: 123,
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('created_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual([subscription])
})
@@ -172,6 +190,7 @@ describe('MySQLUserSubscriptionRepository', () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([subscription])
const result = await createRepository().findBySubscriptionIdAndType(123, UserSubscriptionType.Regular)
@@ -183,6 +202,7 @@ describe('MySQLUserSubscriptionRepository', () => {
type: 'regular',
},
)
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('created_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual([subscription])
})

View File

@@ -14,6 +14,15 @@ export class MySQLUserSubscriptionRepository implements UserSubscriptionReposito
private ormRepository: Repository<UserSubscription>,
) {}
async countByUserUuid(userUuid: Uuid): Promise<number> {
return await this.ormRepository
.createQueryBuilder()
.where('user_uuid = :user_uuid', {
user_uuid: userUuid,
})
.getCount()
}
async save(subscription: UserSubscription): Promise<UserSubscription> {
return this.ormRepository.save(subscription)
}
@@ -35,6 +44,7 @@ export class MySQLUserSubscriptionRepository implements UserSubscriptionReposito
subscriptionId,
type,
})
.orderBy('created_at', 'DESC')
.getMany()
}
@@ -44,6 +54,7 @@ export class MySQLUserSubscriptionRepository implements UserSubscriptionReposito
.where('subscription_id = :subscriptionId', {
subscriptionId,
})
.orderBy('created_at', 'DESC')
.getMany()
}
@@ -77,13 +88,14 @@ export class MySQLUserSubscriptionRepository implements UserSubscriptionReposito
return null
}
async updateEndsAt(subscriptionId: number, endsAt: number, updatedAt: number): Promise<void> {
async updateEndsAt(subscriptionId: number, endsAt: number, timestamp: number): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.update()
.set({
endsAt,
updatedAt,
updatedAt: timestamp,
renewedAt: timestamp,
})
.where('subscription_id = :subscriptionId', {
subscriptionId,

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.33.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.32.0...@standardnotes/common@1.33.0) (2022-09-19)
### Features
* **files:** add validating remote identifiers ([db15457](https://github.com/standardnotes/server/commit/db15457ce4eb533ec822cf93c3ed83eafe9e64d5))
# [1.32.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.31.0...@standardnotes/common@1.32.0) (2022-09-09)
### Features
* **common:** add either and only types ([c3ebb32](https://github.com/standardnotes/server/commit/c3ebb321cfacd20769ebfd99413e283859b6e260))
# [1.31.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.30.0...@standardnotes/common@1.31.0) (2022-09-05)
### Features
* **auth:** add keeping stats on payments ([0c176b7](https://github.com/standardnotes/server/commit/0c176b70f8281e1e490224b9c7ab85f272a3d4e9))
# [1.30.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.29.0...@standardnotes/common@1.30.0) (2022-07-14)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/common",
"version": "1.30.0",
"version": "1.33.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -23,7 +23,7 @@
"prebuild": "yarn clean",
"build": "tsc -p tsconfig.json",
"lint": "eslint . --ext .ts",
"test:unit": "jest spec --coverage"
"test": "jest spec --coverage"
},
"devDependencies": {
"@types/jest": "^28.1.4",

View File

@@ -0,0 +1,3 @@
import { Only } from './Only'
export type Either<T, U> = Only<T, U> | Only<U, T>

View File

@@ -0,0 +1,5 @@
export type Only<T, U> = {
[P in keyof T]: T[P]
} & {
[P in keyof U]?: never
}

View File

@@ -0,0 +1,34 @@
import { UuidValidator } from './UuidValidator'
describe('UuidValidator', () => {
const createValidator = () => new UuidValidator()
const validUuids = [
'2221101c-1da9-4d2b-9b32-b8be2a8d1c82',
'c08f2f29-a74b-42b4-aefd-98af9832391c',
'b453fa64-1493-443b-b5bb-bca7b9c696c7',
]
const invalidUuids = [
123,
'someone@127.0.0.1',
'',
null,
'b453fa64-1493-443b-b5bb-ca7b9c696c7',
'c08f*f29-a74b-42b4-aefd-98af9832391c',
'c08f*f29-a74b-42b4-aefd-98af9832391c',
'../../escaped.sh',
]
it('should validate proper uuids', () => {
for (const validUuid of validUuids) {
expect(createValidator().validate(validUuid)).toBeTruthy()
}
})
it('should not validate invalid uuids', () => {
for (const invalidUuid of invalidUuids) {
expect(createValidator().validate(invalidUuid as string)).toBeFalsy()
}
})
})

View File

@@ -0,0 +1,10 @@
import { Uuid } from '../DataType/Uuid'
import { ValidatorInterface } from './ValidatorInterface'
export class UuidValidator implements ValidatorInterface<Uuid> {
private readonly UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i
validate(data: Uuid): boolean {
return String(data).toLowerCase().match(this.UUID_REGEX) !== null
}
}

View File

@@ -0,0 +1,3 @@
export interface ValidatorInterface<T> {
validate(data: T): boolean
}

View File

@@ -18,3 +18,7 @@ export * from './Protocol/ProtocolVersion'
export * from './Role/PaidRoles'
export * from './Role/RoleName'
export * from './Subscription/SubscriptionName'
export * from './Type/Either'
export * from './Type/Only'
export * from './Validator/UuidValidator'
export * from './Validator/ValidatorInterface'

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