Compare commits

..

37 Commits

Author SHA1 Message Date
standardci
3532289575 chore(release): publish new version
- @standardnotes/auth-server@1.93.7
2023-03-10 06:21:38 +00:00
Karol Sójko
7db9ba03f3 fix(auth): change supported algorithms on authenticator registration options 2023-03-10 07:07:48 +01:00
standardci
b0baaf9ea6 chore(release): publish new version
- @standardnotes/analytics@2.21.3
 - @standardnotes/api-gateway@1.49.6
 - @standardnotes/auth-server@1.93.6
 - @standardnotes/event-store@1.7.5
 - @standardnotes/files-server@1.10.7
 - @standardnotes/revisions-server@1.12.9
 - @standardnotes/scheduler-server@1.17.7
 - @standardnotes/syncing-server@1.32.2
 - @standardnotes/websockets-server@1.6.8
2023-03-09 12:43:53 +00:00
Karol Sójko
b7c6dab3ad chore: upgrade node version to latest LTS 2023-03-09 13:30:16 +01:00
standardci
2daa145867 chore(release): publish new version
- @standardnotes/auth-server@1.93.5
2023-03-09 10:02:53 +00:00
Karol Sójko
4bd5fb22b4 fix(auth): remove migrate email settings procedure 2023-03-09 10:48:40 +01:00
standardci
78533a6045 chore(release): publish new version
- @standardnotes/auth-server@1.93.4
2023-03-09 06:25:32 +00:00
Karol Sójko
e1c533a15e fix(auth): change response from verifying authenticator registration 2023-03-09 07:09:43 +01:00
standardci
b6c2bb8023 chore(release): publish new version
- @standardnotes/auth-server@1.93.3
2023-03-09 05:59:55 +00:00
Karol Sójko
c45653a50a fix(auth): remove authenticator names from server 2023-03-09 06:46:35 +01:00
Karol Sójko
d827513b73 fix(auth): migrate encrypted sign in settings 2023-03-09 06:34:50 +01:00
standardci
ad183ca621 chore(release): publish new version
- @standardnotes/auth-server@1.93.2
2023-03-08 13:22:34 +00:00
Karol Sójko
1d11c5a186 fix(auth): authentication options 2023-03-08 14:08:40 +01:00
standardci
e84e78ec55 chore(release): publish new version
- @standardnotes/auth-server@1.93.1
2023-03-08 12:57:57 +00:00
Karol Sójko
f91e4316ff fix(auth): migrate muted email notifications settings 2023-03-08 13:42:45 +01:00
standardci
d54b812881 chore(release): publish new version
- @standardnotes/analytics@2.21.2
 - @standardnotes/api-gateway@1.49.5
 - @standardnotes/auth-server@1.93.0
 - @standardnotes/domain-core@1.12.0
 - @standardnotes/files-server@1.10.6
 - @standardnotes/revisions-server@1.12.8
 - @standardnotes/scheduler-server@1.17.6
 - @standardnotes/settings@1.20.1
 - @standardnotes/syncing-server@1.32.1
 - @standardnotes/websockets-server@1.6.7
2023-03-08 10:09:09 +00:00
Karol Sójko
28dc5ba2a4 fix(auth): setting name value objects in typeorm queries 2023-03-08 10:54:48 +01:00
Karol Sójko
979a320ca6 feat(domain-core): add internal team user role (#473)
* feat(domain-core): add internal team user role

* feat(auth): add internal team user role

* chore: upgrade @standardnotes/features

---------

Co-authored-by: Karol Sójko <karolsojko@protonmail.com>
2023-03-08 10:45:50 +01:00
standardci
c46186b237 chore(release): publish new version
- @standardnotes/auth-server@1.92.0
 - @standardnotes/settings@1.20.0
 - @standardnotes/syncing-server@1.32.0
2023-03-08 09:35:54 +00:00
Karol Sójko
27cf093f85 feat: sign in setting refactor (#472)
* fix(auth): refactor setting names into domain core value objects

* fix(auth): refactor specs with setting name value objects

* feat(auth): move mute sign in emails to a subscription kind of setting

* feat(auth): add migration script to change sign in email settings to subscription settings

* chore: fix setting name usage

* fix(auth): upper casing setting names

---------

Co-authored-by: Karol Sójko <karolsojko@protonmail.com>
2023-03-08 10:22:27 +01:00
standardci
ec0fb7e0b9 chore(release): publish new version
- @standardnotes/auth-server@1.91.2
2023-03-06 14:02:45 +00:00
Karol Sójko
90029456fe fix(auth): associate setting with sign in alerts permission 2023-03-06 14:48:21 +01:00
Karol Sójko
b167b00075 fix(auth): remove sign in emails permission from free accounts 2023-03-06 14:44:50 +01:00
standardci
b13fab76f3 chore(release): publish new version
- @standardnotes/auth-server@1.91.1
2023-03-06 13:28:57 +00:00
Karol Sójko
782a9d310d fix(auth): disable sign in emails on newly created accounts 2023-03-06 14:15:10 +01:00
standardci
537b1f2a29 chore(release): publish new version
- @standardnotes/auth-server@1.91.0
2023-03-06 10:00:13 +00:00
Karol Sójko
2fad6b62cb feat(auth): add cleanup of expired sessions 2023-03-06 10:43:53 +01:00
standardci
bf173b4ede chore(release): publish new version
- @standardnotes/auth-server@1.90.1
2023-03-06 09:17:04 +00:00
Eric Pierce
c52f038c76 fix: Adding support for redis databases with passwords (#468)
Redis databases with passwords can be supported by specifying the environment parameter REDIS_URL=redis://:$REDIS_PASSWORD@redis:6379
Without this change the redis URL will always be hardcoded without support for a password
2023-03-06 10:02:36 +01:00
mousta0x
b12ba98a5c fix: revisions server url (#469) 2023-03-06 09:57:56 +01:00
Karol Sójko
dbccdf342b fix(auth): prevent listing sessions on readonly access 2023-03-06 09:47:54 +01:00
standardci
49b6d029c4 chore(release): publish new version
- @standardnotes/auth-server@1.90.0
2023-03-02 14:10:08 +00:00
Karol Sójko
d6469954ce feat(auth): add configurable list of readonly users (#462) 2023-03-02 14:51:52 +01:00
standardci
5f40550ad4 chore(release): publish new version
- @standardnotes/auth-server@1.89.7
2023-03-02 11:20:59 +00:00
Karol Sójko
79ccbdf100 fix(auth): function naming for more clarity 2023-03-02 12:07:17 +01:00
standardci
1983cfcab2 chore(release): publish new version
- @standardnotes/auth-server@1.89.6
2023-03-02 10:32:02 +00:00
Karol Sójko
753f86707f fix(auth): changing the updated_at property on sessions 2023-03-02 11:16:32 +01:00
137 changed files with 1756 additions and 948 deletions

2
.nvmrc
View File

@@ -1 +1 @@
18.13.0
18.15.0

15
.pnp.cjs generated
View File

@@ -4163,7 +4163,7 @@ const RAW_RUNTIME_STATE =
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/features", "npm:1.58.4"],\
["@standardnotes/features", "npm:1.58.9"],\
["@standardnotes/predicates", "workspace:packages/predicates"],\
["@standardnotes/responses", "npm:1.13.9"],\
["@standardnotes/security", "workspace:packages/security"],\
@@ -4348,10 +4348,10 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@standardnotes/features", [\
["npm:1.58.4", {\
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.58.4-a84962d125-a39afc145a.zip/node_modules/@standardnotes/features/",\
["npm:1.58.8", {\
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.58.8-d97ff2aae1-77bac7d0a0.zip/node_modules/@standardnotes/features/",\
"packageDependencies": [\
["@standardnotes/features", "npm:1.58.4"],\
["@standardnotes/features", "npm:1.58.8"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/security", "workspace:packages/security"],\
@@ -4359,10 +4359,10 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "HARD"\
}],\
["npm:1.58.8", {\
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.58.8-d97ff2aae1-77bac7d0a0.zip/node_modules/@standardnotes/features/",\
["npm:1.58.9", {\
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.58.9-c278f712cd-218350ee55.zip/node_modules/@standardnotes/features/",\
"packageDependencies": [\
["@standardnotes/features", "npm:1.58.8"],\
["@standardnotes/features", "npm:1.58.9"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/security", "workspace:packages/security"],\
@@ -4601,6 +4601,7 @@ const RAW_RUNTIME_STATE =
"packageLocation": "./packages/settings/",\
"packageDependencies": [\
["@standardnotes/settings", "workspace:packages/settings"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.48.2"],\
["eslint-plugin-prettier", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:4.2.1"],\
["reflect-metadata", "npm:0.1.13"],\

View File

@@ -1,4 +1,4 @@
FROM node:18.13.0-alpine
FROM node:18.15.0-alpine
ENV NODE_ENV production

View File

@@ -58,7 +58,9 @@ if [ -z "$REDIS_HOST" ]; then
export REDIS_HOST="cache"
fi
export REDIS_URL="redis://$REDIS_HOST"
if [ -z "$REDIS_URL" ]; then
export REDIS_URL="redis://$REDIS_HOST"
fi
##########
# SHARED #
@@ -349,7 +351,7 @@ export API_GATEWAY_NEW_RELIC_NO_CONFIG_FILE=true
export API_GATEWAY_SYNCING_SERVER_JS_URL=http://localhost:$SYNCING_SERVER_PORT
export API_GATEWAY_AUTH_SERVER_URL=http://localhost:$AUTH_SERVER_PORT
export API_GATEWAY_REVISIONS_SERVER_URL=http://localhost:3005
export API_GATEWAY_REVISIONS_SERVER_URL=http://localhost:$REVISIONS_SERVER_PORT
if [ -z "$PUBLIC_FILES_SERVER_URL" ]; then
export PUBLIC_FILES_SERVER_URL=http://localhost:3125
fi

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.21.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.21.2...@standardnotes/analytics@2.21.3) (2023-03-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.21.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.21.1...@standardnotes/analytics@2.21.2) (2023-03-08)
**Note:** Version bump only for package @standardnotes/analytics
## [2.21.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.21.0...@standardnotes/analytics@2.21.1) (2023-02-23)
### Bug Fixes

View File

@@ -1,4 +1,4 @@
FROM node:18.13.0-alpine
FROM node:18.15.0-alpine
RUN apk add --update \
curl \

View File

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

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.49.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.49.5...@standardnotes/api-gateway@1.49.6) (2023-03-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.49.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.49.4...@standardnotes/api-gateway@1.49.5) (2023-03-08)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.49.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.49.3...@standardnotes/api-gateway@1.49.4) (2023-02-25)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -1,4 +1,4 @@
FROM node:18.13.0-alpine
FROM node:18.15.0-alpine
RUN apk add --update \
curl \

View File

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

View File

@@ -3,6 +3,106 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.93.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.93.6...@standardnotes/auth-server@1.93.7) (2023-03-10)
### Bug Fixes
* **auth:** change supported algorithms on authenticator registration options ([7db9ba0](https://github.com/standardnotes/server/commit/7db9ba03f3c14b83dc4344935499f48db800c87d))
## [1.93.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.93.5...@standardnotes/auth-server@1.93.6) (2023-03-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.93.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.93.4...@standardnotes/auth-server@1.93.5) (2023-03-09)
### Bug Fixes
* **auth:** remove migrate email settings procedure ([4bd5fb2](https://github.com/standardnotes/server/commit/4bd5fb22b447b0e0fdb136aa46ddc812c8b272cd))
## [1.93.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.93.3...@standardnotes/auth-server@1.93.4) (2023-03-09)
### Bug Fixes
* **auth:** change response from verifying authenticator registration ([e1c533a](https://github.com/standardnotes/server/commit/e1c533a15e33e215e90fbe15d2d4994605eaa1bd))
## [1.93.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.93.2...@standardnotes/auth-server@1.93.3) (2023-03-09)
### Bug Fixes
* **auth:** migrate encrypted sign in settings ([d827513](https://github.com/standardnotes/server/commit/d827513b73a57fbdb72c3112f32dc2a296103450))
* **auth:** remove authenticator names from server ([c45653a](https://github.com/standardnotes/server/commit/c45653a50a9d25de1e0fc86127ff6931dc98406d))
## [1.93.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.93.1...@standardnotes/auth-server@1.93.2) (2023-03-08)
### Bug Fixes
* **auth:** authentication options ([1d11c5a](https://github.com/standardnotes/server/commit/1d11c5a1865f81ca57d0ad4313cc3df497b4c445))
## [1.93.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.93.0...@standardnotes/auth-server@1.93.1) (2023-03-08)
### Bug Fixes
* **auth:** migrate muted email notifications settings ([f91e431](https://github.com/standardnotes/server/commit/f91e4316ff4993d032c016bb233b93a9f3356cf3))
# [1.93.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.92.0...@standardnotes/auth-server@1.93.0) (2023-03-08)
### Bug Fixes
* **auth:** setting name value objects in typeorm queries ([28dc5ba](https://github.com/standardnotes/server/commit/28dc5ba2a4e946b7aed86432da160c0be76f839d))
### Features
* **domain-core:** add internal team user role ([#473](https://github.com/standardnotes/server/issues/473)) ([979a320](https://github.com/standardnotes/server/commit/979a320ca666991ad2b023436f58c59ae168c768))
# [1.92.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.91.2...@standardnotes/auth-server@1.92.0) (2023-03-08)
### Features
* sign in setting refactor ([#472](https://github.com/standardnotes/server/issues/472)) ([27cf093](https://github.com/standardnotes/server/commit/27cf093f85d0f2e208f48e7c7ddcce36b341ffb7))
## [1.91.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.91.1...@standardnotes/auth-server@1.91.2) (2023-03-06)
### Bug Fixes
* **auth:** associate setting with sign in alerts permission ([9002945](https://github.com/standardnotes/server/commit/90029456fe6d654747d6b8b7ae106d3d58b3a3fe))
* **auth:** remove sign in emails permission from free accounts ([b167b00](https://github.com/standardnotes/server/commit/b167b0007555b3850ae274354b6c271fe0a1e47f))
## [1.91.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.91.0...@standardnotes/auth-server@1.91.1) (2023-03-06)
### Bug Fixes
* **auth:** disable sign in emails on newly created accounts ([782a9d3](https://github.com/standardnotes/server/commit/782a9d310dc2d2819a49540138ed10b36ebd0d94))
# [1.91.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.90.1...@standardnotes/auth-server@1.91.0) (2023-03-06)
### Features
* **auth:** add cleanup of expired sessions ([2fad6b6](https://github.com/standardnotes/server/commit/2fad6b62cbb5bec38a3171a996d3f9c4eedf7836))
## [1.90.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.90.0...@standardnotes/auth-server@1.90.1) (2023-03-06)
### Bug Fixes
* **auth:** prevent listing sessions on readonly access ([dbccdf3](https://github.com/standardnotes/server/commit/dbccdf342b52f81fb14f246784d5dc6def2ff3fc))
# [1.90.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.89.7...@standardnotes/auth-server@1.90.0) (2023-03-02)
### Features
* **auth:** add configurable list of readonly users ([#462](https://github.com/standardnotes/server/issues/462)) ([d646995](https://github.com/standardnotes/server/commit/d6469954ceb24580c465535e61588b04924734ab))
## [1.89.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.89.6...@standardnotes/auth-server@1.89.7) (2023-03-02)
### Bug Fixes
* **auth:** function naming for more clarity ([79ccbdf](https://github.com/standardnotes/server/commit/79ccbdf1000c699074b5271f3c04a30fcb1b3311))
## [1.89.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.89.5...@standardnotes/auth-server@1.89.6) (2023-03-02)
### Bug Fixes
* **auth:** changing the updated_at property on sessions ([753f867](https://github.com/standardnotes/server/commit/753f86707ffdbab0d04f49b42275dbb28589780b))
## [1.89.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.89.4...@standardnotes/auth-server@1.89.5) (2023-03-01)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -1,4 +1,4 @@
FROM node:18.13.0-alpine
FROM node:18.15.0-alpine
RUN apk add --update \
curl \

View File

@@ -32,36 +32,36 @@ const requestBackups = async (
): Promise<void> => {
let settingName: SettingName,
permissionName: PermissionName,
muteEmailsSettingName: SettingName,
muteEmailsSettingName: string,
muteEmailsSettingValue: string,
providerTokenSettingName: SettingName
switch (backupProvider) {
case 'email':
settingName = SettingName.EmailBackupFrequency
settingName = SettingName.create(SettingName.NAMES.EmailBackupFrequency).getValue()
permissionName = PermissionName.DailyEmailBackup
muteEmailsSettingName = SettingName.MuteFailedBackupsEmails
muteEmailsSettingName = SettingName.NAMES.MuteFailedBackupsEmails
muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted
break
case 'dropbox':
settingName = SettingName.DropboxBackupFrequency
settingName = SettingName.create(SettingName.NAMES.DropboxBackupFrequency).getValue()
permissionName = PermissionName.DailyDropboxBackup
muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails
muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted
providerTokenSettingName = SettingName.DropboxBackupToken
providerTokenSettingName = SettingName.create(SettingName.NAMES.DropboxBackupToken).getValue()
break
case 'one_drive':
settingName = SettingName.OneDriveBackupFrequency
settingName = SettingName.create(SettingName.NAMES.OneDriveBackupFrequency).getValue()
permissionName = PermissionName.DailyOneDriveBackup
muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails
muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted
providerTokenSettingName = SettingName.OneDriveBackupToken
providerTokenSettingName = SettingName.create(SettingName.NAMES.OneDriveBackupToken).getValue()
break
case 'google_drive':
settingName = SettingName.GoogleDriveBackupFrequency
settingName = SettingName.create(SettingName.NAMES.GoogleDriveBackupFrequency).getValue()
permissionName = PermissionName.DailyGDriveBackup
muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails
muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted
providerTokenSettingName = SettingName.GoogleDriveBackupToken
providerTokenSettingName = SettingName.create(SettingName.NAMES.GoogleDriveBackupToken).getValue()
break
default:
throw new Error(`Not handled backup provider: ${backupProvider}`)

View File

@@ -8,6 +8,17 @@ import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { CleanupSessionTraces } from '../src/Domain/UseCase/CleanupSessionTraces/CleanupSessionTraces'
import { CleanupExpiredSessions } from '../src/Domain/UseCase/CleanupExpiredSessions/CleanupExpiredSessions'
const cleanup = async (
cleanupSessionTraces: CleanupSessionTraces,
cleanupExpiredSessions: CleanupExpiredSessions,
): Promise<void> => {
const date = new Date()
await cleanupSessionTraces.execute({ date })
await cleanupExpiredSessions.execute({ date })
}
const container = new ContainerConfigLoader()
void container.load().then((container) => {
@@ -16,22 +27,19 @@ void container.load().then((container) => {
const logger: Logger = container.get(TYPES.Logger)
logger.info('Starting session traces cleanup')
logger.info('Starting sessions and session traces cleanup')
const cleanupSessionTraces: CleanupSessionTraces = container.get(TYPES.CleanupSessionTraces)
const cleanupExpiredSessions: CleanupExpiredSessions = container.get(TYPES.CleanupExpiredSessions)
Promise.resolve(
cleanupSessionTraces.execute({
date: new Date(),
}),
)
Promise.resolve(cleanup(cleanupSessionTraces, cleanupExpiredSessions))
.then(() => {
logger.info('Expired session traces cleaned.')
logger.info('Expired sessions and session traces cleaned.')
process.exit(0)
})
.catch((error) => {
logger.error(`Could not clean session traces: ${error.message}`)
logger.error(`Could not clean sessions and session traces: ${error.message}`)
process.exit(1)
})

View File

@@ -28,7 +28,7 @@ const requestBackups = async (
domainEventPublisher: DomainEventPublisherInterface,
): Promise<void> => {
const permissionName = PermissionName.DailyEmailBackup
const muteEmailsSettingName = SettingName.MuteFailedBackupsEmails
const muteEmailsSettingName = SettingName.NAMES.MuteFailedBackupsEmails
const muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted
if (!backupEmail) {

View File

@@ -34,7 +34,7 @@ export class moveMfaItemsToUserSettings1627638504691 implements MigrationInterfa
const setting = new Setting()
setting.uuid = item['uuid']
setting.name = SettingName.MfaSecret
setting.name = SettingName.NAMES.MfaSecret
setting.value = item['content']
if (item['deleted']) {
setting.value = null

View File

@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class removeSignInEmailsOnFreeAcounts1678110075698 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'DELETE FROM `role_permissions` WHERE role_uuid="23bf88ca-bee1-4a4c-adf0-b7a48749eea7" AND permission_uuid="2074d312-78bc-4533-b008-38e1232226c0"',
)
await queryRunner.query(
'DELETE FROM `role_permissions` WHERE role_uuid="bde42e26-628c-44e6-9d76-21b08954b0bf" AND permission_uuid="2074d312-78bc-4533-b008-38e1232226c0"',
)
}
public async down(): Promise<void> {
return
}
}

View File

@@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addInternalTeamUserRole1678266947362 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// remove beta files user role and permission
await queryRunner.query('DELETE FROM `role_permissions` WHERE role_uuid="1cd9ee6e-bc95-4f32-957c-d8c41f94d4ef"')
await queryRunner.query('DELETE FROM `user_roles` WHERE role_uuid="1cd9ee6e-bc95-4f32-957c-d8c41f94d4ef"')
await queryRunner.query('DELETE FROM `roles` WHERE name="FILES_BETA_USER"')
await queryRunner.query('DELETE FROM `permissions` WHERE name="app:files-beta"')
// add internal team user role and permission
await queryRunner.query(
'INSERT INTO `roles` (uuid, name, version) VALUES ("9f8d2313-e8d0-48ad-b19c-026601d0ddf4", "INTERNAL_TEAM_USER", 1)',
)
await queryRunner.query(
'INSERT INTO `permissions` (uuid, name) VALUES ("fb13e7d3-936f-4ded-a543-e1650cc99dfd", "server:universal-second-factor")',
)
await queryRunner.query(
'INSERT INTO `role_permissions` (role_uuid, permission_uuid) VALUES ("9f8d2313-e8d0-48ad-b19c-026601d0ddf4", "fb13e7d3-936f-4ded-a543-e1650cc99dfd")',
)
}
public async down(): Promise<void> {
return
}
}

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class removeAuthenticatorNamesFromServer1678340701766 implements MigrationInterface {
name = 'removeAuthenticatorNamesFromServer1678340701766'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `authenticators` DROP COLUMN `name`')
}
public async down(): Promise<void> {
return
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.89.5",
"version": "1.93.7",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -45,7 +45,7 @@
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",
"@standardnotes/features": "^1.58.4",
"@standardnotes/features": "^1.58.9",
"@standardnotes/predicates": "workspace:*",
"@standardnotes/responses": "^1.13.9",
"@standardnotes/security": "workspace:*",

View File

@@ -168,7 +168,6 @@ import { ListSharedSubscriptionInvitations } from '../Domain/UseCase/ListSharedS
import { UserSubscriptionServiceInterface } from '../Domain/Subscription/UserSubscriptionServiceInterface'
import { UserSubscriptionService } from '../Domain/Subscription/UserSubscriptionService'
import { SubscriptionSettingProjector } from '../Projection/SubscriptionSettingProjector'
import { GetSubscriptionSetting } from '../Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting'
import { SubscriptionSettingsAssociationService } from '../Domain/Setting/SubscriptionSettingsAssociationService'
import { SubscriptionSettingsAssociationServiceInterface } from '../Domain/Setting/SubscriptionSettingsAssociationServiceInterface'
import { PKCERepositoryInterface } from '../Domain/User/PKCERepositoryInterface'
@@ -216,6 +215,7 @@ import { DeleteAuthenticator } from '../Domain/UseCase/DeleteAuthenticator/Delet
import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes'
import { SignInWithRecoveryCodes } from '../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
import { GetUserKeyParamsRecovery } from '../Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery'
import { CleanupExpiredSessions } from '../Domain/UseCase/CleanupExpiredSessions/CleanupExpiredSessions'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -463,6 +463,10 @@ export class ContainerConfigLoader {
container
.bind(TYPES.U2F_REQUIRE_USER_VERIFICATION)
.toConstantValue(env.get('U2F_REQUIRE_USER_VERIFICATION', true) === 'true')
container
.bind(TYPES.READONLY_USERS)
.toConstantValue(env.get('READONLY_USERS', true) ? env.get('READONLY_USERS', true).split(',') : [])
// Services
container.bind<UAParser>(TYPES.DeviceDetector).toConstantValue(new UAParser())
container.bind<SessionService>(TYPES.SessionService).to(SessionService)
@@ -612,6 +616,9 @@ export class ContainerConfigLoader {
container
.bind<CleanupSessionTraces>(TYPES.CleanupSessionTraces)
.toConstantValue(new CleanupSessionTraces(container.get(TYPES.SessionTraceRepository)))
container
.bind<CleanupExpiredSessions>(TYPES.CleanupExpiredSessions)
.toConstantValue(new CleanupExpiredSessions(container.get(TYPES.SessionRepository)))
container.bind<AuthenticateUser>(TYPES.AuthenticateUser).to(AuthenticateUser)
container.bind<AuthenticateRequest>(TYPES.AuthenticateRequest).to(AuthenticateRequest)
container.bind<RefreshSessionToken>(TYPES.RefreshSessionToken).to(RefreshSessionToken)
@@ -683,7 +690,6 @@ export class ContainerConfigLoader {
container
.bind<ListSharedSubscriptionInvitations>(TYPES.ListSharedSubscriptionInvitations)
.to(ListSharedSubscriptionInvitations)
container.bind<GetSubscriptionSetting>(TYPES.GetSubscriptionSetting).to(GetSubscriptionSetting)
container.bind<VerifyPredicate>(TYPES.VerifyPredicate).to(VerifyPredicate)
container.bind<CreateCrossServiceToken>(TYPES.CreateCrossServiceToken).to(CreateCrossServiceToken)
container.bind<ProcessUserRequest>(TYPES.ProcessUserRequest).to(ProcessUserRequest)

View File

@@ -97,6 +97,7 @@ const TYPES = {
U2F_RELYING_PARTY_NAME: Symbol.for('U2F_RELYING_PARTY_NAME'),
U2F_EXPECTED_ORIGIN: Symbol.for('U2F_EXPECTED_ORIGIN'),
U2F_REQUIRE_USER_VERIFICATION: Symbol.for('U2F_REQUIRE_USER_VERIFICATION'),
READONLY_USERS: Symbol.for('READONLY_USERS'),
// use cases
AuthenticateUser: Symbol.for('AuthenticateUser'),
AuthenticateRequest: Symbol.for('AuthenticateRequest'),
@@ -131,12 +132,12 @@ const TYPES = {
DeclineSharedSubscriptionInvitation: Symbol.for('DeclineSharedSubscriptionInvitation'),
CancelSharedSubscriptionInvitation: Symbol.for('CancelSharedSubscriptionInvitation'),
ListSharedSubscriptionInvitations: Symbol.for('ListSharedSubscriptionInvitations'),
GetSubscriptionSetting: Symbol.for('GetSubscriptionSetting'),
VerifyPredicate: Symbol.for('VerifyPredicate'),
CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'),
ProcessUserRequest: Symbol.for('ProcessUserRequest'),
TraceSession: Symbol.for('TraceSession'),
CleanupSessionTraces: Symbol.for('CleanupSessionTraces'),
CleanupExpiredSessions: Symbol.for('CleanupExpiredSessions'),
PersistStatistics: Symbol.for('PersistStatistics'),
GenerateAuthenticatorRegistrationOptions: Symbol.for('GenerateAuthenticatorRegistrationOptions'),
VerifyAuthenticatorRegistrationResponse: Symbol.for('VerifyAuthenticatorRegistrationResponse'),

View File

@@ -69,7 +69,7 @@ export class AdminController extends BaseHttpController {
const result = await this.doDeleteSetting.execute({
uuid,
userUuid,
settingName: SettingName.MfaSecret,
settingName: SettingName.NAMES.MfaSecret,
timestamp: updatedAt,
softDelete: true,
})
@@ -115,7 +115,7 @@ export class AdminController extends BaseHttpController {
const result = await this.doDeleteSetting.execute({
userUuid,
settingName: SettingName.EmailBackupFrequency,
settingName: SettingName.NAMES.EmailBackupFrequency,
})
if (result.success) {

View File

@@ -88,7 +88,6 @@ export class AuthenticatorsController {
): Promise<HttpResponse<VerifyAuthenticatorRegistrationResponseResponseBody>> {
const result = await this.verifyAuthenticatorRegistrationResponse.execute({
userUuid: params.userUuid,
name: params.name,
attestationResponse: params.attestationResponse,
})
@@ -105,7 +104,7 @@ export class AuthenticatorsController {
return {
status: HttpStatusCode.Success,
data: { success: result.getValue() },
data: { id: result.getValue().toString() },
}
}

View File

@@ -58,6 +58,10 @@ export class SessionsController extends BaseHttpController {
@httpGet('/', TYPES.AuthMiddleware, TYPES.SessionMiddleware)
async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
if (response.locals.readOnlyAccess) {
return this.json([])
}
const useCaseResponse = await this.getActiveSessionsForUser.execute({
userUuid: response.locals.user.uuid,
})

View File

@@ -90,7 +90,7 @@ describe('SettingsController', () => {
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
const result = await httpResponse.executeAsync()
expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'test' })
expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'TEST' })
expect(result.statusCode).toEqual(200)
})
@@ -124,7 +124,7 @@ describe('SettingsController', () => {
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
const result = await httpResponse.executeAsync()
expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'test' })
expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'TEST' })
expect(result.statusCode).toEqual(400)
})

View File

@@ -61,7 +61,7 @@ export class SettingsController extends BaseHttpController {
}
const { userUuid, settingName } = request.params
const result = await this.doGetSetting.execute({ userUuid, settingName })
const result = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
if (result.success) {
return this.json(result)

View File

@@ -4,24 +4,24 @@ import * as express from 'express'
import { results } from 'inversify-express-utils'
import { User } from '../Domain/User/User'
import { GetSubscriptionSetting } from '../Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting'
import { SubscriptionSettingsController } from './SubscriptionSettingsController'
import { GetSetting } from '../Domain/UseCase/GetSetting/GetSetting'
describe('SubscriptionSettingsController', () => {
let getSubscriptionSetting: GetSubscriptionSetting
let getSetting: GetSetting
let request: express.Request
let response: express.Response
let user: User
const createController = () => new SubscriptionSettingsController(getSubscriptionSetting)
const createController = () => new SubscriptionSettingsController(getSetting)
beforeEach(() => {
user = {} as jest.Mocked<User>
user.uuid = '123'
getSubscriptionSetting = {} as jest.Mocked<GetSubscriptionSetting>
getSubscriptionSetting.execute = jest.fn()
getSetting = {} as jest.Mocked<GetSetting>
getSetting.execute = jest.fn()
request = {
headers: {},
@@ -41,12 +41,12 @@ describe('SubscriptionSettingsController', () => {
uuid: '1-2-3',
}
getSubscriptionSetting.execute = jest.fn().mockReturnValue({ success: true })
getSetting.execute = jest.fn().mockReturnValue({ success: true })
const httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
const result = await httpResponse.executeAsync()
expect(getSubscriptionSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', subscriptionSettingName: 'test' })
expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'TEST' })
expect(result.statusCode).toEqual(200)
})
@@ -58,12 +58,12 @@ describe('SubscriptionSettingsController', () => {
uuid: '1-2-3',
}
getSubscriptionSetting.execute = jest.fn().mockReturnValue({ success: false })
getSetting.execute = jest.fn().mockReturnValue({ success: false })
const httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
const result = await httpResponse.executeAsync()
expect(getSubscriptionSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', subscriptionSettingName: 'test' })
expect(getSetting.execute).toHaveBeenCalledWith({ userUuid: '1-2-3', settingName: 'TEST' })
expect(result.statusCode).toEqual(400)
})

View File

@@ -1,4 +1,3 @@
import { SubscriptionSettingName } from '@standardnotes/settings'
import { Request, Response } from 'express'
import { inject } from 'inversify'
import {
@@ -9,19 +8,19 @@ import {
results,
} from 'inversify-express-utils'
import TYPES from '../Bootstrap/Types'
import { GetSubscriptionSetting } from '../Domain/UseCase/GetSubscriptionSetting/GetSubscriptionSetting'
import { GetSetting } from '../Domain/UseCase/GetSetting/GetSetting'
@controller('/users/:userUuid')
export class SubscriptionSettingsController extends BaseHttpController {
constructor(@inject(TYPES.GetSubscriptionSetting) private doGetSubscriptionSetting: GetSubscriptionSetting) {
constructor(@inject(TYPES.GetSetting) private doGetSetting: GetSetting) {
super()
}
@httpGet('/subscription-settings/:subscriptionSettingName', TYPES.ApiGatewayAuthMiddleware)
async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.doGetSubscriptionSetting.execute({
const result = await this.doGetSetting.execute({
userUuid: response.locals.user.uuid,
subscriptionSettingName: request.params.subscriptionSettingName as SubscriptionSettingName,
settingName: request.params.subscriptionSettingName.toUpperCase(),
})
if (result.success) {

View File

@@ -77,7 +77,7 @@ export class SubscriptionTokensController extends BaseHttpController {
const user = authenticateTokenResponse.user as User
let extensionKey = undefined
const extensionKeySetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.ExtensionKey,
settingName: SettingName.create(SettingName.NAMES.ExtensionKey).getValue(),
userUuid: user.uuid,
})
if (extensionKeySetting !== null) {

View File

@@ -6,7 +6,6 @@ describe('Authenticator', () => {
it('should create an entity', () => {
const entityOrError = Authenticator.create({
counter: 1,
name: 'my-key',
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialId: Buffer.from('credentialId'),

View File

@@ -1,7 +1,6 @@
import { Dates, Uuid } from '@standardnotes/domain-core'
export interface AuthenticatorProps {
name: string
userUuid: Uuid
credentialId: Uint8Array
credentialPublicKey: Uint8Array

View File

@@ -343,7 +343,7 @@ describe('FeatureService', () => {
])
const nonSubscriptionRole = {
name: RoleName.NAMES.FilesBetaUser,
name: RoleName.NAMES.InternalTeamUser,
uuid: 'role-files-beta',
permissions: Promise.resolve([nonSubscriptionPermission]),
} as jest.Mocked<Role>

View File

@@ -27,13 +27,13 @@ export class EmailSubscriptionUnsubscribedEventHandler implements DomainEventHan
private getSettingNameFromLevel(level: string): string {
switch (level) {
case EmailLevel.LEVELS.FailedCloudBackup:
return SettingName.MuteFailedCloudBackupsEmails
return SettingName.NAMES.MuteFailedCloudBackupsEmails
case EmailLevel.LEVELS.FailedEmailBackup:
return SettingName.MuteFailedBackupsEmails
return SettingName.NAMES.MuteFailedBackupsEmails
case EmailLevel.LEVELS.Marketing:
return SettingName.MuteMarketingEmails
return SettingName.NAMES.MuteMarketingEmails
case EmailLevel.LEVELS.SignIn:
return SettingName.MuteSignInEmails
return SettingName.NAMES.MuteSignInEmails
default:
throw new Error(`Unknown level: ${level}`)
}

View File

@@ -54,7 +54,7 @@ export class ExtensionKeyGrantedEventHandler implements DomainEventHandlerInterf
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.ExtensionKey,
name: SettingName.NAMES.ExtensionKey,
unencryptedValue: event.payload.extensionKey,
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: true,

View File

@@ -1,5 +1,5 @@
import { DomainEventHandlerInterface, FileRemovedEvent } from '@standardnotes/domain-events'
import { SubscriptionSettingName } from '@standardnotes/settings'
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
@@ -38,7 +38,7 @@ export class FileRemovedEventHandler implements DomainEventHandlerInterface {
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: user.uuid,
userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesUsed,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
})
if (bytesUsedSetting === null) {
this.logger.warn(`Could not find bytes used setting for user with uuid: ${user.uuid}`)
@@ -51,7 +51,7 @@ export class FileRemovedEventHandler implements DomainEventHandlerInterface {
await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription,
props: {
name: SubscriptionSettingName.FileUploadBytesUsed,
name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesUsed - byteSize).toString(),
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,

View File

@@ -1,5 +1,5 @@
import { DomainEventHandlerInterface, FileUploadedEvent } from '@standardnotes/domain-events'
import { SubscriptionSettingName } from '@standardnotes/settings'
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
@@ -47,7 +47,7 @@ export class FileUploadedEventHandler implements DomainEventHandlerInterface {
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: (await subscription.user).uuid,
userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesUsed,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
})
if (bytesUsedSetting !== null) {
bytesUsed = bytesUsedSetting.value as string
@@ -56,7 +56,7 @@ export class FileUploadedEventHandler implements DomainEventHandlerInterface {
await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription,
props: {
name: SubscriptionSettingName.FileUploadBytesUsed,
name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesUsed + byteSize).toString(),
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,

View File

@@ -28,7 +28,7 @@ export class ListedAccountCreatedEventHandler implements DomainEventHandlerInter
let authSecrets: ListedAuthorSecretsData = [newSecret]
const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.ListedAuthorSecrets,
settingName: SettingName.create(SettingName.NAMES.ListedAuthorSecrets).getValue(),
userUuid: user.uuid,
})
if (listedAuthorSecretsSetting !== null) {
@@ -40,7 +40,7 @@ export class ListedAccountCreatedEventHandler implements DomainEventHandlerInter
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.ListedAuthorSecrets,
name: SettingName.NAMES.ListedAuthorSecrets,
unencryptedValue: JSON.stringify(authSecrets),
sensitive: false,
},

View File

@@ -24,7 +24,7 @@ export class ListedAccountDeletedEventHandler implements DomainEventHandlerInter
}
const listedAuthorSecretsSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.ListedAuthorSecrets,
settingName: SettingName.create(SettingName.NAMES.ListedAuthorSecrets).getValue(),
userUuid: user.uuid,
})
if (listedAuthorSecretsSetting === null) {
@@ -43,7 +43,7 @@ export class ListedAccountDeletedEventHandler implements DomainEventHandlerInter
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.ListedAuthorSecrets,
name: SettingName.NAMES.ListedAuthorSecrets,
unencryptedValue: JSON.stringify(filteredSecrets),
sensitive: false,
},

View File

@@ -47,7 +47,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.ExtensionKey,
name: SettingName.NAMES.ExtensionKey,
unencryptedValue: event.payload.extensionKey,
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: true,

View File

@@ -95,7 +95,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.ExtensionKey,
name: SettingName.NAMES.ExtensionKey,
unencryptedValue: event.payload.extensionKey,
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: true,

View File

@@ -27,7 +27,7 @@ describe('RoleToSubscriptionMap', () => {
name: RoleName.NAMES.CoreUser,
} as jest.Mocked<Role>,
{
name: RoleName.NAMES.FilesBetaUser,
name: RoleName.NAMES.InternalTeamUser,
} as jest.Mocked<Role>,
{
name: RoleName.NAMES.PlusUser,
@@ -38,7 +38,7 @@ describe('RoleToSubscriptionMap', () => {
name: RoleName.NAMES.CoreUser,
},
{
name: RoleName.NAMES.FilesBetaUser,
name: RoleName.NAMES.InternalTeamUser,
},
])
})
@@ -49,7 +49,7 @@ describe('RoleToSubscriptionMap', () => {
name: RoleName.NAMES.CoreUser,
} as jest.Mocked<Role>,
{
name: RoleName.NAMES.FilesBetaUser,
name: RoleName.NAMES.InternalTeamUser,
} as jest.Mocked<Role>,
{
name: RoleName.NAMES.PlusUser,

View File

@@ -12,7 +12,7 @@ export class RoleToSubscriptionMap implements RoleToSubscriptionMapInterface {
[RoleName.NAMES.ProUser, SubscriptionName.ProPlan],
])
private readonly nonSubscriptionRoles = [RoleName.NAMES.CoreUser, RoleName.NAMES.FilesBetaUser]
private readonly nonSubscriptionRoles = [RoleName.NAMES.CoreUser, RoleName.NAMES.InternalTeamUser]
filterNonSubscriptionRoles(roles: Role[]): Array<Role> {
return roles.filter((role) => this.nonSubscriptionRoles.includes(role.name))

View File

@@ -12,4 +12,5 @@ export interface SessionRepositoryInterface {
save(session: Session): Promise<Session>
remove(session: Session): Promise<Session>
clearUserAgentByUserUuid(userUuid: string): Promise<void>
removeExpiredBefore(date: Date): Promise<void>
}

View File

@@ -34,6 +34,7 @@ describe('SessionService', () => {
let cryptoNode: CryptoNode
let traceSession: TraceSession
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
const readonlyUsers = ['demo@standardnotes.com']
const createService = () =>
new SessionService(
@@ -49,6 +50,7 @@ describe('SessionService', () => {
cryptoNode,
traceSession,
userSubscriptionRepository,
readonlyUsers,
)
beforeEach(() => {
@@ -59,6 +61,7 @@ describe('SessionService', () => {
session.apiVersion = ApiVersion.v20200115
session.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
session.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
session.readonlyAccess = false
revokedSession = {} as jest.Mocked<RevokedSession>
revokedSession.uuid = '2e1e43'
@@ -182,6 +185,42 @@ describe('SessionService', () => {
})
})
it('should create new readonly session for a user that is readonly restricted', async () => {
const user = {} as jest.Mocked<User>
user.email = 'demo@standardnotes.com'
user.uuid = '123'
const sessionPayload = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
readonlyAccess: false,
})
expect(sessionRepository.save).toHaveBeenCalledWith(expect.any(Session))
expect(sessionRepository.save).toHaveBeenCalledWith({
accessExpiration: expect.any(Date),
apiVersion: '003',
createdAt: expect.any(Date),
hashedAccessToken: expect.any(String),
hashedRefreshToken: expect.any(String),
refreshExpiration: expect.any(Date),
updatedAt: expect.any(Date),
userAgent: 'Google Chrome',
userUuid: '123',
uuid: expect.any(String),
readonlyAccess: true,
})
expect(sessionPayload).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
refresh_token: expect.any(String),
readonly_access: true,
})
})
it('should create new session for a user with disabled user agent logging', async () => {
const user = {} as jest.Mocked<User>
user.uuid = '123'
@@ -409,9 +448,9 @@ describe('SessionService', () => {
})
it('should determine if a refresh token is valid', async () => {
expect(createService().isRefreshTokenValid(session, '1:2:3')).toBeTruthy()
expect(createService().isRefreshTokenValid(session, '1:2:4')).toBeFalsy()
expect(createService().isRefreshTokenValid(session, '1:2')).toBeFalsy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2:3')).toBeTruthy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2:4')).toBeFalsy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2')).toBeFalsy()
})
it('should return device info based on user agent', () => {

View File

@@ -39,6 +39,7 @@ export class SessionService implements SessionServiceInterface {
@inject(TYPES.CryptoNode) private cryptoNode: CryptoNode,
@inject(TYPES.TraceSession) private traceSession: TraceSession,
@inject(TYPES.UserSubscriptionRepository) private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
@inject(TYPES.READONLY_USERS) private readonlyUsers: string[],
) {}
async createNewSessionForUser(dto: {
@@ -113,7 +114,7 @@ export class SessionService implements SessionServiceInterface {
return sessionPayload
}
isRefreshTokenValid(session: Session, token: string): boolean {
isRefreshTokenMatchingHashedSessionToken(session: Session, token: string): boolean {
const tokenParts = token.split(':')
const refreshToken = tokenParts[2]
if (!refreshToken) {
@@ -268,7 +269,9 @@ export class SessionService implements SessionServiceInterface {
session.apiVersion = dto.apiVersion
session.createdAt = this.timer.getUTCDate()
session.updatedAt = this.timer.getUTCDate()
session.readonlyAccess = dto.readonlyAccess
const userIsReadonly = this.readonlyUsers.includes(dto.user.email)
session.readonlyAccess = userIsReadonly || dto.readonlyAccess
return session
}
@@ -302,13 +305,13 @@ export class SessionService implements SessionServiceInterface {
refresh_token: `${SessionService.SESSION_TOKEN_VERSION}:${session.uuid}:${refreshToken}`,
access_expiration: this.timer.convertStringDateToMilliseconds(accessTokenExpiration.toString()),
refresh_expiration: this.timer.convertStringDateToMilliseconds(refreshTokenExpiration.toString()),
readonly_access: false,
readonly_access: session.readonlyAccess,
}
}
private async isLoggingUserAgentEnabledOnSessions(user: User): Promise<boolean> {
const loggingSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.LogSessionUserAgent,
settingName: SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue(),
userUuid: user.uuid,
})

View File

@@ -21,7 +21,7 @@ export interface SessionServiceInterface {
getRevokedSessionFromToken(token: string): Promise<RevokedSession | null>
markRevokedSessionAsReceived(revokedSession: RevokedSession): Promise<RevokedSession>
deleteSessionByToken(token: string): Promise<string | null>
isRefreshTokenValid(session: Session, token: string): boolean
isRefreshTokenMatchingHashedSessionToken(session: Session, token: string): boolean
getDeviceInfo(session: Session): string
getOperatingSystemInfoFromUserAgent(userAgent: string): string
getBrowserInfoFromUserAgent(userAgent: string): string

View File

@@ -1,8 +1,8 @@
import { SubscriptionSettingName } from '@standardnotes/settings'
import { SettingName } from '@standardnotes/settings'
export type FindSubscriptionSettingDTO = {
userUuid: string
userSubscriptionUuid: string
subscriptionSettingName: SubscriptionSettingName
subscriptionSettingName: SettingName
settingUuid?: string
}

View File

@@ -70,12 +70,11 @@ describe('SettingInterpreter', () => {
})
it('should trigger session cleanup if user is disabling session user agent logging', async () => {
const setting = {
name: SettingName.LogSessionUserAgent,
value: LogSessionUserAgentOption.Disabled,
} as jest.Mocked<Setting>
await createInterpreter().interpretSettingUpdated(setting, user, LogSessionUserAgentOption.Disabled)
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.LogSessionUserAgent,
user,
LogSessionUserAgentOption.Disabled,
)
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent).toHaveBeenCalledWith({
@@ -85,55 +84,50 @@ describe('SettingInterpreter', () => {
})
it('should trigger backup if email backup setting is created - emails not muted', async () => {
const setting = {
name: SettingName.EmailBackupFrequency,
value: EmailBackupFrequency.Daily,
} as jest.Mocked<Setting>
await createInterpreter().interpretSettingUpdated(setting, user, EmailBackupFrequency.Daily)
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.EmailBackupFrequency,
user,
EmailBackupFrequency.Daily,
)
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith('4-5-6', '', false)
})
it('should trigger backup if email backup setting is created - emails muted', async () => {
const setting = {
name: SettingName.EmailBackupFrequency,
value: EmailBackupFrequency.Daily,
} as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({
name: SettingName.MuteFailedBackupsEmails,
name: SettingName.NAMES.MuteFailedBackupsEmails,
uuid: '6-7-8',
value: 'muted',
} as jest.Mocked<Setting>)
await createInterpreter().interpretSettingUpdated(setting, user, EmailBackupFrequency.Daily)
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.EmailBackupFrequency,
user,
EmailBackupFrequency.Daily,
)
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailBackupRequestedEvent).toHaveBeenCalledWith('4-5-6', '6-7-8', true)
})
it('should not trigger backup if email backup setting is disabled', async () => {
const setting = {
name: SettingName.EmailBackupFrequency,
value: EmailBackupFrequency.Disabled,
} as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
await createInterpreter().interpretSettingUpdated(setting, user, EmailBackupFrequency.Disabled)
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.EmailBackupFrequency,
user,
EmailBackupFrequency.Disabled,
)
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createEmailBackupRequestedEvent).not.toHaveBeenCalled()
})
it('should trigger cloud backup if dropbox backup setting is created', async () => {
const setting = {
name: SettingName.DropboxBackupToken,
value: 'test-token',
} as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
await createInterpreter().interpretSettingUpdated(setting, user, 'test-token')
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.DropboxBackupToken, user, 'test-token')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith(
@@ -146,17 +140,13 @@ describe('SettingInterpreter', () => {
})
it('should trigger cloud backup if dropbox backup setting is created - muted emails', async () => {
const setting = {
name: SettingName.DropboxBackupToken,
value: 'test-token',
} as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue({
name: SettingName.MuteFailedCloudBackupsEmails,
name: SettingName.NAMES.MuteFailedCloudBackupsEmails,
uuid: '6-7-8',
value: 'muted',
} as jest.Mocked<Setting>)
await createInterpreter().interpretSettingUpdated(setting, user, 'test-token')
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.DropboxBackupToken, user, 'test-token')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith(
@@ -169,13 +159,9 @@ describe('SettingInterpreter', () => {
})
it('should trigger cloud backup if google drive backup setting is created', async () => {
const setting = {
name: SettingName.GoogleDriveBackupToken,
value: 'test-token',
} as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
await createInterpreter().interpretSettingUpdated(setting, user, 'test-token')
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.GoogleDriveBackupToken, user, 'test-token')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith(
@@ -188,13 +174,9 @@ describe('SettingInterpreter', () => {
})
it('should trigger cloud backup if one drive backup setting is created', async () => {
const setting = {
name: SettingName.OneDriveBackupToken,
value: 'test-token',
} as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
await createInterpreter().interpretSettingUpdated(setting, user, 'test-token')
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.OneDriveBackupToken, user, 'test-token')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith(
@@ -207,13 +189,13 @@ describe('SettingInterpreter', () => {
})
it('should trigger mute subscription emails rejection if mute setting changed', async () => {
const setting = {
name: SettingName.MuteMarketingEmails,
value: MuteMarketingEmailsOption.Muted,
} as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
await createInterpreter().interpretSettingUpdated(setting, user, MuteMarketingEmailsOption.Muted)
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.MuteMarketingEmails,
user,
MuteMarketingEmailsOption.Muted,
)
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createMuteEmailsSettingChangedEvent).toHaveBeenCalledWith({
@@ -225,19 +207,13 @@ describe('SettingInterpreter', () => {
it('should trigger cloud backup if backup frequency setting is updated and a backup token setting is present', async () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce({
name: SettingName.OneDriveBackupToken,
name: SettingName.NAMES.OneDriveBackupToken,
serverEncryptionVersion: 1,
value: 'encrypted-backup-token',
sensitive: true,
} as jest.Mocked<Setting>)
const setting = {
name: SettingName.OneDriveBackupFrequency,
serverEncryptionVersion: 0,
value: 'daily',
sensitive: false,
} as jest.Mocked<Setting>
await createInterpreter().interpretSettingUpdated(setting, user, 'daily')
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.OneDriveBackupFrequency, user, 'daily')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).toHaveBeenCalledWith(
@@ -251,19 +227,17 @@ describe('SettingInterpreter', () => {
it('should not trigger cloud backup if backup frequency setting is updated as disabled', async () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce({
name: SettingName.OneDriveBackupToken,
name: SettingName.NAMES.OneDriveBackupToken,
serverEncryptionVersion: 1,
value: 'encrypted-backup-token',
sensitive: true,
} as jest.Mocked<Setting>)
const setting = {
name: SettingName.OneDriveBackupFrequency,
serverEncryptionVersion: 0,
value: OneDriveBackupFrequency.Disabled,
sensitive: false,
} as jest.Mocked<Setting>
await createInterpreter().interpretSettingUpdated(setting, user, OneDriveBackupFrequency.Disabled)
await createInterpreter().interpretSettingUpdated(
SettingName.NAMES.OneDriveBackupFrequency,
user,
OneDriveBackupFrequency.Disabled,
)
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).not.toHaveBeenCalled()
@@ -271,14 +245,8 @@ describe('SettingInterpreter', () => {
it('should not trigger cloud backup if backup frequency setting is updated and a backup token setting is not present', async () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce(null)
const setting = {
name: SettingName.OneDriveBackupFrequency,
serverEncryptionVersion: 0,
value: 'daily',
sensitive: false,
} as jest.Mocked<Setting>
await createInterpreter().interpretSettingUpdated(setting, user, 'daily')
await createInterpreter().interpretSettingUpdated(SettingName.NAMES.OneDriveBackupFrequency, user, 'daily')
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createCloudBackupRequestedEvent).not.toHaveBeenCalled()

View File

@@ -15,7 +15,6 @@ import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { User } from '../User/User'
import { Setting } from './Setting'
import { SettingDecrypterInterface } from './SettingDecrypterInterface'
import { SettingInterpreterInterface } from './SettingInterpreterInterface'
import { SettingRepositoryInterface } from './SettingRepositoryInterface'
@@ -23,15 +22,15 @@ import { SettingRepositoryInterface } from './SettingRepositoryInterface'
@injectable()
export class SettingInterpreter implements SettingInterpreterInterface {
private readonly cloudBackupTokenSettings = [
SettingName.DropboxBackupToken,
SettingName.GoogleDriveBackupToken,
SettingName.OneDriveBackupToken,
SettingName.NAMES.DropboxBackupToken,
SettingName.NAMES.GoogleDriveBackupToken,
SettingName.NAMES.OneDriveBackupToken,
]
private readonly cloudBackupFrequencySettings = [
SettingName.DropboxBackupFrequency,
SettingName.GoogleDriveBackupFrequency,
SettingName.OneDriveBackupFrequency,
SettingName.NAMES.DropboxBackupFrequency,
SettingName.NAMES.GoogleDriveBackupFrequency,
SettingName.NAMES.OneDriveBackupFrequency,
]
private readonly cloudBackupFrequencyDisabledValues = [
@@ -40,11 +39,11 @@ export class SettingInterpreter implements SettingInterpreterInterface {
OneDriveBackupFrequency.Disabled,
]
private readonly emailSettingToSubscriptionRejectionLevelMap: Map<SettingName, string> = new Map([
[SettingName.MuteFailedBackupsEmails, EmailLevel.LEVELS.FailedEmailBackup],
[SettingName.MuteFailedCloudBackupsEmails, EmailLevel.LEVELS.FailedCloudBackup],
[SettingName.MuteMarketingEmails, EmailLevel.LEVELS.Marketing],
[SettingName.MuteSignInEmails, EmailLevel.LEVELS.SignIn],
private readonly emailSettingToSubscriptionRejectionLevelMap: Map<string, string> = new Map([
[SettingName.NAMES.MuteFailedBackupsEmails, EmailLevel.LEVELS.FailedEmailBackup],
[SettingName.NAMES.MuteFailedCloudBackupsEmails, EmailLevel.LEVELS.FailedCloudBackup],
[SettingName.NAMES.MuteMarketingEmails, EmailLevel.LEVELS.Marketing],
[SettingName.NAMES.MuteSignInEmails, EmailLevel.LEVELS.SignIn],
])
constructor(
@@ -55,20 +54,24 @@ export class SettingInterpreter implements SettingInterpreterInterface {
@inject(TYPES.Logger) private logger: Logger,
) {}
async interpretSettingUpdated(updatedSetting: Setting, user: User, unencryptedValue: string | null): Promise<void> {
if (this.isChangingMuteEmailsSetting(updatedSetting)) {
await this.triggerEmailSubscriptionChange(user, updatedSetting.name as SettingName, unencryptedValue)
async interpretSettingUpdated(
updatedSettingName: string,
user: User,
unencryptedValue: string | null,
): Promise<void> {
if (this.isChangingMuteEmailsSetting(updatedSettingName)) {
await this.triggerEmailSubscriptionChange(user, updatedSettingName, unencryptedValue)
}
if (this.isEnablingEmailBackupSetting(updatedSetting)) {
if (this.isEnablingEmailBackupSetting(updatedSettingName, unencryptedValue)) {
await this.triggerEmailBackup(user.uuid)
}
if (this.isEnablingCloudBackupSetting(updatedSetting)) {
await this.triggerCloudBackup(updatedSetting, user.uuid, unencryptedValue)
if (this.isEnablingCloudBackupSetting(updatedSettingName, unencryptedValue)) {
await this.triggerCloudBackup(updatedSettingName, user.uuid, unencryptedValue)
}
if (this.isDisablingSessionUserAgentLogging(updatedSetting)) {
if (this.isDisablingSessionUserAgentLogging(updatedSettingName, unencryptedValue)) {
await this.triggerSessionUserAgentCleanup(user)
}
}
@@ -77,7 +80,7 @@ export class SettingInterpreter implements SettingInterpreterInterface {
let userHasEmailsMuted = false
let muteEmailsSettingUuid = ''
const muteFailedEmailsBackupSetting = await this.settingRepository.findOneByNameAndUserUuid(
SettingName.MuteFailedBackupsEmails,
SettingName.NAMES.MuteFailedBackupsEmails,
userUuid,
)
if (muteFailedEmailsBackupSetting !== null) {
@@ -90,36 +93,39 @@ export class SettingInterpreter implements SettingInterpreterInterface {
)
}
private isChangingMuteEmailsSetting(setting: Setting): boolean {
private isChangingMuteEmailsSetting(settingName: string): boolean {
return [
SettingName.MuteFailedBackupsEmails,
SettingName.MuteFailedCloudBackupsEmails,
SettingName.MuteMarketingEmails,
SettingName.MuteSignInEmails,
].includes(setting.name as SettingName)
SettingName.NAMES.MuteFailedBackupsEmails,
SettingName.NAMES.MuteFailedCloudBackupsEmails,
SettingName.NAMES.MuteMarketingEmails,
SettingName.NAMES.MuteSignInEmails,
].includes(settingName)
}
private isEnablingEmailBackupSetting(setting: Setting): boolean {
return setting.name === SettingName.EmailBackupFrequency && setting.value !== EmailBackupFrequency.Disabled
}
private isEnablingCloudBackupSetting(setting: Setting): boolean {
private isEnablingEmailBackupSetting(settingName: string, newValue: string | null): boolean {
return (
(this.cloudBackupFrequencySettings.includes(setting.name as SettingName) ||
this.cloudBackupTokenSettings.includes(setting.name as SettingName)) &&
settingName === SettingName.NAMES.EmailBackupFrequency &&
[EmailBackupFrequency.Daily, EmailBackupFrequency.Weekly].includes(newValue as EmailBackupFrequency)
)
}
private isEnablingCloudBackupSetting(settingName: string, newValue: string | null): boolean {
return (
(this.cloudBackupFrequencySettings.includes(settingName) ||
this.cloudBackupTokenSettings.includes(settingName)) &&
!this.cloudBackupFrequencyDisabledValues.includes(
setting.value as DropboxBackupFrequency | OneDriveBackupFrequency | GoogleDriveBackupFrequency,
newValue as DropboxBackupFrequency | OneDriveBackupFrequency | GoogleDriveBackupFrequency,
)
)
}
private isDisablingSessionUserAgentLogging(setting: Setting): boolean {
return SettingName.LogSessionUserAgent === setting.name && LogSessionUserAgentOption.Disabled === setting.value
private isDisablingSessionUserAgentLogging(settingName: string, newValue: string | null): boolean {
return SettingName.NAMES.LogSessionUserAgent === settingName && LogSessionUserAgentOption.Disabled === newValue
}
private async triggerEmailSubscriptionChange(
user: User,
settingName: SettingName,
settingName: string,
unencryptedValue: string | null,
): Promise<void> {
await this.domainEventPublisher.publish(
@@ -140,33 +146,34 @@ export class SettingInterpreter implements SettingInterpreterInterface {
)
}
private async triggerCloudBackup(setting: Setting, userUuid: string, unencryptedValue: string | null): Promise<void> {
private async triggerCloudBackup(
settingName: string,
userUuid: string,
unencryptedValue: string | null,
): Promise<void> {
let cloudProvider
let tokenSettingName
switch (setting.name) {
case SettingName.DropboxBackupToken:
case SettingName.DropboxBackupFrequency:
switch (settingName) {
case SettingName.NAMES.DropboxBackupToken:
case SettingName.NAMES.DropboxBackupFrequency:
cloudProvider = 'DROPBOX'
tokenSettingName = SettingName.DropboxBackupToken
tokenSettingName = SettingName.NAMES.DropboxBackupToken
break
case SettingName.GoogleDriveBackupToken:
case SettingName.GoogleDriveBackupFrequency:
case SettingName.NAMES.GoogleDriveBackupToken:
case SettingName.NAMES.GoogleDriveBackupFrequency:
cloudProvider = 'GOOGLE_DRIVE'
tokenSettingName = SettingName.GoogleDriveBackupToken
tokenSettingName = SettingName.NAMES.GoogleDriveBackupToken
break
case SettingName.OneDriveBackupToken:
case SettingName.OneDriveBackupFrequency:
case SettingName.NAMES.OneDriveBackupToken:
case SettingName.NAMES.OneDriveBackupFrequency:
cloudProvider = 'ONE_DRIVE'
tokenSettingName = SettingName.OneDriveBackupToken
tokenSettingName = SettingName.NAMES.OneDriveBackupToken
break
}
let backupToken = null
if (this.cloudBackupFrequencySettings.includes(setting.name as SettingName)) {
const tokenSetting = await this.settingRepository.findLastByNameAndUserUuid(
tokenSettingName as SettingName,
userUuid,
)
if (this.cloudBackupFrequencySettings.includes(settingName)) {
const tokenSetting = await this.settingRepository.findLastByNameAndUserUuid(tokenSettingName as string, userUuid)
if (tokenSetting !== null) {
backupToken = await this.settingDecrypter.decryptSettingValue(tokenSetting, userUuid)
}
@@ -183,7 +190,7 @@ export class SettingInterpreter implements SettingInterpreterInterface {
let userHasEmailsMuted = false
let muteEmailsSettingUuid = ''
const muteFailedCloudBackupSetting = await this.settingRepository.findOneByNameAndUserUuid(
SettingName.MuteFailedCloudBackupsEmails,
SettingName.NAMES.MuteFailedCloudBackupsEmails,
userUuid,
)
if (muteFailedCloudBackupSetting !== null) {

View File

@@ -1,6 +1,5 @@
import { User } from '../User/User'
import { Setting } from './Setting'
export interface SettingInterpreterInterface {
interpretSettingUpdated(updatedSetting: Setting, user: User, newUnencryptedValue: string | null): Promise<void>
interpretSettingUpdated(updatedSettingName: string, user: User, newUnencryptedValue: string | null): Promise<void>
}

View File

@@ -11,6 +11,7 @@ export interface SettingRepositoryInterface {
findLastByNameAndUserUuid(name: string, userUuid: string): Promise<Setting | null>
findAllByUserUuid(userUuid: string): Promise<Setting[]>
streamAllByNameAndValue(name: SettingName, value: string): Promise<ReadStream>
streamAllByName(name: SettingName): Promise<ReadStream>
deleteByUserUuid(dto: DeleteSettingDto): Promise<void>
save(setting: Setting): Promise<Setting>
}

View File

@@ -39,7 +39,9 @@ describe('SettingService', () => {
} as jest.Mocked<User>
user.isPotentiallyAVaultAccount = jest.fn().mockReturnValue(false)
setting = {} as jest.Mocked<Setting>
setting = {
name: SettingName.NAMES.DropboxBackupToken,
} as jest.Mocked<Setting>
factory = {} as jest.Mocked<SettingFactoryInterface>
factory.create = jest.fn().mockReturnValue(setting)
@@ -54,7 +56,7 @@ describe('SettingService', () => {
settingsAssociationService.getDefaultSettingsAndValuesForNewUser = jest.fn().mockReturnValue(
new Map([
[
SettingName.MuteSignInEmails,
SettingName.NAMES.MuteSignInEmails,
{
value: MuteSignInEmailsOption.NotMuted,
sensitive: 0,
@@ -67,7 +69,7 @@ describe('SettingService', () => {
settingsAssociationService.getDefaultSettingsAndValuesForNewVaultAccount = jest.fn().mockReturnValue(
new Map([
[
SettingName.LogSessionUserAgent,
SettingName.NAMES.LogSessionUserAgent,
{
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
@@ -107,7 +109,7 @@ describe('SettingService', () => {
const result = await createService().createOrReplace({
user,
props: {
name: 'name',
name: SettingName.NAMES.MuteFailedBackupsEmails,
unencryptedValue: 'value',
serverEncryptionVersion: 1,
sensitive: false,
@@ -117,6 +119,20 @@ describe('SettingService', () => {
expect(result.status).toEqual('created')
})
it('should throw error if setting name is not valid', async () => {
await expect(
createService().createOrReplace({
user,
props: {
name: 'invalid',
unencryptedValue: 'value',
serverEncryptionVersion: 1,
sensitive: false,
},
}),
).rejects.toThrowError('Invalid setting name: invalid')
})
it('should create setting with a given uuid if it does not exist', async () => {
settingRepository.findOneByUuid = jest.fn().mockReturnValue(null)
@@ -124,7 +140,7 @@ describe('SettingService', () => {
user,
props: {
uuid: '1-2-3',
name: 'name',
name: SettingName.NAMES.MuteFailedBackupsEmails,
unencryptedValue: 'value',
serverEncryptionVersion: 1,
sensitive: false,
@@ -174,7 +190,10 @@ describe('SettingService', () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValue(setting)
expect(
await createService().findSettingWithDecryptedValue({ userUuid: '1-2-3', settingName: 'test' as SettingName }),
await createService().findSettingWithDecryptedValue({
userUuid: '1-2-3',
settingName: SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue(),
}),
).toEqual({
serverEncryptionVersion: 1,
value: 'decrypted',

View File

@@ -57,7 +57,7 @@ export class SettingService implements SettingServiceInterface {
if (dto.settingUuid !== undefined) {
setting = await this.settingRepository.findOneByUuid(dto.settingUuid)
} else {
setting = await this.settingRepository.findLastByNameAndUserUuid(dto.settingName, dto.userUuid)
setting = await this.settingRepository.findLastByNameAndUserUuid(dto.settingName.value, dto.userUuid)
}
if (setting === null) {
@@ -72,9 +72,15 @@ export class SettingService implements SettingServiceInterface {
async createOrReplace(dto: CreateOrReplaceSettingDto): Promise<CreateOrReplaceSettingResponse> {
const { user, props } = dto
const settingNameOrError = SettingName.create(props.name)
if (settingNameOrError.isFailed()) {
throw new Error(settingNameOrError.getError())
}
const settingName = settingNameOrError.getValue()
const existing = await this.findSettingWithDecryptedValue({
userUuid: user.uuid,
settingName: props.name as SettingName,
settingName,
settingUuid: props.uuid,
})
@@ -83,7 +89,7 @@ export class SettingService implements SettingServiceInterface {
this.logger.debug('[%s] Created setting %s: %O', user.uuid, props.name, setting)
await this.settingInterpreter.interpretSettingUpdated(setting, user, props.unencryptedValue)
await this.settingInterpreter.interpretSettingUpdated(setting.name, user, props.unencryptedValue)
return {
status: 'created',
@@ -95,7 +101,7 @@ export class SettingService implements SettingServiceInterface {
this.logger.debug('[%s] Replaced existing setting %s with: %O', user.uuid, props.name, setting)
await this.settingInterpreter.interpretSettingUpdated(setting, user, props.unencryptedValue)
await this.settingInterpreter.interpretSettingUpdated(setting.name, user, props.unencryptedValue)
return {
status: 'replaced',

View File

@@ -11,52 +11,68 @@ describe('SettingsAssociationService', () => {
const createService = () => new SettingsAssociationService()
it('should tell if a setting is mutable by the client', () => {
expect(createService().isSettingMutableByClient(SettingName.DropboxBackupFrequency)).toBeTruthy()
expect(
createService().isSettingMutableByClient(SettingName.create(SettingName.NAMES.DropboxBackupFrequency).getValue()),
).toBeTruthy()
})
it('should tell if a setting is immutable by the client', () => {
expect(createService().isSettingMutableByClient(SettingName.ListedAuthorSecrets)).toBeFalsy()
expect(
createService().isSettingMutableByClient(SettingName.create(SettingName.NAMES.ListedAuthorSecrets).getValue()),
).toBeFalsy()
})
it('should return default encryption version for a setting which enecryption version is not strictly defined', () => {
expect(createService().getEncryptionVersionForSetting(SettingName.MfaSecret)).toEqual(EncryptionVersion.Default)
expect(
createService().getEncryptionVersionForSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue()),
).toEqual(EncryptionVersion.Default)
})
it('should return a defined encryption version for a setting which enecryption version is strictly defined', () => {
expect(createService().getEncryptionVersionForSetting(SettingName.EmailBackupFrequency)).toEqual(
EncryptionVersion.Unencrypted,
)
expect(
createService().getEncryptionVersionForSetting(
SettingName.create(SettingName.NAMES.EmailBackupFrequency).getValue(),
),
).toEqual(EncryptionVersion.Unencrypted)
})
it('should return default sensitivity for a setting which sensitivity is not strictly defined', () => {
expect(createService().getSensitivityForSetting(SettingName.DropboxBackupToken)).toBeTruthy()
expect(
createService().getSensitivityForSetting(SettingName.create(SettingName.NAMES.DropboxBackupToken).getValue()),
).toBeTruthy()
})
it('should return a defined sensitivity for a setting which sensitivity is strictly defined', () => {
expect(createService().getSensitivityForSetting(SettingName.DropboxBackupFrequency)).toBeFalsy()
expect(
createService().getSensitivityForSetting(SettingName.create(SettingName.NAMES.DropboxBackupFrequency).getValue()),
).toBeFalsy()
})
it('should return the default set of settings for a newly registered user', () => {
const settings = createService().getDefaultSettingsAndValuesForNewUser()
const flatSettings = [...(settings as Map<SettingName, SettingDescription>).keys()]
expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
const flatSettings = [...(settings as Map<string, SettingDescription>).keys()]
expect(flatSettings).toEqual(['MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
})
it('should return the default set of settings for a newly registered vault account', () => {
const settings = createService().getDefaultSettingsAndValuesForNewVaultAccount()
const flatSettings = [...(settings as Map<SettingName, SettingDescription>).keys()]
expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
const flatSettings = [...(settings as Map<string, SettingDescription>).keys()]
expect(flatSettings).toEqual(['MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
expect(settings.get(SettingName.LogSessionUserAgent)?.value).toEqual('disabled')
expect(settings.get(SettingName.NAMES.LogSessionUserAgent)?.value).toEqual('disabled')
})
it('should return a permission name associated to a given setting', () => {
expect(createService().getPermissionAssociatedWithSetting(SettingName.EmailBackupFrequency)).toEqual(
PermissionName.DailyEmailBackup,
)
expect(
createService().getPermissionAssociatedWithSetting(
SettingName.create(SettingName.NAMES.EmailBackupFrequency).getValue(),
),
).toEqual(PermissionName.DailyEmailBackup)
})
it('should not return a permission name if not associated to a given setting', () => {
expect(createService().getPermissionAssociatedWithSetting(SettingName.ExtensionKey)).toBeUndefined()
expect(
createService().getPermissionAssociatedWithSetting(SettingName.create(SettingName.NAMES.ExtensionKey).getValue()),
).toBeUndefined()
})
})

View File

@@ -1,10 +1,5 @@
import { PermissionName } from '@standardnotes/features'
import {
LogSessionUserAgentOption,
MuteMarketingEmailsOption,
MuteSignInEmailsOption,
SettingName,
} from '@standardnotes/settings'
import { LogSessionUserAgentOption, MuteMarketingEmailsOption, SettingName } from '@standardnotes/settings'
import { injectable } from 'inversify'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
@@ -15,48 +10,44 @@ import { SettingsAssociationServiceInterface } from './SettingsAssociationServic
@injectable()
export class SettingsAssociationService implements SettingsAssociationServiceInterface {
private readonly UNENCRYPTED_SETTINGS = [
SettingName.EmailBackupFrequency,
SettingName.MuteFailedBackupsEmails,
SettingName.MuteFailedCloudBackupsEmails,
SettingName.MuteSignInEmails,
SettingName.MuteMarketingEmails,
SettingName.DropboxBackupFrequency,
SettingName.GoogleDriveBackupFrequency,
SettingName.OneDriveBackupFrequency,
SettingName.LogSessionUserAgent,
SettingName.NAMES.EmailBackupFrequency,
SettingName.NAMES.MuteFailedBackupsEmails,
SettingName.NAMES.MuteFailedCloudBackupsEmails,
SettingName.NAMES.MuteSignInEmails,
SettingName.NAMES.MuteMarketingEmails,
SettingName.NAMES.DropboxBackupFrequency,
SettingName.NAMES.GoogleDriveBackupFrequency,
SettingName.NAMES.OneDriveBackupFrequency,
SettingName.NAMES.LogSessionUserAgent,
]
private readonly UNSENSITIVE_SETTINGS = [
SettingName.DropboxBackupFrequency,
SettingName.GoogleDriveBackupFrequency,
SettingName.OneDriveBackupFrequency,
SettingName.EmailBackupFrequency,
SettingName.MuteFailedBackupsEmails,
SettingName.MuteFailedCloudBackupsEmails,
SettingName.MuteSignInEmails,
SettingName.MuteMarketingEmails,
SettingName.ListedAuthorSecrets,
SettingName.LogSessionUserAgent,
SettingName.NAMES.DropboxBackupFrequency,
SettingName.NAMES.GoogleDriveBackupFrequency,
SettingName.NAMES.OneDriveBackupFrequency,
SettingName.NAMES.EmailBackupFrequency,
SettingName.NAMES.MuteFailedBackupsEmails,
SettingName.NAMES.MuteFailedCloudBackupsEmails,
SettingName.NAMES.MuteSignInEmails,
SettingName.NAMES.MuteMarketingEmails,
SettingName.NAMES.ListedAuthorSecrets,
SettingName.NAMES.LogSessionUserAgent,
]
private readonly CLIENT_IMMUTABLE_SETTINGS = [SettingName.ListedAuthorSecrets]
private readonly CLIENT_IMMUTABLE_SETTINGS = [
SettingName.NAMES.ListedAuthorSecrets,
SettingName.NAMES.FileUploadBytesLimit,
SettingName.NAMES.FileUploadBytesUsed,
]
private readonly permissionsAssociatedWithSettings = new Map<SettingName, PermissionName>([
[SettingName.EmailBackupFrequency, PermissionName.DailyEmailBackup],
private readonly permissionsAssociatedWithSettings = new Map<string, PermissionName>([
[SettingName.NAMES.EmailBackupFrequency, PermissionName.DailyEmailBackup],
[SettingName.NAMES.MuteSignInEmails, PermissionName.SignInAlerts],
])
private readonly defaultSettings = new Map<SettingName, SettingDescription>([
private readonly defaultSettings = new Map<string, SettingDescription>([
[
SettingName.MuteSignInEmails,
{
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: MuteSignInEmailsOption.NotMuted,
replaceable: false,
},
],
[
SettingName.MuteMarketingEmails,
SettingName.NAMES.MuteMarketingEmails,
{
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
@@ -65,7 +56,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
},
],
[
SettingName.LogSessionUserAgent,
SettingName.NAMES.LogSessionUserAgent,
{
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
@@ -75,9 +66,9 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
],
])
private readonly vaultAccountDefaultSettingsOverwrites = new Map<SettingName, SettingDescription>([
private readonly vaultAccountDefaultSettingsOverwrites = new Map<string, SettingDescription>([
[
SettingName.LogSessionUserAgent,
SettingName.NAMES.LogSessionUserAgent,
{
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
@@ -88,7 +79,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
])
isSettingMutableByClient(settingName: SettingName): boolean {
if (this.CLIENT_IMMUTABLE_SETTINGS.includes(settingName)) {
if (this.CLIENT_IMMUTABLE_SETTINGS.includes(settingName.value)) {
return false
}
@@ -96,7 +87,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
}
getSensitivityForSetting(settingName: SettingName): boolean {
if (this.UNSENSITIVE_SETTINGS.includes(settingName)) {
if (this.UNSENSITIVE_SETTINGS.includes(settingName.value)) {
return false
}
@@ -104,7 +95,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
}
getEncryptionVersionForSetting(settingName: SettingName): EncryptionVersion {
if (this.UNENCRYPTED_SETTINGS.includes(settingName)) {
if (this.UNENCRYPTED_SETTINGS.includes(settingName.value)) {
return EncryptionVersion.Unencrypted
}
@@ -112,18 +103,18 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
}
getPermissionAssociatedWithSetting(settingName: SettingName): PermissionName | undefined {
if (!this.permissionsAssociatedWithSettings.has(settingName)) {
if (!this.permissionsAssociatedWithSettings.has(settingName.value)) {
return undefined
}
return this.permissionsAssociatedWithSettings.get(settingName)
return this.permissionsAssociatedWithSettings.get(settingName.value)
}
getDefaultSettingsAndValuesForNewUser(): Map<SettingName, SettingDescription> {
getDefaultSettingsAndValuesForNewUser(): Map<string, SettingDescription> {
return this.defaultSettings
}
getDefaultSettingsAndValuesForNewVaultAccount(): Map<SettingName, SettingDescription> {
getDefaultSettingsAndValuesForNewVaultAccount(): Map<string, SettingDescription> {
const defaultVaultSettings = new Map(this.defaultSettings)
for (const vaultAccountDefaultSettingOverwriteKey of this.vaultAccountDefaultSettingsOverwrites.keys()) {

View File

@@ -1,13 +1,14 @@
import { PermissionName } from '@standardnotes/features'
import { SettingName, SubscriptionSettingName } from '@standardnotes/settings'
import { SettingName } from '@standardnotes/settings'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { SettingDescription } from './SettingDescription'
export interface SettingsAssociationServiceInterface {
getDefaultSettingsAndValuesForNewUser(): Map<SettingName, SettingDescription>
getDefaultSettingsAndValuesForNewVaultAccount(): Map<SettingName, SettingDescription>
getDefaultSettingsAndValuesForNewUser(): Map<string, SettingDescription>
getDefaultSettingsAndValuesForNewVaultAccount(): Map<string, SettingDescription>
getPermissionAssociatedWithSetting(settingName: SettingName): PermissionName | undefined
getEncryptionVersionForSetting(settingName: SettingName): EncryptionVersion
getSensitivityForSetting(settingName: SettingName): boolean
isSettingMutableByClient(settingName: SettingName | SubscriptionSettingName): boolean
isSettingMutableByClient(settingName: SettingName): boolean
}

View File

@@ -3,5 +3,6 @@ import { SubscriptionSetting } from './SubscriptionSetting'
export interface SubscriptionSettingRepositoryInterface {
findOneByUuid(uuid: string): Promise<SubscriptionSetting | null>
findLastByNameAndUserSubscriptionUuid(name: string, userSubscriptionUuid: string): Promise<SubscriptionSetting | null>
findAllBySubscriptionUuid(userSubscriptionUuid: string): Promise<SubscriptionSetting[]>
save(subscriptionSetting: SubscriptionSetting): Promise<SubscriptionSetting>
}

View File

@@ -1,6 +1,5 @@
import 'reflect-metadata'
import { SubscriptionSettingName } from '@standardnotes/settings'
import { Logger } from 'winston'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
@@ -14,6 +13,8 @@ import { User } from '../User/User'
import { SettingFactoryInterface } from './SettingFactoryInterface'
import { SubscriptionSettingsAssociationServiceInterface } from './SubscriptionSettingsAssociationServiceInterface'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { SettingName } from '@standardnotes/settings'
import { SettingInterpreterInterface } from './SettingInterpreterInterface'
describe('SubscriptionSettingService', () => {
let setting: SubscriptionSetting
@@ -22,6 +23,7 @@ describe('SubscriptionSettingService', () => {
let factory: SettingFactoryInterface
let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface
let subscriptionSettingsAssociationService: SubscriptionSettingsAssociationServiceInterface
let settingInterpreter: SettingInterpreterInterface
let settingDecrypter: SettingDecrypterInterface
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
let logger: Logger
@@ -31,6 +33,7 @@ describe('SubscriptionSettingService', () => {
factory,
subscriptionSettingRepository,
subscriptionSettingsAssociationService,
settingInterpreter,
settingDecrypter,
userSubscriptionRepository,
logger,
@@ -44,7 +47,9 @@ describe('SubscriptionSettingService', () => {
user: Promise.resolve(user),
} as jest.Mocked<UserSubscription>
setting = {} as jest.Mocked<SubscriptionSetting>
setting = {
name: SettingName.NAMES.FileUploadBytesUsed,
} as jest.Mocked<SubscriptionSetting>
factory = {} as jest.Mocked<SettingFactoryInterface>
factory.createSubscriptionSetting = jest.fn().mockReturnValue(setting)
@@ -68,7 +73,7 @@ describe('SubscriptionSettingService', () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([
[
SubscriptionSettingName.FileUploadBytesUsed,
SettingName.NAMES.FileUploadBytesUsed,
{
value: '0',
sensitive: 0,
@@ -79,6 +84,9 @@ describe('SubscriptionSettingService', () => {
]),
)
settingInterpreter = {} as jest.Mocked<SettingInterpreterInterface>
settingInterpreter.interpretSettingUpdated = jest.fn()
settingDecrypter = {} as jest.Mocked<SettingDecrypterInterface>
settingDecrypter.decryptSettingValue = jest.fn().mockReturnValue('decrypted')
@@ -98,11 +106,59 @@ describe('SubscriptionSettingService', () => {
expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting)
})
it('should throw error if subscription setting is invalid', async () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([
[
'invalid',
{
value: '0',
sensitive: 0,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
replaceable: true,
},
],
]),
)
await expect(
createService().applyDefaultSubscriptionSettingsForSubscription(
userSubscription,
SubscriptionName.PlusPlan,
'1-2-3',
),
).rejects.toThrow()
})
it('should throw error if setting name is not a subscription setting when applying defaults', async () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([
[
SettingName.NAMES.DropboxBackupFrequency,
{
value: '0',
sensitive: 0,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
replaceable: false,
},
],
]),
)
await expect(
createService().applyDefaultSubscriptionSettingsForSubscription(
userSubscription,
SubscriptionName.PlusPlan,
'1-2-3',
),
).rejects.toThrow()
})
it('should reassign existing default settings for a subscription if it is not replaceable', async () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([
[
SubscriptionSettingName.FileUploadBytesUsed,
SettingName.NAMES.FileUploadBytesUsed,
{
value: '0',
sensitive: 0,
@@ -127,7 +183,7 @@ describe('SubscriptionSettingService', () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([
[
SubscriptionSettingName.FileUploadBytesUsed,
SettingName.NAMES.FileUploadBytesUsed,
{
value: '0',
sensitive: 0,
@@ -152,7 +208,7 @@ describe('SubscriptionSettingService', () => {
subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue(
new Map([
[
SubscriptionSettingName.FileUploadBytesUsed,
SettingName.NAMES.FileUploadBytesUsed,
{
value: '0',
sensitive: 0,
@@ -196,7 +252,7 @@ describe('SubscriptionSettingService', () => {
const result = await createService().createOrReplace({
userSubscription,
props: {
name: 'name',
name: SettingName.NAMES.FileUploadBytesLimit,
unencryptedValue: 'value',
serverEncryptionVersion: 1,
sensitive: false,
@@ -206,6 +262,34 @@ describe('SubscriptionSettingService', () => {
expect(result.status).toEqual('created')
})
it('should throw error if the setting name is not valid', async () => {
await expect(
createService().createOrReplace({
userSubscription,
props: {
name: 'invalid',
unencryptedValue: 'value',
serverEncryptionVersion: 1,
sensitive: false,
},
}),
).rejects.toThrow()
})
it('should throw error if the setting name is not a subscription setting', async () => {
await expect(
createService().createOrReplace({
userSubscription,
props: {
name: SettingName.NAMES.DropboxBackupFrequency,
unencryptedValue: 'value',
serverEncryptionVersion: 1,
sensitive: false,
},
}),
).rejects.toThrow()
})
it('should create setting with a given uuid if it does not exist', async () => {
subscriptionSettingRepository.findOneByUuid = jest.fn().mockReturnValue(null)
@@ -213,7 +297,7 @@ describe('SubscriptionSettingService', () => {
userSubscription,
props: {
uuid: '1-2-3',
name: 'name',
name: SettingName.NAMES.FileUploadBytesLimit,
unencryptedValue: 'value',
serverEncryptionVersion: 1,
sensitive: false,
@@ -266,11 +350,21 @@ describe('SubscriptionSettingService', () => {
await createService().findSubscriptionSettingWithDecryptedValue({
userSubscriptionUuid: '2-3-4',
userUuid: '1-2-3',
subscriptionSettingName: 'test' as SubscriptionSettingName,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
}),
).toEqual({
serverEncryptionVersion: 1,
value: 'decrypted',
})
})
it('should throw error when trying to find and decrypt a setting with invalid subscription setting name', async () => {
await expect(
createService().findSubscriptionSettingWithDecryptedValue({
userSubscriptionUuid: '2-3-4',
userUuid: '1-2-3',
subscriptionSettingName: SettingName.create(SettingName.NAMES.DropboxBackupFrequency).getValue(),
}),
).rejects.toThrow()
})
})

View File

@@ -1,5 +1,4 @@
import { SubscriptionName } from '@standardnotes/common'
import { SubscriptionSettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
@@ -17,6 +16,8 @@ import { SubscriptionSettingRepositoryInterface } from './SubscriptionSettingRep
import { SettingFactoryInterface } from './SettingFactoryInterface'
import { SubscriptionSettingsAssociationServiceInterface } from './SubscriptionSettingsAssociationServiceInterface'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { SettingName } from '@standardnotes/settings'
import { SettingInterpreterInterface } from './SettingInterpreterInterface'
@injectable()
export class SubscriptionSettingService implements SubscriptionSettingServiceInterface {
@@ -26,6 +27,7 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
private subscriptionSettingRepository: SubscriptionSettingRepositoryInterface,
@inject(TYPES.SubscriptionSettingsAssociationService)
private subscriptionSettingAssociationService: SubscriptionSettingsAssociationServiceInterface,
@inject(TYPES.SettingInterpreter) private settingInterpreter: SettingInterpreterInterface,
@inject(TYPES.SettingDecrypter) private settingDecrypter: SettingDecrypterInterface,
@inject(TYPES.UserSubscriptionRepository) private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
@inject(TYPES.Logger) private logger: Logger,
@@ -44,8 +46,17 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
return
}
for (const settingName of defaultSettingsWithValues.keys()) {
const setting = defaultSettingsWithValues.get(settingName) as SettingDescription
for (const settingNameString of defaultSettingsWithValues.keys()) {
const settingNameOrError = SettingName.create(settingNameString)
if (settingNameOrError.isFailed()) {
throw new Error(settingNameOrError.getError())
}
const settingName = settingNameOrError.getValue()
if (!settingName.isASubscriptionSetting()) {
throw new Error(`Setting ${settingName.value} is not a subscription setting`)
}
const setting = defaultSettingsWithValues.get(settingName.value) as SettingDescription
if (!setting.replaceable) {
const existingSetting = await this.findPreviousSubscriptionSetting(settingName, userSubscription.uuid, userUuid)
if (existingSetting !== null) {
@@ -59,7 +70,7 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
await this.createOrReplace({
userSubscription,
props: {
name: settingName,
name: settingName.value,
unencryptedValue: setting.value,
serverEncryptionVersion: setting.serverEncryptionVersion,
sensitive: setting.sensitive,
@@ -71,12 +82,16 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
async findSubscriptionSettingWithDecryptedValue(
dto: FindSubscriptionSettingDTO,
): Promise<SubscriptionSetting | null> {
if (!dto.subscriptionSettingName.isASubscriptionSetting()) {
throw new Error(`Setting ${dto.subscriptionSettingName.value} is not a subscription setting`)
}
let setting: SubscriptionSetting | null
if (dto.settingUuid !== undefined) {
setting = await this.subscriptionSettingRepository.findOneByUuid(dto.settingUuid)
} else {
setting = await this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid(
dto.subscriptionSettingName,
dto.subscriptionSettingName.value,
dto.userSubscriptionUuid,
)
}
@@ -95,10 +110,21 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
): Promise<CreateOrReplaceSubscriptionSettingResponse> {
const { userSubscription, props } = dto
const settingNameOrError = SettingName.create(props.name)
if (settingNameOrError.isFailed()) {
throw new Error(settingNameOrError.getError())
}
const settingName = settingNameOrError.getValue()
if (!settingName.isASubscriptionSetting()) {
throw new Error(`Setting ${settingName.value} is not a subscription setting`)
}
const user = await userSubscription.user
const existing = await this.findSubscriptionSettingWithDecryptedValue({
userUuid: (await userSubscription.user).uuid,
userUuid: user.uuid,
userSubscriptionUuid: userSubscription.uuid,
subscriptionSettingName: props.name as SubscriptionSettingName,
subscriptionSettingName: settingName,
settingUuid: props.uuid,
})
@@ -109,6 +135,8 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
this.logger.debug('Created subscription setting %s: %O', props.name, subscriptionSetting)
await this.settingInterpreter.interpretSettingUpdated(settingName.value, user, props.unencryptedValue)
return {
status: 'created',
subscriptionSetting,
@@ -121,6 +149,8 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
this.logger.debug('Replaced existing subscription setting %s with: %O', props.name, subscriptionSetting)
await this.settingInterpreter.interpretSettingUpdated(settingName.value, user, props.unencryptedValue)
return {
status: 'replaced',
subscriptionSetting,
@@ -128,7 +158,7 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
}
private async findPreviousSubscriptionSetting(
settingName: SubscriptionSettingName,
settingName: SettingName,
currentUserSubscriptionUuid: string,
userUuid: string,
): Promise<SubscriptionSetting | null> {
@@ -142,6 +172,9 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt
return null
}
return this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid(settingName, lastSubscription.uuid)
return this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid(
settingName.value,
lastSubscription.uuid,
)
}
}

View File

@@ -2,9 +2,9 @@ import 'reflect-metadata'
import { SubscriptionName } from '@standardnotes/common'
import { RoleName } from '@standardnotes/domain-core'
import { SubscriptionSettingName } from '@standardnotes/settings'
import { SettingName } from '@standardnotes/settings'
import { PermissionName } from '@standardnotes/features'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
import { RoleToSubscriptionMapInterface } from '../Role/RoleToSubscriptionMapInterface'
@@ -51,14 +51,11 @@ describe('SubscriptionSettingsAssociationService', () => {
const flatSettings = [
...(
settings as Map<
SubscriptionSettingName,
{ value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }
>
settings as Map<string, { value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }>
).keys(),
]
expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'FILE_UPLOAD_BYTES_LIMIT'])
expect(settings?.get(SubscriptionSettingName.FileUploadBytesLimit)).toEqual({
expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'MUTE_SIGN_IN_EMAILS', 'FILE_UPLOAD_BYTES_LIMIT'])
expect(settings?.get(SettingName.NAMES.FileUploadBytesLimit)).toEqual({
sensitive: false,
serverEncryptionVersion: 0,
value: '107374182400',
@@ -79,14 +76,11 @@ describe('SubscriptionSettingsAssociationService', () => {
const flatSettings = [
...(
settings as Map<
SubscriptionSettingName,
{ value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }
>
settings as Map<string, { value: string; sensitive: boolean; serverEncryptionVersion: EncryptionVersion }>
).keys(),
]
expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'FILE_UPLOAD_BYTES_LIMIT'])
expect(settings?.get(SubscriptionSettingName.FileUploadBytesLimit)).toEqual({
expect(flatSettings).toEqual(['FILE_UPLOAD_BYTES_USED', 'MUTE_SIGN_IN_EMAILS', 'FILE_UPLOAD_BYTES_LIMIT'])
expect(settings?.get(SettingName.NAMES.FileUploadBytesLimit)).toEqual({
sensitive: false,
serverEncryptionVersion: 0,
value: '104857600',

View File

@@ -1,6 +1,6 @@
import { SubscriptionName } from '@standardnotes/common'
import { PermissionName } from '@standardnotes/features'
import { SubscriptionSettingName } from '@standardnotes/settings'
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
@@ -19,40 +19,55 @@ export class SubscriptionSettingsAssociationService implements SubscriptionSetti
@inject(TYPES.RoleRepository) private roleRepository: RoleRepositoryInterface,
) {}
private readonly settingsToSubscriptionNameMap = new Map<
SubscriptionName,
Map<SubscriptionSettingName, SettingDescription>
>([
private readonly settingsToSubscriptionNameMap = new Map<SubscriptionName, Map<string, SettingDescription>>([
[
SubscriptionName.PlusPlan,
new Map([
[
SubscriptionSettingName.FileUploadBytesUsed,
SettingName.NAMES.FileUploadBytesUsed,
{ sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false },
],
[
SettingName.NAMES.MuteSignInEmails,
{
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: 'not_muted',
replaceable: false,
},
],
]),
],
[
SubscriptionName.ProPlan,
new Map([
[
SubscriptionSettingName.FileUploadBytesUsed,
SettingName.NAMES.FileUploadBytesUsed,
{ sensitive: false, serverEncryptionVersion: EncryptionVersion.Unencrypted, value: '0', replaceable: false },
],
[
SettingName.NAMES.MuteSignInEmails,
{
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: 'not_muted',
replaceable: false,
},
],
]),
],
])
async getDefaultSettingsAndValuesForSubscriptionName(
subscriptionName: SubscriptionName,
): Promise<Map<SubscriptionSettingName, SettingDescription> | undefined> {
): Promise<Map<string, SettingDescription> | undefined> {
const defaultSettings = this.settingsToSubscriptionNameMap.get(subscriptionName)
if (defaultSettings === undefined) {
return undefined
}
defaultSettings.set(SubscriptionSettingName.FileUploadBytesLimit, {
defaultSettings.set(SettingName.NAMES.FileUploadBytesLimit, {
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: (await this.getFileUploadLimit(subscriptionName)).toString(),

View File

@@ -1,11 +1,10 @@
import { SubscriptionName } from '@standardnotes/common'
import { SubscriptionSettingName } from '@standardnotes/settings'
import { SettingDescription } from './SettingDescription'
export interface SubscriptionSettingsAssociationServiceInterface {
getDefaultSettingsAndValuesForSubscriptionName(
subscriptionName: SubscriptionName,
): Promise<Map<SubscriptionSettingName, SettingDescription> | undefined>
): Promise<Map<string, SettingDescription> | undefined>
getFileUploadLimit(subscriptionName: SubscriptionName): Promise<number>
}

View File

@@ -0,0 +1,20 @@
import { SessionRepositoryInterface } from '../../Session/SessionRepositoryInterface'
import { CleanupExpiredSessions } from './CleanupExpiredSessions'
describe('CleanupExpiredSessions', () => {
let sessionsRepository: SessionRepositoryInterface
const createUseCase = () => new CleanupExpiredSessions(sessionsRepository)
beforeEach(() => {
sessionsRepository = {} as jest.Mocked<SessionRepositoryInterface>
sessionsRepository.removeExpiredBefore = jest.fn()
})
it('should remove stale sessions', async () => {
await createUseCase().execute({ date: new Date() })
expect(sessionsRepository.removeExpiredBefore).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,15 @@
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { SessionRepositoryInterface } from '../../Session/SessionRepositoryInterface'
import { CleanupExpiredSessionsDTO } from './CleanupExpiredSessionsDTO'
export class CleanupExpiredSessions implements UseCaseInterface<string> {
constructor(private sessionTracesRepository: SessionRepositoryInterface) {}
async execute(dto: CleanupExpiredSessionsDTO): Promise<Result<string>> {
await this.sessionTracesRepository.removeExpiredBefore(dto.date)
return Result.ok('Expired sessions removed')
}
}

View File

@@ -0,0 +1,3 @@
export interface CleanupExpiredSessionsDTO {
date: Date
}

View File

@@ -3,7 +3,7 @@ import { SubscriptionName } from '@standardnotes/common'
import { TimerInterface } from '@standardnotes/time'
import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security'
import { CreateValetTokenPayload, CreateValetTokenResponseData } from '@standardnotes/responses'
import { SubscriptionSettingName } from '@standardnotes/settings'
import { SettingName } from '@standardnotes/settings'
import TYPES from '../../../Bootstrap/Types'
import { UseCaseInterface } from '../UseCaseInterface'
@@ -56,7 +56,7 @@ export class CreateValetToken implements UseCaseInterface {
const uploadBytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: regularSubscriptionUserUuid,
userSubscriptionUuid: regularSubscription.uuid,
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesUsed,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
})
if (uploadBytesUsedSetting !== null) {
uploadBytesUsed = +(uploadBytesUsedSetting.value as string)
@@ -70,7 +70,7 @@ export class CreateValetToken implements UseCaseInterface {
await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: regularSubscriptionUserUuid,
userSubscriptionUuid: regularSubscription.uuid,
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
})
if (overwriteWithUserUploadBytesLimitSetting !== null) {
uploadBytesLimit = +(overwriteWithUserUploadBytesLimitSetting.value as string)

View File

@@ -12,7 +12,6 @@ describe('DeleteAuthenticator', () => {
beforeEach(() => {
authenticator = Authenticator.create({
counter: 1,
name: 'my-key',
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialId: Buffer.from('credentialId'),

View File

@@ -24,7 +24,6 @@ describe('GenerateAuthenticatorAuthenticationOptions', () => {
beforeEach(() => {
const authenticator = Authenticator.create({
counter: 1,
name: 'my-key',
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialId: Buffer.from('credentialId'),
@@ -54,7 +53,7 @@ describe('GenerateAuthenticatorAuthenticationOptions', () => {
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Could not generate authenticator registration options: Username cannot be empty')
expect(result.getError()).toBe('Could not generate authenticator authentication options: Username cannot be empty')
})
it('should return error if user uuid is not valid', async () => {
@@ -70,7 +69,7 @@ describe('GenerateAuthenticatorAuthenticationOptions', () => {
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe(
'Could not generate authenticator registration options: Given value is not a valid uuid: invalid',
'Could not generate authenticator authentication options: Given value is not a valid uuid: invalid',
)
})
@@ -97,7 +96,7 @@ describe('GenerateAuthenticatorAuthenticationOptions', () => {
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Could not generate authenticator registration options: Oops')
expect(result.getError()).toBe('Could not generate authenticator authentication options: Oops')
mock.mockRestore()
})

View File

@@ -19,7 +19,7 @@ export class GenerateAuthenticatorAuthenticationOptions implements UseCaseInterf
async execute(dto: GenerateAuthenticatorAuthenticationOptionsDTO): Promise<Result<Record<string, unknown>>> {
const usernameOrError = Username.create(dto.username)
if (usernameOrError.isFailed()) {
return Result.fail(`Could not generate authenticator registration options: ${usernameOrError.getError()}`)
return Result.fail(`Could not generate authenticator authentication options: ${usernameOrError.getError()}`)
}
const username = usernameOrError.getValue()
@@ -46,7 +46,7 @@ export class GenerateAuthenticatorAuthenticationOptions implements UseCaseInterf
const userUuidOrError = Uuid.create(user.uuid)
if (userUuidOrError.isFailed()) {
return Result.fail(`Could not generate authenticator registration options: ${userUuidOrError.getError()}`)
return Result.fail(`Could not generate authenticator authentication options: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
@@ -67,7 +67,7 @@ export class GenerateAuthenticatorAuthenticationOptions implements UseCaseInterf
})
if (authenticatorChallengeOrError.isFailed()) {
return Result.fail(
`Could not generate authenticator registration options: ${authenticatorChallengeOrError.getError()}`,
`Could not generate authenticator authentication options: ${authenticatorChallengeOrError.getError()}`,
)
}
const authenticatorChallenge = authenticatorChallengeOrError.getValue()

View File

@@ -21,7 +21,6 @@ describe('GenerateAuthenticatorRegistrationOptions', () => {
beforeEach(() => {
const authenticator = Authenticator.create({
counter: 1,
name: 'my-key',
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialId: Buffer.from('credentialId'),

View File

@@ -43,6 +43,7 @@ export class GenerateAuthenticatorRegistrationOptions implements UseCaseInterfac
type: 'public-key',
transports: authenticator.props.transports,
})),
supportedAlgorithmIDs: [-7, -257],
})
const authenticatorChallengeOrError = AuthenticatorChallenge.create({

View File

@@ -35,7 +35,7 @@ export class GenerateRecoveryCodes implements UseCaseInterface<string> {
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.RecoveryCodes,
name: SettingName.NAMES.RecoveryCodes,
unencryptedValue: recoveryCodes,
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: false,

View File

@@ -1,75 +1,239 @@
import { SettingName } from '@standardnotes/settings'
import 'reflect-metadata'
import { SettingName } from '@standardnotes/settings'
import { SettingProjector } from '../../../Projection/SettingProjector'
import { Setting } from '../../Setting/Setting'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
import { GetSetting } from './GetSetting'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
import { SubscriptionSetting } from '../../Setting/SubscriptionSetting'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
describe('GetSetting', () => {
let settingProjector: SettingProjector
let setting: Setting
let subscriptionSetting: SubscriptionSetting
let settingService: SettingServiceInterface
let userSubscriptionService: UserSubscriptionServiceInterface
let subscriptionSettingProjector: SubscriptionSettingProjector
let subscriptionSettingService: SubscriptionSettingServiceInterface
let regularSubscription: UserSubscription
let sharedSubscription: UserSubscription
const createUseCase = () => new GetSetting(settingProjector, settingService)
const createUseCase = () =>
new GetSetting(
settingProjector,
subscriptionSettingProjector,
settingService,
subscriptionSettingService,
userSubscriptionService,
)
beforeEach(() => {
setting = {} as jest.Mocked<Setting>
subscriptionSetting = {
sensitive: false,
} as jest.Mocked<SubscriptionSetting>
settingService = {} as jest.Mocked<SettingServiceInterface>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
settingProjector = {} as jest.Mocked<SettingProjector>
settingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' })
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest
.fn()
.mockReturnValue(subscriptionSetting)
regularSubscription = {
uuid: '1-2-3',
subscriptionType: UserSubscriptionType.Regular,
} as jest.Mocked<UserSubscription>
sharedSubscription = {
uuid: '2-3-4',
subscriptionType: UserSubscriptionType.Shared,
} as jest.Mocked<UserSubscription>
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
subscriptionSettingProjector = {} as jest.Mocked<SubscriptionSettingProjector>
subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' })
})
it('should find a setting for user', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test' })).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'bar' },
describe('no subscription', () => {
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'bar' },
})
})
it('should not find a setting if the setting name is invalid', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })).toEqual({
success: false,
error: {
message: 'Invalid setting name: invalid',
},
})
})
it('should not get a setting for user if it does not exist', async () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
).toEqual({
success: false,
error: {
message: 'Setting DROPBOX_BACKUP_FREQUENCY for user 1-2-3 not found!',
},
})
})
it('should not retrieve a sensitive setting for user', async () => {
setting = {
sensitive: true,
name: SettingName.NAMES.MfaSecret,
} as jest.Mocked<Setting>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })).toEqual({
success: true,
sensitive: true,
})
})
it('should not retrieve a subscription setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: false,
error: {
message: 'No subscription found.',
},
})
})
it('should retrieve a sensitive setting for user if explicitly told to', async () => {
setting = {
sensitive: true,
name: SettingName.NAMES.MfaSecret,
} as jest.Mocked<Setting>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(
await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MfaSecret,
allowSensitiveRetrieval: true,
}),
).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'bar' },
})
})
})
it('should not get a setting for user if it does not exist', async () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
describe('regular subscription', () => {
beforeEach(() => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription: null })
})
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test' })).toEqual({
success: false,
error: {
message: 'Setting test for user 1-2-3 not found!',
},
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
})
it('should not get a suscription setting for user if it does not exist', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: false,
error: {
message: 'Subscription setting MUTE_SIGN_IN_EMAILS for user 1-2-3 not found!',
},
})
})
it('should not retrieve a sensitive subscription setting for user', async () => {
subscriptionSetting.sensitive = true
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest
.fn()
.mockReturnValue(subscriptionSetting)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
sensitive: true,
})
})
})
it('should not retrieve a sensitive setting for user', async () => {
setting = {
sensitive: true,
name: SettingName.MfaSecret,
} as jest.Mocked<Setting>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.MfaSecret })).toEqual({
success: true,
sensitive: true,
describe('shared subscription', () => {
beforeEach(() => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription })
})
})
it('should retrieve a sensitive setting for user if explicitly told to', async () => {
setting = {
sensitive: true,
name: SettingName.MfaSecret,
} as jest.Mocked<Setting>
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(subscriptionSettingService.findSubscriptionSettingWithDecryptedValue).toHaveBeenCalledWith({
subscriptionSettingName: SettingName.create(SettingName.NAMES.MuteSignInEmails).getValue(),
userSubscriptionUuid: '2-3-4',
userUuid: '1-2-3',
})
})
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: 'MFA_SECRET', allowSensitiveRetrieval: true }),
).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'bar' },
it('should find a regular subscription only setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.FileUploadBytesLimit }),
).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
expect(subscriptionSettingService.findSubscriptionSettingWithDecryptedValue).toHaveBeenCalledWith({
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
userSubscriptionUuid: '1-2-3',
userUuid: '1-2-3',
})
})
})
})

View File

@@ -1,32 +1,100 @@
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { GetSettingDto } from './GetSettingDto'
import { GetSettingResponse } from './GetSettingResponse'
import { UseCaseInterface } from '../UseCaseInterface'
import TYPES from '../../../Bootstrap/Types'
import { SettingProjector } from '../../../Projection/SettingProjector'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { GetSettingDto } from './GetSettingDto'
import { GetSettingResponse } from './GetSettingResponse'
import { UserSubscription } from '../../Subscription/UserSubscription'
@injectable()
export class GetSetting implements UseCaseInterface {
constructor(
@inject(TYPES.SettingProjector) private settingProjector: SettingProjector,
@inject(TYPES.SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
@inject(TYPES.SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface,
@inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
) {}
async execute(dto: GetSettingDto): Promise<GetSettingResponse> {
const { userUuid, settingName } = dto
const settingNameOrError = SettingName.create(dto.settingName)
if (settingNameOrError.isFailed()) {
return {
success: false,
error: {
message: settingNameOrError.getError(),
},
}
}
const settingName = settingNameOrError.getValue()
if (settingName.isASubscriptionSetting()) {
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(dto.userUuid)
let subscription: UserSubscription | null
if (settingName.isARegularOnlySubscriptionSetting()) {
subscription = regularSubscription
} else {
subscription = sharedSubscription ?? regularSubscription
}
if (!subscription) {
return {
success: false,
error: {
message: 'No subscription found.',
},
}
}
const subscriptionSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: dto.userUuid,
subscriptionSettingName: settingName,
userSubscriptionUuid: subscription.uuid,
})
if (subscriptionSetting === null) {
return {
success: false,
error: {
message: `Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`,
},
}
}
if (subscriptionSetting.sensitive && !dto.allowSensitiveRetrieval) {
return {
success: true,
sensitive: true,
}
}
const simpleSubscriptionSetting = await this.subscriptionSettingProjector.projectSimple(subscriptionSetting)
return {
success: true,
userUuid: dto.userUuid,
setting: simpleSubscriptionSetting,
}
}
const setting = await this.settingService.findSettingWithDecryptedValue({
userUuid,
settingName: settingName as SettingName,
userUuid: dto.userUuid,
settingName,
})
if (setting === null) {
return {
success: false,
error: {
message: `Setting ${settingName} for user ${userUuid} not found!`,
message: `Setting ${settingName.value} for user ${dto.userUuid} not found!`,
},
}
}
@@ -42,7 +110,7 @@ export class GetSetting implements UseCaseInterface {
return {
success: true,
userUuid,
userUuid: dto.userUuid,
setting: simpleSetting,
}
}

View File

@@ -11,36 +11,84 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { User } from '../../User/User'
import { CrypterInterface } from '../../Encryption/CrypterInterface'
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
import { SubscriptionSetting } from '../../Setting/SubscriptionSetting'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
describe('GetSettings', () => {
let settingRepository: SettingRepositoryInterface
let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface
let userSubscriptionService: UserSubscriptionServiceInterface
let settingProjector: SettingProjector
let subscriptionSettingProjector: SubscriptionSettingProjector
let setting: Setting
let mfaSetting: Setting
let signInEmailsSetting: SubscriptionSetting
let userRepository: UserRepositoryInterface
let user: User
let crypter: CrypterInterface
let regularSubscription: UserSubscription
let sharedSubscription: UserSubscription
const createUseCase = () => new GetSettings(settingRepository, settingProjector, userRepository, crypter)
const createUseCase = () =>
new GetSettings(
settingRepository,
subscriptionSettingRepository,
userSubscriptionService,
settingProjector,
subscriptionSettingProjector,
userRepository,
crypter,
)
beforeEach(() => {
setting = {
name: 'test',
updatedAt: 345,
sensitive: false,
} as jest.Mocked<Setting>
setting = new Setting()
setting.name = 'test'
setting.updatedAt = 345
setting.sensitive = false
mfaSetting = {
name: SettingName.MfaSecret,
updatedAt: 122,
sensitive: true,
} as jest.Mocked<Setting>
mfaSetting = new Setting()
mfaSetting.name = SettingName.NAMES.MfaSecret
mfaSetting.updatedAt = 122
mfaSetting.sensitive = true
signInEmailsSetting = new SubscriptionSetting()
signInEmailsSetting.name = SettingName.NAMES.MuteSignInEmails
signInEmailsSetting.updatedAt = 122
signInEmailsSetting.sensitive = false
signInEmailsSetting.value = 'not_muted'
settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting, mfaSetting])
subscriptionSettingRepository = {} as jest.Mocked<SubscriptionSettingRepositoryInterface>
subscriptionSettingRepository.findAllBySubscriptionUuid = jest.fn().mockReturnValue([signInEmailsSetting])
regularSubscription = {
uuid: '1-2-3',
subscriptionType: UserSubscriptionType.Regular,
user: Promise.resolve(user),
} as jest.Mocked<UserSubscription>
sharedSubscription = {
uuid: '2-3-4',
subscriptionType: UserSubscriptionType.Shared,
user: Promise.resolve(user),
} as jest.Mocked<UserSubscription>
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
settingProjector = {} as jest.Mocked<SettingProjector>
settingProjector.projectManySimple = jest.fn().mockReturnValue([{ foo: 'bar' }])
settingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' })
subscriptionSettingProjector = {} as jest.Mocked<SubscriptionSettingProjector>
subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' })
user = {} as jest.Mocked<User>
@@ -51,83 +99,126 @@ describe('GetSettings', () => {
crypter.decryptForUser = jest.fn().mockReturnValue('decrypted')
})
it('should fail if a user is not found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
describe('no subscription', () => {
it('should fail if a user is not found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: false,
error: {
message: 'User 1-2-3 not found.',
},
})
})
it('should return all user settings except mfa', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: false,
error: {
message: 'User 1-2-3 not found.',
},
})
})
expect(settingProjector.projectManySimple).toHaveBeenCalledWith([setting])
})
it('should return all user settings except mfa', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
})
it('should return all setting with decrypted values', async () => {
setting = {
name: 'test',
updatedAt: 345,
value: 'encrypted',
serverEncryptionVersion: EncryptionVersion.Default,
} as jest.Mocked<Setting>
settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting])
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting)
expect(subscriptionSettingProjector.projectSimple).not.toHaveBeenCalled()
})
expect(settingProjector.projectManySimple).toHaveBeenCalledWith([
{
it('should return all setting with decrypted values', async () => {
setting = {
name: 'test',
updatedAt: 345,
value: 'encrypted',
serverEncryptionVersion: EncryptionVersion.Default,
} as jest.Mocked<Setting>
settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting])
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
})
expect(settingProjector.projectSimple).toHaveBeenCalledWith({
name: 'test',
updatedAt: 345,
value: 'decrypted',
serverEncryptionVersion: 1,
},
])
})
it('should return all user settings of certain name', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test', allowSensitiveRetrieval: true }),
).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
})
})
expect(settingProjector.projectManySimple).toHaveBeenCalledWith([setting])
})
it('should return all user settings of certain name', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test', allowSensitiveRetrieval: true }),
).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
})
it('should return all user settings updated after', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true, updatedAfter: 123 }),
).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting)
})
expect(settingProjector.projectManySimple).toHaveBeenCalledWith([setting])
})
it('should return all user settings updated after', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true, updatedAfter: 123 }),
).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
})
it('should return all sensitive user settings if explicit', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting)
})
expect(settingProjector.projectManySimple).toHaveBeenCalledWith([setting, mfaSetting])
it('should return all sensitive user settings if explicit', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }, { foo: 'bar' }],
})
expect(settingProjector.projectSimple).toHaveBeenCalledTimes(2)
expect(settingProjector.projectSimple).toHaveBeenNthCalledWith(1, setting)
expect(settingProjector.projectSimple).toHaveBeenNthCalledWith(2, mfaSetting)
})
})
describe('regular subscription', () => {
beforeEach(() => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription: null })
})
it('should return all user settings except mfa', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }, { foo: 'sub-bar' }],
})
expect(subscriptionSettingRepository.findAllBySubscriptionUuid).toHaveBeenCalledWith('1-2-3')
expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting)
expect(subscriptionSettingProjector.projectSimple).toHaveBeenCalledWith(signInEmailsSetting)
})
})
describe('shared subscription', () => {
beforeEach(() => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription })
})
it('should return all user settings except mfa', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }, { foo: 'sub-bar' }],
})
expect(subscriptionSettingRepository.findAllBySubscriptionUuid).toHaveBeenCalledWith('2-3-4')
expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting)
expect(subscriptionSettingProjector.projectSimple).toHaveBeenCalledWith(signInEmailsSetting)
})
})
})

View File

@@ -9,12 +9,22 @@ import { Setting } from '../../Setting/Setting'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { CrypterInterface } from '../../Encryption/CrypterInterface'
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface'
import { SubscriptionSetting } from '../../Setting/SubscriptionSetting'
import { SimpleSetting } from '../../Setting/SimpleSetting'
import { SimpleSubscriptionSetting } from '../../Setting/SimpleSubscriptionSetting'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
@injectable()
export class GetSettings implements UseCaseInterface {
constructor(
@inject(TYPES.SettingRepository) private settingRepository: SettingRepositoryInterface,
@inject(TYPES.SubscriptionSettingRepository)
private subscriptionSettingRepository: SubscriptionSettingRepositoryInterface,
@inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
@inject(TYPES.SettingProjector) private settingProjector: SettingProjector,
@inject(TYPES.SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Crypter) private crypter: CrypterInterface,
) {}
@@ -33,27 +43,43 @@ export class GetSettings implements UseCaseInterface {
}
}
let settings = await this.settingRepository.findAllByUserUuid(userUuid)
let settings: Array<Setting | SubscriptionSetting>
settings = await this.settingRepository.findAllByUserUuid(userUuid)
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid)
const subscription = sharedSubscription ?? regularSubscription
if (subscription) {
const subscriptionSettings = await this.subscriptionSettingRepository.findAllBySubscriptionUuid(subscription.uuid)
settings = settings.concat(subscriptionSettings)
}
if (dto.settingName !== undefined) {
settings = settings.filter((setting: Setting) => setting.name === dto.settingName)
settings = settings.filter((setting: Setting | SubscriptionSetting) => setting.name === dto.settingName)
}
if (dto.updatedAfter !== undefined) {
settings = settings.filter((setting: Setting) => setting.updatedAt >= (dto.updatedAfter as number))
settings = settings.filter(
(setting: Setting | SubscriptionSetting) => setting.updatedAt >= (dto.updatedAfter as number),
)
}
if (!dto.allowSensitiveRetrieval) {
settings = settings.filter((setting: Setting) => !setting.sensitive)
settings = settings.filter((setting: Setting | SubscriptionSetting) => !setting.sensitive)
}
const simpleSettings: Array<SimpleSetting | SimpleSubscriptionSetting> = []
for (const setting of settings) {
if (setting.value !== null && setting.serverEncryptionVersion === EncryptionVersion.Default) {
setting.value = await this.crypter.decryptForUser(setting.value, user)
}
}
const simpleSettings = await this.settingProjector.projectManySimple(settings)
if (setting instanceof SubscriptionSetting) {
simpleSettings.push(await this.subscriptionSettingProjector.projectSimple(setting))
} else {
simpleSettings.push(await this.settingProjector.projectSimple(setting))
}
}
return {
success: true,

View File

@@ -1,10 +1,11 @@
import { SimpleSetting } from '../../Setting/SimpleSetting'
import { SimpleSubscriptionSetting } from '../../Setting/SimpleSubscriptionSetting'
export type GetSettingsResponse =
| {
success: true
userUuid: string
settings: SimpleSetting[]
settings: Array<SimpleSetting | SimpleSubscriptionSetting>
}
| {
success: false

View File

@@ -1,141 +0,0 @@
import 'reflect-metadata'
import { SubscriptionSettingName } from '@standardnotes/settings'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
import { SubscriptionSetting } from '../../Setting/SubscriptionSetting'
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { GetSubscriptionSetting } from './GetSubscriptionSetting'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
import { User } from '../../User/User'
describe('GetSubscriptionSetting', () => {
let userSubscriptionService: UserSubscriptionServiceInterface
let subscriptionSettingService: SubscriptionSettingServiceInterface
let subscriptionSettingProjector: SubscriptionSettingProjector
let subscriptionSetting: SubscriptionSetting
let regularSubscription: UserSubscription
let user: User
const createUseCase = () =>
new GetSubscriptionSetting(userSubscriptionService, subscriptionSettingService, subscriptionSettingProjector)
beforeEach(() => {
subscriptionSetting = {} as jest.Mocked<SubscriptionSetting>
user = {
uuid: '1-2-3',
} as jest.Mocked<User>
regularSubscription = {
uuid: '1-2-3',
subscriptionType: UserSubscriptionType.Regular,
user: Promise.resolve(user),
} as jest.Mocked<UserSubscription>
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription: null })
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest
.fn()
.mockReturnValue(subscriptionSetting)
subscriptionSettingProjector = {} as jest.Mocked<SubscriptionSettingProjector>
subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' })
})
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({
userUuid: '1-2-3',
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesUsed,
}),
).toEqual({
success: true,
setting: { foo: 'bar' },
})
})
it('should not get a setting for user if user has no corresponding regular subscription', async () => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
expect(
await createUseCase().execute({
userUuid: '1-2-3',
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit,
}),
).toEqual({
success: false,
error: {
message: 'No subscription found.',
},
})
})
it('should not get a setting for user if it does not exist', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({
userUuid: '1-2-3',
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit,
}),
).toEqual({
success: false,
error: {
message: 'Setting FILE_UPLOAD_BYTES_LIMIT for user 1-2-3 not found!',
},
})
})
it('should not retrieve a sensitive setting for user', async () => {
subscriptionSetting = {
sensitive: true,
name: SubscriptionSettingName.FileUploadBytesLimit,
} as jest.Mocked<SubscriptionSetting>
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest
.fn()
.mockReturnValue(subscriptionSetting)
expect(
await createUseCase().execute({
userUuid: '1-2-3',
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit,
}),
).toEqual({
success: true,
sensitive: true,
})
})
it('should retrieve a sensitive setting for user if explicitly told to', async () => {
subscriptionSetting = {
sensitive: true,
name: SubscriptionSettingName.FileUploadBytesLimit,
} as jest.Mocked<SubscriptionSetting>
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest
.fn()
.mockReturnValue(subscriptionSetting)
expect(
await createUseCase().execute({
userUuid: '1-2-3',
subscriptionSettingName: SubscriptionSettingName.FileUploadBytesLimit,
allowSensitiveRetrieval: true,
}),
).toEqual({
success: true,
setting: { foo: 'bar' },
})
})
})

View File

@@ -1,61 +0,0 @@
import { inject, injectable } from 'inversify'
import { GetSubscriptionSettingDTO } from './GetSubscriptionSettingDTO'
import { GetSubscriptionSettingResponse } from './GetSubscriptionSettingResponse'
import { UseCaseInterface } from '../UseCaseInterface'
import TYPES from '../../../Bootstrap/Types'
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
@injectable()
export class GetSubscriptionSetting implements UseCaseInterface {
constructor(
@inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
@inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface,
@inject(TYPES.SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
) {}
async execute(dto: GetSubscriptionSettingDTO): Promise<GetSubscriptionSettingResponse> {
const { regularSubscription } = await this.userSubscriptionService.findRegularSubscriptionForUserUuid(dto.userUuid)
if (regularSubscription === null) {
return {
success: false,
error: {
message: 'No subscription found.',
},
}
}
const regularSubscriptionUser = await regularSubscription.user
const setting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: regularSubscriptionUser.uuid,
userSubscriptionUuid: regularSubscription.uuid,
subscriptionSettingName: dto.subscriptionSettingName,
})
if (setting === null) {
return {
success: false,
error: {
message: `Setting ${dto.subscriptionSettingName} for user ${dto.userUuid} not found!`,
},
}
}
if (setting.sensitive && !dto.allowSensitiveRetrieval) {
return {
success: true,
sensitive: true,
}
}
const simpleSetting = await this.subscriptionSettingProjector.projectSimple(setting)
return {
success: true,
setting: simpleSetting,
}
}
}

View File

@@ -1,7 +0,0 @@
import { SubscriptionSettingName } from '@standardnotes/settings'
export type GetSubscriptionSettingDTO = {
userUuid: string
subscriptionSettingName: SubscriptionSettingName
allowSensitiveRetrieval?: boolean
}

View File

@@ -1,17 +0,0 @@
import { SimpleSetting } from '../../Setting/SimpleSetting'
export type GetSubscriptionSettingResponse =
| {
success: true
setting: SimpleSetting
}
| {
success: true
sensitive: true
}
| {
success: false
error: {
message: string
}
}

View File

@@ -40,7 +40,7 @@ export class GetUserKeyParamsRecovery implements UseCaseInterface<KeyParamsData>
}
const recoveryCodesSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.RecoveryCodes,
settingName: SettingName.create(SettingName.NAMES.RecoveryCodes).getValue(),
userUuid: user.uuid,
})
if (!recoveryCodesSetting) {

View File

@@ -25,7 +25,7 @@ describe('RefreshSessionToken', () => {
session.refreshExpiration = new Date(123)
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.isRefreshTokenValid = jest.fn().mockReturnValue(true)
sessionService.isRefreshTokenMatchingHashedSessionToken = jest.fn().mockReturnValue(true)
sessionService.getSessionFromToken = jest.fn().mockReturnValue(session)
sessionService.refreshTokens = jest.fn().mockReturnValue({
access_token: 'token1',
@@ -105,7 +105,7 @@ describe('RefreshSessionToken', () => {
})
it('should not refresh a session token if refresh token is not valid', async () => {
sessionService.isRefreshTokenValid = jest.fn().mockReturnValue(false)
sessionService.isRefreshTokenMatchingHashedSessionToken = jest.fn().mockReturnValue(false)
const result = await createUseCase().execute({
accessToken: '123',

View File

@@ -30,7 +30,7 @@ export class RefreshSessionToken {
}
}
if (!this.sessionService.isRefreshTokenValid(session, dto.refreshToken)) {
if (!this.sessionService.isRefreshTokenMatchingHashedSessionToken(session, dto.refreshToken)) {
return {
success: false,
errorTag: 'invalid-refresh-token',

View File

@@ -83,7 +83,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
}
const recoveryCodesSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.RecoveryCodes,
settingName: SettingName.create(SettingName.NAMES.RecoveryCodes).getValue(),
userUuid: user.uuid,
})
if (!recoveryCodesSetting) {
@@ -116,7 +116,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
}
await this.deleteSetting.execute({
settingName: SettingName.MfaSecret,
settingName: SettingName.NAMES.MfaSecret,
userUuid: user.uuid,
})

View File

@@ -14,6 +14,11 @@ import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UpdateSetting } from './UpdateSetting'
import { SettingName } from '@standardnotes/settings'
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
import { UserSubscription } from '../../Subscription/UserSubscription'
describe('UpdateSetting', () => {
let settingService: SettingServiceInterface
@@ -25,9 +30,24 @@ describe('UpdateSetting', () => {
let userRepository: UserRepositoryInterface
let roleService: RoleServiceInterface
let logger: Logger
let userSubscriptionService: UserSubscriptionServiceInterface
let subscriptionSettingProjector: SubscriptionSettingProjector
let subscriptionSettingService: SubscriptionSettingServiceInterface
let regularSubscription: UserSubscription
let sharedSubscription: UserSubscription
const createUseCase = () =>
new UpdateSetting(settingService, settingProjector, settingsAssociationService, userRepository, roleService, logger)
new UpdateSetting(
settingService,
subscriptionSettingService,
userSubscriptionService,
settingProjector,
subscriptionSettingProjector,
settingsAssociationService,
userRepository,
roleService,
logger,
)
beforeEach(() => {
setting = {} as jest.Mocked<Setting>
@@ -35,9 +55,32 @@ describe('UpdateSetting', () => {
settingService = {} as jest.Mocked<SettingServiceInterface>
settingService.createOrReplace = jest.fn().mockReturnValue({ status: 'created', setting })
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
subscriptionSettingService.createOrReplace = jest.fn().mockReturnValue({ status: 'created', setting })
settingProjector = {} as jest.Mocked<SettingProjector>
settingProjector.projectSimple = jest.fn().mockReturnValue(settingProjection)
regularSubscription = {
uuid: '1-2-3',
subscriptionType: UserSubscriptionType.Regular,
user: Promise.resolve(user),
} as jest.Mocked<UserSubscription>
sharedSubscription = {
uuid: '2-3-4',
subscriptionType: UserSubscriptionType.Shared,
user: Promise.resolve(user),
} as jest.Mocked<UserSubscription>
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
subscriptionSettingProjector = {} as jest.Mocked<SubscriptionSettingProjector>
subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' })
user = {} as jest.Mocked<User>
userRepository = {} as jest.Mocked<UserRepositoryInterface>
@@ -57,124 +100,217 @@ describe('UpdateSetting', () => {
logger.error = jest.fn()
})
it('should create a setting', async () => {
const props = {
name: SettingName.ExtensionKey,
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: false,
}
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
expect(settingService.createOrReplace).toHaveBeenCalledWith({
props: {
name: 'EXTENSION_KEY',
describe('no subscription', () => {
it('should create a setting', async () => {
const props = {
name: SettingName.NAMES.ExtensionKey,
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: 1,
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: false,
},
user,
}
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
expect(settingService.createOrReplace).toHaveBeenCalledWith({
props: {
name: 'EXTENSION_KEY',
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: 1,
sensitive: false,
},
user,
})
expect(response).toEqual({
success: true,
setting: settingProjection,
statusCode: 201,
})
})
expect(response).toEqual({
success: true,
setting: settingProjection,
statusCode: 201,
it('should not create a setting if user does not exist', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const props = {
name: SettingName.NAMES.ExtensionKey,
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Unencrypted,
sensitive: false,
}
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
expect(settingService.createOrReplace).not.toHaveBeenCalled()
expect(response).toEqual({
success: false,
error: {
message: 'User 1-2-3 not found.',
},
statusCode: 404,
})
})
it('should not create a subscription setting', async () => {
const props = {
name: SettingName.NAMES.MuteSignInEmails,
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Unencrypted,
sensitive: false,
}
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
expect(settingService.createOrReplace).not.toHaveBeenCalled()
expect(response).toEqual({
success: false,
error: {
message: 'User 1-2-3 has no subscription to change a subscription setting.',
},
statusCode: 401,
})
})
it('should not create a setting if the setting name is invalid', async () => {
const props = {
name: 'random-setting',
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Unencrypted,
sensitive: false,
}
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
expect(settingService.createOrReplace).not.toHaveBeenCalled()
expect(response).toEqual({
success: false,
error: {
message: 'Invalid setting name: random-setting',
},
statusCode: 400,
})
})
it('should not create a setting if user is not permitted to', async () => {
settingsAssociationService.getPermissionAssociatedWithSetting = jest
.fn()
.mockReturnValue(PermissionName.DailyEmailBackup)
roleService.userHasPermission = jest.fn().mockReturnValue(false)
const props = {
name: SettingName.NAMES.ExtensionKey,
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Unencrypted,
sensitive: false,
}
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
expect(settingService.createOrReplace).not.toHaveBeenCalled()
expect(response).toEqual({
success: false,
error: {
message: 'User 1-2-3 is not permitted to change the setting.',
},
statusCode: 401,
})
})
it('should not create a setting if setting is not mutable by the client', async () => {
settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(false)
const props = {
name: SettingName.NAMES.ExtensionKey,
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Unencrypted,
sensitive: false,
}
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
expect(settingService.createOrReplace).not.toHaveBeenCalled()
expect(response).toEqual({
success: false,
error: {
message: 'User 1-2-3 is not permitted to change the setting.',
},
statusCode: 401,
})
})
})
it('should not create a setting if user does not exist', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
describe('regular subscription', () => {
beforeEach(() => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription: null })
})
const props = {
name: SettingName.ExtensionKey,
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Unencrypted,
sensitive: false,
}
it('should create a subscription setting', async () => {
const props = {
name: SettingName.NAMES.MuteSignInEmails,
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: false,
}
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
expect(settingService.createOrReplace).not.toHaveBeenCalled()
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
props: {
name: 'MUTE_SIGN_IN_EMAILS',
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: 1,
sensitive: false,
},
userSubscription: regularSubscription,
})
expect(response).toEqual({
success: false,
error: {
message: 'User 1-2-3 not found.',
},
statusCode: 404,
expect(response).toEqual({
success: true,
setting: { foo: 'sub-bar' },
statusCode: 201,
})
})
})
it('should not create a setting if the setting name is invalid', async () => {
const props = {
name: 'random-setting',
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Unencrypted,
sensitive: false,
}
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
expect(settingService.createOrReplace).not.toHaveBeenCalled()
expect(response).toEqual({
success: false,
error: {
message: 'Setting name random-setting is invalid.',
},
statusCode: 400,
describe('shared subscription', () => {
beforeEach(() => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription })
})
})
it('should not create a setting if user is not permitted to', async () => {
settingsAssociationService.getPermissionAssociatedWithSetting = jest
.fn()
.mockReturnValue(PermissionName.DailyEmailBackup)
it('should create a subscription setting', async () => {
const props = {
name: SettingName.NAMES.MuteSignInEmails,
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: false,
}
roleService.userHasPermission = jest.fn().mockReturnValue(false)
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
const props = {
name: SettingName.ExtensionKey,
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Unencrypted,
sensitive: false,
}
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
props: {
name: 'MUTE_SIGN_IN_EMAILS',
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: 1,
sensitive: false,
},
userSubscription: sharedSubscription,
})
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
expect(settingService.createOrReplace).not.toHaveBeenCalled()
expect(response).toEqual({
success: false,
error: {
message: 'User 1-2-3 is not permitted to change the setting.',
},
statusCode: 401,
})
})
it('should not create a setting if setting is not mutable by the client', async () => {
settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(false)
const props = {
name: SettingName.ExtensionKey,
unencryptedValue: 'test-setting-value',
serverEncryptionVersion: EncryptionVersion.Unencrypted,
sensitive: false,
}
const response = await createUseCase().execute({ props, userUuid: '1-2-3' })
expect(settingService.createOrReplace).not.toHaveBeenCalled()
expect(response).toEqual({
success: false,
error: {
message: 'User 1-2-3 is not permitted to change the setting.',
},
statusCode: 401,
expect(response).toEqual({
success: true,
setting: { foo: 'sub-bar' },
statusCode: 201,
})
})
})
})

View File

@@ -12,12 +12,19 @@ import { User } from '../../User/User'
import { SettingName } from '@standardnotes/settings'
import { RoleServiceInterface } from '../../Role/RoleServiceInterface'
import { SettingsAssociationServiceInterface } from '../../Setting/SettingsAssociationServiceInterface'
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { CreateOrReplaceSubscriptionSettingResponse } from '../../Setting/CreateOrReplaceSubscriptionSettingResponse'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
@injectable()
export class UpdateSetting implements UseCaseInterface {
constructor(
@inject(TYPES.SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface,
@inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
@inject(TYPES.SettingProjector) private settingProjector: SettingProjector,
@inject(TYPES.SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
@inject(TYPES.SettingsAssociationService) private settingsAssociationService: SettingsAssociationServiceInterface,
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.RoleService) private roleService: RoleServiceInterface,
@@ -25,15 +32,17 @@ export class UpdateSetting implements UseCaseInterface {
) {}
async execute(dto: UpdateSettingDto): Promise<UpdateSettingResponse> {
if (!Object.values(SettingName).includes(dto.props.name as SettingName)) {
const settingNameOrError = SettingName.create(dto.props.name)
if (settingNameOrError.isFailed()) {
return {
success: false,
error: {
message: `Setting name ${dto.props.name} is invalid.`,
message: settingNameOrError.getError(),
},
statusCode: 400,
}
}
const settingName = settingNameOrError.getValue()
this.logger.debug('[%s] Updating setting: %O', dto.userUuid, dto)
@@ -51,7 +60,7 @@ export class UpdateSetting implements UseCaseInterface {
}
}
if (!(await this.userHasPermissionToUpdateSetting(user, props.name as SettingName))) {
if (!(await this.userHasPermissionToUpdateSetting(user, settingName))) {
return {
success: false,
error: {
@@ -61,10 +70,34 @@ export class UpdateSetting implements UseCaseInterface {
}
}
props.serverEncryptionVersion = this.settingsAssociationService.getEncryptionVersionForSetting(
props.name as SettingName,
)
props.sensitive = this.settingsAssociationService.getSensitivityForSetting(props.name as SettingName)
props.serverEncryptionVersion = this.settingsAssociationService.getEncryptionVersionForSetting(settingName)
props.sensitive = this.settingsAssociationService.getSensitivityForSetting(settingName)
if (settingName.isASubscriptionSetting()) {
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid)
const subscription = sharedSubscription ?? regularSubscription
if (!subscription) {
return {
success: false,
error: {
message: `User ${userUuid} has no subscription to change a subscription setting.`,
},
statusCode: 401,
}
}
const response = await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription,
props,
})
return {
success: true,
setting: await this.subscriptionSettingProjector.projectSimple(response.subscriptionSetting),
statusCode: this.statusToStatusCode(response),
}
}
const response = await this.settingService.createOrReplace({
user,
@@ -79,7 +112,9 @@ export class UpdateSetting implements UseCaseInterface {
}
/* istanbul ignore next */
private statusToStatusCode(response: CreateOrReplaceSettingResponse): number {
private statusToStatusCode(
response: CreateOrReplaceSettingResponse | CreateOrReplaceSubscriptionSettingResponse,
): number {
if (response.status === 'created') {
return 201
}
@@ -92,7 +127,7 @@ export class UpdateSetting implements UseCaseInterface {
}
private async userHasPermissionToUpdateSetting(user: User, settingName: SettingName): Promise<boolean> {
const settingIsMutableByClient = await this.settingsAssociationService.isSettingMutableByClient(settingName)
const settingIsMutableByClient = this.settingsAssociationService.isSettingMutableByClient(settingName)
if (!settingIsMutableByClient) {
return false
}

View File

@@ -24,7 +24,6 @@ describe('VerifyAuthenticatorAuthenticationResponse', () => {
beforeEach(() => {
const authenticator = Authenticator.create({
counter: 1,
name: 'my-key',
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialId: Buffer.from('credentialId'),

View File

@@ -38,7 +38,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: 'invalid',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
@@ -56,27 +55,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
)
})
it('should return error if name is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: '',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
response: {
attestationObject: Buffer.from('attestationObject'),
clientDataJSON: Buffer.from('clientDataJSON'),
},
type: 'type',
},
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not verify authenticator registration response: Given value is empty: ')
})
it('should return error if challenge is not found', async () => {
authenticatorChallengeRepository.findByUserUuid = jest.fn().mockReturnValue(null)
@@ -84,7 +62,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
@@ -125,7 +102,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
@@ -159,7 +135,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
@@ -195,7 +170,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
@@ -245,7 +219,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
@@ -289,7 +262,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),

View File

@@ -1,4 +1,4 @@
import { Dates, Result, UseCaseInterface, Uuid, Validator } from '@standardnotes/domain-core'
import { Dates, Result, UniqueEntityId, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { VerifiedRegistrationResponse, verifyRegistrationResponse } from '@simplewebauthn/server'
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
@@ -6,7 +6,7 @@ import { AuthenticatorRepositoryInterface } from '../../Authenticator/Authentica
import { Authenticator } from '../../Authenticator/Authenticator'
import { VerifyAuthenticatorRegistrationResponseDTO } from './VerifyAuthenticatorRegistrationResponseDTO'
export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface<boolean> {
export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface<UniqueEntityId> {
constructor(
private authenticatorRepository: AuthenticatorRepositoryInterface,
private authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface,
@@ -15,18 +15,13 @@ export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface
private requireUserVerification: boolean,
) {}
async execute(dto: VerifyAuthenticatorRegistrationResponseDTO): Promise<Result<boolean>> {
async execute(dto: VerifyAuthenticatorRegistrationResponseDTO): Promise<Result<UniqueEntityId>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(`Could not verify authenticator registration response: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const nameValidation = Validator.isNotEmpty(dto.name)
if (nameValidation.isFailed()) {
return Result.fail(`Could not verify authenticator registration response: ${nameValidation.getError()}`)
}
const authenticatorChallenge = await this.authenticatorChallengeRepository.findByUserUuid(userUuid)
if (!authenticatorChallenge) {
return Result.fail('Could not verify authenticator registration response: challenge not found')
@@ -55,7 +50,6 @@ export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface
const authenticatorOrError = Authenticator.create({
userUuid,
name: dto.name,
counter: verification.registrationInfo.counter,
credentialBackedUp: verification.registrationInfo.credentialBackedUp,
credentialDeviceType: verification.registrationInfo.credentialDeviceType,
@@ -71,6 +65,6 @@ export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface
await this.authenticatorRepository.save(authenticator)
return Result.ok(true)
return Result.ok(authenticator.id)
}
}

View File

@@ -1,5 +1,4 @@
export interface VerifyAuthenticatorRegistrationResponseDTO {
userUuid: string
name: string
attestationResponse: Record<string, unknown>
}

View File

@@ -55,7 +55,7 @@ describe('VerifyMFA', () => {
lockRepository.lockSuccessfullOTP = jest.fn()
setting = {
name: SettingName.MfaSecret,
name: SettingName.NAMES.MfaSecret,
value: 'shhhh',
} as jest.Mocked<Setting>
@@ -87,7 +87,7 @@ describe('VerifyMFA', () => {
it('should pass MFA verification if user has MFA deleted', async () => {
setting = {
name: SettingName.MfaSecret,
name: SettingName.NAMES.MfaSecret,
value: null,
} as jest.Mocked<Setting>
@@ -177,7 +177,7 @@ describe('VerifyMFA', () => {
it('should not pass MFA verification if mfa is not correct', async () => {
setting = {
name: SettingName.MfaSecret,
name: SettingName.NAMES.MfaSecret,
value: 'shhhh2',
} as jest.Mocked<Setting>

View File

@@ -90,7 +90,7 @@ export class VerifyMFA implements UseCaseInterface {
const mfaSecret = await this.settingService.findSettingWithDecryptedValue({
userUuid: user.uuid,
settingName: SettingName.MfaSecret,
settingName: SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
})
const twoFactorEnabled = mfaSecret !== null && mfaSecret.value !== null

View File

@@ -39,7 +39,10 @@ export class VerifyPredicate implements UseCaseInterface {
}
private async hasUserEnabledEmailBackups(userUuid: string): Promise<boolean> {
const setting = await this.settingRepository.findOneByNameAndUserUuid(SettingName.EmailBackupFrequency, userUuid)
const setting = await this.settingRepository.findOneByNameAndUserUuid(
SettingName.NAMES.EmailBackupFrequency,
userUuid,
)
if (setting === null || setting.value === EmailBackupFrequency.Disabled) {
return false

View File

@@ -1,4 +1,3 @@
export interface AuthenticatorHttpProjection {
id: string
name: string
}

View File

@@ -1,5 +1,4 @@
export interface VerifyAuthenticatorRegistrationResponseRequestParams {
userUuid: string
name: string
attestationResponse: Record<string, unknown>
}

View File

@@ -1,3 +1,3 @@
export interface VerifyAuthenticatorRegistrationResponseResponseBody {
success: boolean
id: string
}

View File

@@ -52,7 +52,6 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
const result = await this.authenticatorsController.verifyRegistrationResponse({
userUuid: response.locals.user.uuid,
attestationResponse: request.body.attestationResponse,
name: request.body.name,
})
return this.json(result.data, result.status)

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