Compare commits

..

41 Commits

Author SHA1 Message Date
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
standardci
16d0ed505b chore(release): publish new version
- @standardnotes/auth-server@1.89.5
 - @standardnotes/revisions-server@1.12.7
 - @standardnotes/syncing-server@1.31.8
 - @standardnotes/websockets-server@1.6.6
2023-03-01 20:05:36 +00:00
Mo
9de09c55f8 chore: upgrade api and responses dependencies (#459) 2023-03-01 13:51:26 -06:00
standardci
c3d7a33aa2 chore(release): publish new version
- @standardnotes/auth-server@1.89.4
2023-03-01 12:01:59 +00:00
Karol Sójko
a9cc00a478 fix(auth): updating counter post authenticator verification 2023-03-01 12:48:10 +01:00
standardci
ec035ba648 chore(release): publish new version
- @standardnotes/auth-server@1.89.3
2023-02-27 10:59:56 +00:00
Karol Sójko
5446f3cae4 chore: upgrade @simplewebauthn lib 2023-02-27 11:46:00 +01:00
standardci
6a550092c2 chore(release): publish new version
- @standardnotes/api-gateway@1.49.4
2023-02-25 15:14:24 +00:00
Mo
1b691f6bcd refactor: remove explicit route since subscription token no longer required 2023-02-25 09:00:07 -06:00
standardci
98f45cc4c2 chore(release): publish new version
- @standardnotes/auth-server@1.89.2
2023-02-24 10:59:33 +00:00
Karol Sójko
edc4a20859 fix(auth): add cross-platform authenticator selection option 2023-02-24 11:45:33 +01:00
standardci
74e1380df8 chore(release): publish new version
- @standardnotes/api-gateway@1.49.3
2023-02-24 07:35:22 +00:00
Karol Sójko
dfa5187ff7 fix(api-gateywa): remove stale proxy references 2023-02-24 08:21:22 +01:00
Karol Sójko
c99c4425cd chore: update yarn lock 2023-02-24 08:14:33 +01:00
Karol Sójko
2d8919a079 chore: remove proxy service 2023-02-24 08:14:17 +01:00
standardci
f638287213 chore(release): publish new version
- @standardnotes/analytics@2.21.1
 - @standardnotes/api-gateway@1.49.2
 - @standardnotes/auth-server@1.89.1
 - @standardnotes/common@1.46.6
 - @standardnotes/domain-events-infra@1.9.72
 - @standardnotes/domain-events@2.108.1
 - @standardnotes/event-store@1.7.4
 - @standardnotes/files-server@1.10.5
 - @standardnotes/revisions-server@1.12.6
 - @standardnotes/scheduler-server@1.17.5
 - @standardnotes/syncing-server@1.31.7
 - @standardnotes/websockets-server@1.6.5
2023-02-23 13:01:18 +00:00
Karol Sójko
991d885b63 chore: fix faulty import 2023-02-23 13:47:06 +01:00
Karol Sójko
bb17efa817 chore: fix yarn lock 2023-02-23 13:35:06 +01:00
Karol Sójko
deec29c1b4 chore: remove workspaces from code base 2023-02-23 13:30:39 +01:00
Karol Sójko
9d872008a7 fix(analytics): add general activity metric to mixpanel 2023-02-23 13:21:12 +01:00
280 changed files with 2089 additions and 5534 deletions

View File

@@ -95,11 +95,6 @@ updates:
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/packages/workspace"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:

View File

@@ -94,81 +94,3 @@ jobs:
name: E2E
uses: standardnotes/server/.github/workflows/common-e2e.yml@main
secrets: inherit
legacy_e2e:
needs: build
name: Legacy E2E
strategy:
matrix:
application:
- { "service_name": "api-gateway", "workspace_name": "@standardnotes/api-gateway", "e2e_tag_parameter_name": "api_gateway_image_tag", "package_path": "packages/api-gateway" }
- { "service_name": "auth", "workspace_name": "@standardnotes/auth-server", "e2e_tag_parameter_name": "auth_image_tag", "package_path": "packages/auth" }
- { "service_name": "files", "workspace_name": "@standardnotes/files-server", "e2e_tag_parameter_name": "files_image_tag", "package_path": "packages/files" }
- { "service_name": "revisions", "workspace_name": "@standardnotes/revisions-server", "e2e_tag_parameter_name": "revisions_image_tag", "package_path": "packages/revisions"}
- { "service_name": "syncing-server-js", "workspace_name": "@standardnotes/syncing-server", "e2e_tag_parameter_name": "syncing_server_js_image_tag", "package_path": "packages/syncing-server" }
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create Bundle Dir
id: bundle-dir
run: echo "temp_dir=$(mktemp -d -t ${{ matrix.application.service_name }}-${{ github.sha }}-XXXXXXX)" >> $GITHUB_OUTPUT
- name: Cache build
id: cache-build
uses: actions/cache@v3
with:
path: |
packages/**/dist
${{ needs.legacy_e2e.outputs.temp_dir }}
key: ${{ runner.os }}-build-${{ github.sha }}
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build
if: steps.cache-build.outputs.cache-hit != 'true'
run: yarn build
- name: Bundle
run: yarn workspace ${{ matrix.application.workspace_name }} bundle --no-compress --output-directory ${{ steps.bundle-dir.outputs.temp_dir }}
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
- name: Publish Docker image for E2E testing
uses: docker/build-push-action@v3
with:
builder: ${{ steps.buildx.outputs.name }}
context: ${{ steps.bundle-dir.outputs.temp_dir }}
file: ${{ steps.bundle-dir.outputs.temp_dir }}/${{ matrix.application.package_path }}/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: standardnotes/${{ matrix.application.service_name }}:${{ github.sha }}
- name: Run E2E test suite
uses: convictional/trigger-workflow-and-wait@master
with:
owner: standardnotes
repo: self-hosted
github_token: ${{ secrets.CI_PAT_TOKEN }}
workflow_file_name: testing-with-updating-client-and-server.yml
wait_interval: 30
client_payload: '{"${{ matrix.application.e2e_tag_parameter_name }}": "${{ github.sha }}"}'
propagate_failure: true
trigger_workflow: true
wait_workflow: true

View File

@@ -1,22 +0,0 @@
name: Proxy Server
concurrency:
group: proxy_server
cancel-in-progress: true
on:
push:
tags:
- '*standardnotes/proxy-server*'
workflow_dispatch:
jobs:
call_server_application_workflow:
name: Server Application
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
with:
service_name: proxy
workspace_name: "@standardnotes/proxy-server"
deploy_worker: false
package_path: packages/proxy
secrets: inherit

View File

@@ -1,47 +0,0 @@
name: Workspace Server
concurrency:
group: workspace
cancel-in-progress: true
on:
push:
tags:
- '*standardnotes/workspace-server*'
workflow_dispatch:
jobs:
call_server_application_workflow:
name: Server Application
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
with:
service_name: workspace
workspace_name: "@standardnotes/workspace-server"
package_path: packages/workspace
secrets: inherit
newrelic:
needs: call_server_application_workflow
runs-on: ubuntu-latest
steps:
- name: Create New Relic deployment marker for Web
uses: newrelic/deployment-marker-action@v1
with:
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_WORKSPACE_WEB_PROD }}
revision: "${{ github.sha }}"
description: "Automated Deployment via Github Actions"
user: "${{ github.actor }}"
- name: Create New Relic deployment marker for Worker
uses: newrelic/deployment-marker-action@v1
with:
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_WORKSPACE_WORKER_PROD }}
revision: "${{ github.sha }}"
description: "Automated Deployment via Github Actions"
user: "${{ github.actor }}"

186
.pnp.cjs generated
View File

@@ -53,10 +53,6 @@ const RAW_RUNTIME_STATE =
"name": "@standardnotes/predicates",\
"reference": "workspace:packages/predicates"\
},\
{\
"name": "@standardnotes/proxy-server",\
"reference": "workspace:packages/proxy"\
},\
{\
"name": "@standardnotes/revisions-server",\
"reference": "workspace:packages/revisions"\
@@ -88,10 +84,6 @@ const RAW_RUNTIME_STATE =
{\
"name": "@standardnotes/websockets-server",\
"reference": "workspace:packages/websockets"\
},\
{\
"name": "@standardnotes/workspace-server",\
"reference": "workspace:packages/workspace"\
}\
],\
"enableTopLevelFallback": true,\
@@ -107,7 +99,6 @@ const RAW_RUNTIME_STATE =
["@standardnotes/event-store", ["workspace:packages/event-store"]],\
["@standardnotes/files-server", ["workspace:packages/files"]],\
["@standardnotes/predicates", ["workspace:packages/predicates"]],\
["@standardnotes/proxy-server", ["workspace:packages/proxy"]],\
["@standardnotes/revisions-server", ["workspace:packages/revisions"]],\
["@standardnotes/scheduler-server", ["workspace:packages/scheduler"]],\
["@standardnotes/security", ["workspace:packages/security"]],\
@@ -116,8 +107,7 @@ const RAW_RUNTIME_STATE =
["@standardnotes/sncrypto-node", ["workspace:packages/sncrypto-node"]],\
["@standardnotes/syncing-server", ["workspace:packages/syncing-server"]],\
["@standardnotes/time", ["workspace:packages/time"]],\
["@standardnotes/websockets-server", ["workspace:packages/websockets"]],\
["@standardnotes/workspace-server", ["workspace:packages/workspace"]]\
["@standardnotes/websockets-server", ["workspace:packages/websockets"]]\
],\
"fallbackPool": [\
],\
@@ -3970,26 +3960,26 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@simplewebauthn/iso-webcrypto", [\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-iso-webcrypto-npm-7.0.0-352babf4a0-c1644f9b68.zip/node_modules/@simplewebauthn/iso-webcrypto/",\
["npm:7.0.1", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-iso-webcrypto-npm-7.0.1-bae5f6738c-ed506490e0.zip/node_modules/@simplewebauthn/iso-webcrypto/",\
"packageDependencies": [\
["@simplewebauthn/iso-webcrypto", "npm:7.0.0"]\
["@simplewebauthn/iso-webcrypto", "npm:7.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@simplewebauthn/server", [\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-server-npm-7.0.0-e34589f137-836eb9fb97.zip/node_modules/@simplewebauthn/server/",\
["npm:7.0.1", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-server-npm-7.0.1-ac81233d49-d11c708008.zip/node_modules/@simplewebauthn/server/",\
"packageDependencies": [\
["@simplewebauthn/server", "npm:7.0.0"],\
["@simplewebauthn/server", "npm:7.0.1"],\
["@hexagon/base64", "npm:1.1.25"],\
["@peculiar/asn1-android", "npm:2.3.3"],\
["@peculiar/asn1-ecc", "npm:2.3.4"],\
["@peculiar/asn1-rsa", "npm:2.3.4"],\
["@peculiar/asn1-schema", "npm:2.3.3"],\
["@peculiar/asn1-x509", "npm:2.3.4"],\
["@simplewebauthn/iso-webcrypto", "npm:7.0.0"],\
["@simplewebauthn/iso-webcrypto", "npm:7.0.1"],\
["cbor-x", "npm:1.5.0"],\
["cross-fetch", "npm:3.1.5"],\
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"]\
@@ -4093,17 +4083,17 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@standardnotes/api", [\
["npm:1.24.10", {\
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.24.10-63391538ba-1b9a97fdd8.zip/node_modules/@standardnotes/api/",\
["npm:1.25.3", {\
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.25.3-29ba336725-bc7953c440.zip/node_modules/@standardnotes/api/",\
"packageDependencies": [\
["@standardnotes/api", "npm:1.24.10"],\
["@standardnotes/api", "npm:1.25.3"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/encryption", "npm:1.21.9"],\
["@standardnotes/models", "npm:1.42.11"],\
["@standardnotes/responses", "npm:1.13.6"],\
["@standardnotes/encryption", "npm:1.21.17"],\
["@standardnotes/models", "npm:1.43.5"],\
["@standardnotes/responses", "npm:1.13.9"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/utils", "npm:1.16.3"],\
["@standardnotes/utils", "npm:1.16.4"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
@@ -4166,16 +4156,16 @@ const RAW_RUNTIME_STATE =
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/tracing", "npm:7.28.1"],\
["@simplewebauthn/server", "npm:7.0.0"],\
["@simplewebauthn/server", "npm:7.0.1"],\
["@simplewebauthn/typescript-types", "npm:7.0.0"],\
["@standardnotes/api", "npm:1.24.10"],\
["@standardnotes/api", "npm:1.25.3"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/features", "npm:1.58.4"],\
["@standardnotes/features", "npm:1.58.9"],\
["@standardnotes/predicates", "workspace:packages/predicates"],\
["@standardnotes/responses", "npm:1.13.4"],\
["@standardnotes/responses", "npm:1.13.9"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/settings", "workspace:packages/settings"],\
["@standardnotes/sncrypto-common", "npm:1.13.0"],\
@@ -4312,15 +4302,15 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@standardnotes/encryption", [\
["npm:1.21.9", {\
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.21.9-092bc2cb51-dc1336cc05.zip/node_modules/@standardnotes/encryption/",\
["npm:1.21.17", {\
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.21.17-0801937c9c-ece7ac644e.zip/node_modules/@standardnotes/encryption/",\
"packageDependencies": [\
["@standardnotes/encryption", "npm:1.21.9"],\
["@standardnotes/encryption", "npm:1.21.17"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/models", "npm:1.42.11"],\
["@standardnotes/responses", "npm:1.13.6"],\
["@standardnotes/models", "npm:1.43.5"],\
["@standardnotes/responses", "npm:1.13.9"],\
["@standardnotes/sncrypto-common", "npm:1.13.3"],\
["@standardnotes/utils", "npm:1.16.3"],\
["@standardnotes/utils", "npm:1.16.4"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
@@ -4358,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"],\
@@ -4369,10 +4359,10 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "HARD"\
}],\
["npm:1.58.6", {\
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.58.6-7b1e198c39-98550416f1.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.6"],\
["@standardnotes/features", "npm:1.58.9"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/security", "workspace:packages/security"],\
@@ -4438,14 +4428,14 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@standardnotes/models", [\
["npm:1.42.11", {\
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.42.11-7db16001ef-6ff3409f70.zip/node_modules/@standardnotes/models/",\
["npm:1.43.5", {\
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.43.5-5180388ed4-fd8e3b60bd.zip/node_modules/@standardnotes/models/",\
"packageDependencies": [\
["@standardnotes/models", "npm:1.42.11"],\
["@standardnotes/models", "npm:1.43.5"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.58.6"],\
["@standardnotes/responses", "npm:1.13.6"],\
["@standardnotes/utils", "npm:1.16.3"],\
["@standardnotes/features", "npm:1.58.8"],\
["@standardnotes/responses", "npm:1.13.9"],\
["@standardnotes/utils", "npm:1.16.4"],\
["lodash", "npm:4.17.21"]\
],\
"linkType": "HARD"\
@@ -4466,40 +4456,13 @@ const RAW_RUNTIME_STATE =
"linkType": "SOFT"\
}]\
]],\
["@standardnotes/proxy-server", [\
["workspace:packages/proxy", {\
"packageLocation": "./packages/proxy/",\
"packageDependencies": [\
["@standardnotes/proxy-server", "workspace:packages/proxy"],\
["@types/newrelic", "npm:9.4.0"],\
["@types/node", "npm:18.14.0"],\
["@typescript-eslint/eslint-plugin", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:5.48.2"],\
["eslint", "npm:8.32.0"],\
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
["newrelic", "npm:9.8.0"],\
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"]\
],\
"linkType": "SOFT"\
}]\
]],\
["@standardnotes/responses", [\
["npm:1.13.4", {\
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.13.4-70cbd72561-4803ee14bd.zip/node_modules/@standardnotes/responses/",\
["npm:1.13.9", {\
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.13.9-5b1858da5d-5cb5daf9f3.zip/node_modules/@standardnotes/responses/",\
"packageDependencies": [\
["@standardnotes/responses", "npm:1.13.4"],\
["@standardnotes/responses", "npm:1.13.9"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.58.4"],\
["@standardnotes/security", "workspace:packages/security"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.13.6", {\
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.13.6-5df25fe3dd-c57e3e1fa1.zip/node_modules/@standardnotes/responses/",\
"packageDependencies": [\
["@standardnotes/responses", "npm:1.13.6"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.58.6"],\
["@standardnotes/features", "npm:1.58.8"],\
["@standardnotes/security", "workspace:packages/security"],\
["reflect-metadata", "npm:0.1.13"]\
],\
@@ -4515,11 +4478,12 @@ const RAW_RUNTIME_STATE =
["@aws-sdk/client-sqs", "npm:3.259.0"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/api", "npm:1.24.10"],\
["@standardnotes/api", "npm:1.25.3"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/responses", "npm:1.13.9"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/time", "workspace:packages/time"],\
["@types/cors", "npm:2.8.12"],\
@@ -4637,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"],\
@@ -4694,12 +4659,12 @@ const RAW_RUNTIME_STATE =
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/tracing", "npm:7.28.1"],\
["@standardnotes/api", "npm:1.24.10"],\
["@standardnotes/api", "npm:1.25.3"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/responses", "npm:1.13.4"],\
["@standardnotes/responses", "npm:1.13.9"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/settings", "workspace:packages/settings"],\
["@standardnotes/time", "workspace:packages/time"],\
@@ -4772,10 +4737,10 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "HARD"\
}],\
["npm:1.16.3", {\
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.16.3-87b47ad954-5c34beaafb.zip/node_modules/@standardnotes/utils/",\
["npm:1.16.4", {\
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.16.4-d7c627b154-ed29da54cb.zip/node_modules/@standardnotes/utils/",\
"packageDependencies": [\
["@standardnotes/utils", "npm:1.16.3"],\
["@standardnotes/utils", "npm:1.16.4"],\
["@standardnotes/common", "workspace:packages/common"],\
["dompurify", "npm:2.4.3"],\
["lodash", "npm:4.17.21"],\
@@ -4792,11 +4757,12 @@ const RAW_RUNTIME_STATE =
["@aws-sdk/client-sqs", "npm:3.259.0"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/api", "npm:1.24.10"],\
["@standardnotes/api", "npm:1.25.3"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/responses", "npm:1.13.9"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/utils", "npm:1.16.2"],\
["@types/cors", "npm:2.8.12"],\
@@ -4826,49 +4792,6 @@ const RAW_RUNTIME_STATE =
"linkType": "SOFT"\
}]\
]],\
["@standardnotes/workspace-server", [\
["workspace:packages/workspace", {\
"packageLocation": "./packages/workspace/",\
"packageDependencies": [\
["@standardnotes/workspace-server", "workspace:packages/workspace"],\
["@aws-sdk/client-sns", "npm:3.259.0"],\
["@aws-sdk/client-sqs", "npm:3.259.0"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/api", "npm:1.24.10"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/models", "npm:1.42.11"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/time", "workspace:packages/time"],\
["@types/cors", "npm:2.8.12"],\
["@types/express", "npm:4.17.14"],\
["@types/ioredis", "npm:5.0.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/newrelic", "npm:9.4.0"],\
["@typescript-eslint/eslint-plugin", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:5.48.2"],\
["cors", "npm:2.8.5"],\
["dotenv", "npm:16.0.1"],\
["eslint", "npm:8.32.0"],\
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
["express", "npm:4.18.2"],\
["inversify", "npm:6.0.1"],\
["inversify-express-utils", "npm:6.4.3"],\
["ioredis", "npm:5.2.4"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["mysql2", "npm:3.0.1"],\
["newrelic", "npm:9.8.0"],\
["reflect-metadata", "npm:0.1.13"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
["typeorm", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.10"],\
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"],\
["winston", "npm:3.8.2"]\
],\
"linkType": "SOFT"\
}]\
]],\
["@szmarczak/http-timer", [\
["npm:5.0.1", {\
"packageLocation": "./.yarn/cache/@szmarczak-http-timer-npm-5.0.1-52261e5986-67236cba79.zip/node_modules/@szmarczak/http-timer/",\
@@ -5272,13 +5195,6 @@ const RAW_RUNTIME_STATE =
["@types/node", "npm:18.11.9"]\
],\
"linkType": "HARD"\
}],\
["npm:18.14.0", {\
"packageLocation": "./.yarn/cache/@types-node-npm-18.14.0-ddc1a221d2-d17dff07c7.zip/node_modules/@types/node/",\
"packageDependencies": [\
["@types/node", "npm:18.14.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/nodemailer", [\

Binary file not shown.

View File

@@ -327,7 +327,7 @@ endif
quiet_cmd_regen_makefile = ACTION Regenerating $@
cmd_regen_makefile = cd $(srcdir); /Users/mo/Desktop/sn/dev/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/mo/Library/Caches/node-gyp/18.13.0" "-Dnode_gyp_dir=/Users/mo/Desktop/sn/dev/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp" "-Dnode_lib_file=/Users/mo/Library/Caches/node-gyp/18.13.0/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/mo/Desktop/sn/dev/server/.yarn/unplugged/@newrelic-native-metrics-npm-9.0.0-590d2e713a/node_modules/@newrelic/native-metrics" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/mo/Desktop/sn/dev/server/.yarn/unplugged/@newrelic-native-metrics-npm-9.0.0-590d2e713a/node_modules/@newrelic/native-metrics/build/config.gypi -I/Users/mo/Desktop/sn/dev/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi -I/Users/mo/Library/Caches/node-gyp/18.13.0/include/node/common.gypi "--toplevel-dir=." binding.gyp
Makefile: $(srcdir)/binding.gyp $(srcdir)/build/config.gypi $(srcdir)/../../../../node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi $(srcdir)/../../../../../../../../../../Library/Caches/node-gyp/18.13.0/include/node/common.gypi
Makefile: $(srcdir)/../../../../node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi $(srcdir)/binding.gyp $(srcdir)/../../../../../../../../../../Library/Caches/node-gyp/18.13.0/include/node/common.gypi $(srcdir)/build/config.gypi
$(call do_cmd,regen_makefile)
# "all" is a concatenation of the "all" targets from all the included

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,8 +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_WORKSPACE_SERVER_URL=http://localhost:3004
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

@@ -91,13 +91,6 @@ TOPIC_CREATED_RESULT=$(create_topic ${SCHEDULER_TOPIC_NAME})
echo "created topic: $TOPIC_CREATED_RESULT"
SCHEDULER_TOPIC_ARN=$(get_topic_arn_from_name $SCHEDULER_TOPIC_NAME)
WORKSPACE_TOPIC_NAME="workspace-local-topic"
echo "creating topic $WORKSPACE_TOPIC_NAME"
TOPIC_CREATED_RESULT=$(create_topic ${WORKSPACE_TOPIC_NAME})
echo "created topic: $TOPIC_CREATED_RESULT"
WORKSPACE_TOPIC_ARN=$(get_topic_arn_from_name $WORKSPACE_TOPIC_NAME)
QUEUE_NAME="analytics-local-queue"
echo "creating queue $QUEUE_NAME"
@@ -182,13 +175,6 @@ QUEUE_URL=$(create_queue ${QUEUE_NAME})
echo "created queue: $QUEUE_URL"
SCHEDULER_QUEUE_ARN=$(get_queue_arn_from_name $QUEUE_NAME)
QUEUE_NAME="workspace-local-queue"
echo "creating queue $QUEUE_NAME"
QUEUE_URL=$(create_queue ${QUEUE_NAME})
echo "created queue: $QUEUE_URL"
WORKSPACE_QUEUE_ARN=$(get_queue_arn_from_name $QUEUE_NAME)
echo "all topics are:"
echo "$(get_all_topics)"

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
* **analytics:** add general activity metric to mixpanel ([9d87200](https://github.com/standardnotes/server/commit/9d872008a7df7ccdd9afe7e7d99ccb0f12680319))
# [2.21.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.20.3...@standardnotes/analytics@2.21.0) (2023-02-23)
### Features

View File

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

View File

@@ -19,6 +19,10 @@ export class SessionCreatedEventHandler implements DomainEventHandlerInterface {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
})
this.mixpanelClient.track('GENERAL_ACTIVITY', {
distinct_id: analyticsId.toString(),
})
}
}
}

View File

@@ -19,6 +19,10 @@ export class SessionRefreshedEventHandler implements DomainEventHandlerInterface
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
})
this.mixpanelClient.track('GENERAL_ACTIVITY', {
distinct_id: analyticsId.toString(),
})
}
}
}

View File

@@ -6,7 +6,6 @@ PORT=3000
SYNCING_SERVER_JS_URL=http://syncing_server_js:3000
AUTH_SERVER_URL=http://auth:3000
WORKSPACE_SERVER_URL=http://workspace:3000
WEB_SOCKET_SERVER_URL=http://websockets:3000
PAYMENTS_SERVER_URL=http://payments:3000
FILES_SERVER_URL=http://files:3000

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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
## [1.49.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.49.2...@standardnotes/api-gateway@1.49.3) (2023-02-24)
### Bug Fixes
* **api-gateywa:** remove stale proxy references ([dfa5187](https://github.com/standardnotes/api-gateway/commit/dfa5187ff73833bf981d273da79f78ae0309a493))
## [1.49.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.49.1...@standardnotes/api-gateway@1.49.2) (2023-02-23)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.49.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.49.0...@standardnotes/api-gateway@1.49.1) (2023-02-23)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -19,10 +19,7 @@ import '../src/Controller/v1/TokensController'
import '../src/Controller/v1/OfflineController'
import '../src/Controller/v1/FilesController'
import '../src/Controller/v1/SubscriptionInvitesController'
import '../src/Controller/v1/WorkspacesController'
import '../src/Controller/v1/InvitesController'
import '../src/Controller/v1/AuthenticatorsController'
import '../src/Controller/v1/ProxyController'
import '../src/Controller/v2/PaymentsControllerV2'
import '../src/Controller/v2/ActionsControllerV2'

View File

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

View File

@@ -59,9 +59,7 @@ export class ContainerConfigLoader {
container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true))
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
container.bind(TYPES.WORKSPACE_SERVER_URL).toConstantValue(env.get('WORKSPACE_SERVER_URL', true))
container.bind(TYPES.WEB_SOCKET_SERVER_URL).toConstantValue(env.get('WEB_SOCKET_SERVER_URL', true))
container.bind(TYPES.PROXY_SERVER_URL).toConstantValue(env.get('PROXY_SERVER_URL', true))
container
.bind(TYPES.HTTP_CALL_TIMEOUT)
.toConstantValue(env.get('HTTP_CALL_TIMEOUT', true) ? +env.get('HTTP_CALL_TIMEOUT', true) : 60_000)

View File

@@ -9,9 +9,7 @@ const TYPES = {
FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'),
REVISIONS_SERVER_URL: Symbol.for('REVISIONS_SERVER_URL'),
EMAIL_SERVER_URL: Symbol.for('EMAIL_SERVER_URL'),
WORKSPACE_SERVER_URL: Symbol.for('WORKSPACE_SERVER_URL'),
WEB_SOCKET_SERVER_URL: Symbol.for('WEB_SOCKET_SERVER_URL'),
PROXY_SERVER_URL: Symbol.for('PROXY_SERVER_URL'),
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
HTTP_CALL_TIMEOUT: Symbol.for('HTTP_CALL_TIMEOUT'),
VERSION: Symbol.for('VERSION'),

View File

@@ -1,23 +0,0 @@
import { inject } from 'inversify'
import { Request, Response } from 'express'
import { controller, BaseHttpController, httpPost } from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
@controller('/v1/invites', TYPES.AuthMiddleware)
export class InvitesController extends BaseHttpController {
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
super()
}
@httpPost('/:inviteUuid/accept')
async accept(request: Request, response: Response): Promise<void> {
await this.httpService.callWorkspaceServer(
request,
response,
`invites/${request.params.inviteUuid}/accept`,
request.body,
)
}
}

View File

@@ -145,11 +145,6 @@ export class PaymentsController extends BaseHttpController {
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/stripe-setup-intent', request.body)
}
@httpGet('/pro_users/cp-prepayment-info', TYPES.SubscriptionTokenAuthMiddleware)
async coinpaymentsPrepaymentInfo(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/cp-prepayment-info', request.body)
}
@all('/pro_users(/*)?')
async proUsers(request: Request, response: Response): Promise<void> {
await this.httpService.callPaymentsServer(request, response, request.path.replace('v1', 'api'), request.body)

View File

@@ -1,18 +0,0 @@
import { Request, Response } from 'express'
import { inject } from 'inversify'
import { all, BaseHttpController, controller } from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
@controller('/v1/proxy')
export class ProxyController extends BaseHttpController {
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
super()
}
@all('*', TYPES.AuthMiddleware)
async createToken(request: Request, response: Response): Promise<void> {
await this.httpService.callProxyServer(request, response, request.path.replace('/v1/proxy', ''), request.body)
}
}

View File

@@ -1,53 +0,0 @@
import { inject } from 'inversify'
import { Request, Response } from 'express'
import { controller, BaseHttpController, httpPost, httpGet } from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
@controller('/v1/workspaces', TYPES.AuthMiddleware)
export class WorkspacesController extends BaseHttpController {
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
super()
}
@httpPost('/')
async create(request: Request, response: Response): Promise<void> {
await this.httpService.callWorkspaceServer(request, response, 'workspaces', request.body)
}
@httpGet('/:workspaceUuid/users')
async listWorkspaceUsers(request: Request, response: Response): Promise<void> {
await this.httpService.callWorkspaceServer(
request,
response,
`workspaces/${request.params.workspaceUuid}/users`,
request.body,
)
}
@httpPost('/:workspaceUuid/users/:userUuid/keyshare')
async initiateKeyshare(request: Request, response: Response): Promise<void> {
await this.httpService.callWorkspaceServer(
request,
response,
`workspaces/${request.params.workspaceUuid}/users/${request.params.userUuid}/keyshare`,
request.body,
)
}
@httpGet('/')
async listWorkspaces(request: Request, response: Response): Promise<void> {
await this.httpService.callWorkspaceServer(request, response, 'workspaces', request.body)
}
@httpPost('/:workspaceUuid/invites')
async invite(request: Request, response: Response): Promise<void> {
await this.httpService.callWorkspaceServer(
request,
response,
`workspaces/${request.params.workspaceUuid}/invites`,
request.body,
)
}
}

View File

@@ -16,11 +16,9 @@ export class HttpService implements HttpServiceInterface {
@inject(TYPES.SYNCING_SERVER_JS_URL) private syncingServerJsUrl: string,
@inject(TYPES.PAYMENTS_SERVER_URL) private paymentsServerUrl: string,
@inject(TYPES.FILES_SERVER_URL) private filesServerUrl: string,
@inject(TYPES.WORKSPACE_SERVER_URL) private workspaceServerUrl: string,
@inject(TYPES.WEB_SOCKET_SERVER_URL) private webSocketServerUrl: string,
@inject(TYPES.REVISIONS_SERVER_URL) private revisionsServerUrl: string,
@inject(TYPES.EMAIL_SERVER_URL) private emailServerUrl: string,
@inject(TYPES.PROXY_SERVER_URL) private proxyServerUrl: string,
@inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Logger) private logger: Logger,
@@ -82,21 +80,6 @@ export class HttpService implements HttpServiceInterface {
await this.callServer(this.emailServerUrl, request, response, endpoint, payload)
}
async callWorkspaceServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void> {
if (!this.workspaceServerUrl) {
response.status(400).send({ message: 'Workspace Server not configured' })
return
}
await this.callServer(this.workspaceServerUrl, request, response, endpoint, payload)
}
async callWebSocketServer(
request: Request,
response: Response,
@@ -112,21 +95,6 @@ export class HttpService implements HttpServiceInterface {
await this.callServer(this.webSocketServerUrl, request, response, endpoint, payload)
}
async callProxyServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void> {
if (!this.proxyServerUrl) {
this.logger.debug('Proxy Server URL not defined. Skipped request to Proxy.')
return
}
await this.callServer(this.proxyServerUrl, request, response, endpoint, payload)
}
async callPaymentsServer(
request: Request,
response: Response,

View File

@@ -43,22 +43,10 @@ export interface HttpServiceInterface {
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void>
callWorkspaceServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void>
callWebSocketServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void>
callProxyServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void>
}

View File

@@ -3,6 +3,89 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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
## [1.89.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.89.3...@standardnotes/auth-server@1.89.4) (2023-03-01)
### Bug Fixes
* **auth:** updating counter post authenticator verification ([a9cc00a](https://github.com/standardnotes/server/commit/a9cc00a4783c12e71eb181a3ccf3218b418750d9))
## [1.89.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.89.2...@standardnotes/auth-server@1.89.3) (2023-02-27)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.89.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.89.1...@standardnotes/auth-server@1.89.2) (2023-02-24)
### Bug Fixes
* **auth:** add cross-platform authenticator selection option ([edc4a20](https://github.com/standardnotes/server/commit/edc4a2085952efe0b83c8e837a52555087714ef7))
## [1.89.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.89.0...@standardnotes/auth-server@1.89.1) (2023-02-23)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.89.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.88.3...@standardnotes/auth-server@1.89.0) (2023-02-23)
### Bug Fixes

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

@@ -0,0 +1,137 @@
import 'reflect-metadata'
import 'newrelic'
import { Stream } from 'stream'
import { Logger } from 'winston'
import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
import { SettingName } from '@standardnotes/settings'
import { EmailLevel } from '@standardnotes/domain-core'
import { UserSubscriptionServiceInterface } from '../src/Domain/Subscription/UserSubscriptionServiceInterface'
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
import { SubscriptionSettingServiceInterface } from '../src/Domain/Setting/SubscriptionSettingServiceInterface'
import { EncryptionVersion } from '../src/Domain/Encryption/EncryptionVersion'
const requestSettingMigration = async (
settingRepository: SettingRepositoryInterface,
subscriptionSettingService: SubscriptionSettingServiceInterface,
userRepository: UserRepositoryInterface,
userSubscriptionService: UserSubscriptionServiceInterface,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
): Promise<void> => {
const stream = await settingRepository.streamAllByNameAndValue(
SettingName.create(SettingName.NAMES.MuteSignInEmails).getValue(),
'not_muted',
)
return new Promise((resolve, reject) => {
stream
.pipe(
new Stream.Transform({
objectMode: true,
transform: async (setting, _encoding, callback) => {
const user = await userRepository.findOneByUuid(setting.setting_user_uuid)
if (!user) {
callback()
return
}
const { regularSubscription, sharedSubscription } =
await userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid)
const subscription = sharedSubscription ?? regularSubscription
if (!subscription) {
await domainEventPublisher.publish(
domainEventFactory.createMuteEmailsSettingChangedEvent({
username: user.email,
mute: true,
emailSubscriptionRejectionLevel: EmailLevel.LEVELS.SignIn,
}),
)
await settingRepository.deleteByUserUuid({
userUuid: user.uuid,
settingName: SettingName.NAMES.MuteSignInEmails,
})
callback()
return
}
await subscriptionSettingService.createOrReplace({
userSubscription: subscription,
props: {
name: SettingName.NAMES.MuteSignInEmails,
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
unencryptedValue: 'not_muted',
},
})
await settingRepository.deleteByUserUuid({
userUuid: user.uuid,
settingName: SettingName.NAMES.MuteSignInEmails,
})
callback()
},
}),
)
.on('finish', resolve)
.on('error', reject)
})
}
const container = new ContainerConfigLoader()
void container.load().then((container) => {
dayjs.extend(utc)
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Logger)
logger.info('Starting migration of mute sign in emails settings to subscription settings...')
const settingRepository: SettingRepositoryInterface = container.get(TYPES.SettingRepository)
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
const subscriptionSettingService: SubscriptionSettingServiceInterface = container.get(
TYPES.SubscriptionSettingService,
)
const userRepository: UserRepositoryInterface = container.get(TYPES.UserRepository)
const userSubscriptionService: UserSubscriptionServiceInterface = container.get(TYPES.UserSubscriptionService)
Promise.resolve(
requestSettingMigration(
settingRepository,
subscriptionSettingService,
userRepository,
userSubscriptionService,
domainEventFactory,
domainEventPublisher,
),
)
.then(() => {
logger.info('Migration of mute sign in emails settings to subscription settings finished successfully.')
process.exit(0)
})
.catch((error) => {
logger.error(`Migration of mute sign in emails settings to subscription settings failed: ${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

@@ -4,7 +4,7 @@ const path = require('path')
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/worker.js')))
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/migrate_email_settings.js')))
Object.defineProperty(exports, '__esModule', { value: true })

View File

@@ -40,6 +40,11 @@ case "$COMMAND" in
node docker/entrypoint-user-email-backup.js $EMAIL
;;
'migrate-email-settings' )
echo "[Docker] Starting Email Settings Migration..."
node docker/entrypoint-migrate-email-settings.js
;;
'dropbox-daily-backup' )
echo "[Docker] Starting Dropbox Daily Backup..."
node docker/entrypoint-backup.js dropbox daily

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

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.89.0",
"version": "1.93.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -39,15 +39,15 @@
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.28.1",
"@sentry/tracing": "^7.28.1",
"@simplewebauthn/server": "^7.0.0",
"@standardnotes/api": "^1.24.10",
"@simplewebauthn/server": "^7.0.1",
"@standardnotes/api": "^1.25.3",
"@standardnotes/common": "workspace:*",
"@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.4",
"@standardnotes/responses": "^1.13.9",
"@standardnotes/security": "workspace:*",
"@standardnotes/settings": "workspace:*",
"@standardnotes/sncrypto-common": "^1.9.0",

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

@@ -2,12 +2,12 @@ import { inject, injectable } from 'inversify'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import {
ApiVersion,
HttpStatusCode,
UserDeletionResponse,
UserRegistrationRequestParams,
UserRegistrationResponse,
UserServerInterface,
UserDeletionResponseBody,
UserRegistrationResponseBody,
} from '@standardnotes/api'
import { HttpResponse, HttpStatusCode } from '@standardnotes/responses'
import { ProtocolVersion } from '@standardnotes/common'
import TYPES from '../Bootstrap/Types'
@@ -16,13 +16,13 @@ import { Register } from '../Domain/UseCase/Register'
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
import { SignInWithRecoveryCodes } from '../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
import { SignInWithRecoveryCodesRequestParams } from '../Infra/Http/Request/SignInWithRecoveryCodesRequestParams'
import { SignInWithRecoveryCodesResponse } from '../Infra/Http/Response/SignInWithRecoveryCodesResponse'
import { GetUserKeyParamsRecovery } from '../Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery'
import { RecoveryKeyParamsRequestParams } from '../Infra/Http/Request/RecoveryKeyParamsRequestParams'
import { RecoveryKeyParamsResponse } from '../Infra/Http/Response/RecoveryKeyParamsResponse'
import { SignInWithRecoveryCodesResponseBody } from '../Infra/Http/Response/SignInWithRecoveryCodesResponseBody'
import { RecoveryKeyParamsResponseBody } from '../Infra/Http/Response/RecoveryKeyParamsResponseBody'
import { GenerateRecoveryCodesResponseBody } from '../Infra/Http/Response/GenerateRecoveryCodesResponseBody'
import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes'
import { GenerateRecoveryCodesRequestParams } from '../Infra/Http/Request/GenerateRecoveryCodesRequestParams'
import { GenerateRecoveryCodesResponse } from '../Infra/Http/Response/GenerateRecoveryCodesResponse'
import { Logger } from 'winston'
@injectable()
@@ -38,11 +38,11 @@ export class AuthController implements UserServerInterface {
@inject(TYPES.Logger) private logger: Logger,
) {}
async deleteAccount(_params: never): Promise<UserDeletionResponse> {
async deleteAccount(_params: never): Promise<HttpResponse<UserDeletionResponseBody>> {
throw new Error('This method is implemented on the payments server.')
}
async register(params: UserRegistrationRequestParams): Promise<UserRegistrationResponse> {
async register(params: UserRegistrationRequestParams): Promise<HttpResponse<UserRegistrationResponseBody>> {
if (!params.email || !params.password) {
return {
status: HttpStatusCode.BadRequest,
@@ -93,7 +93,9 @@ export class AuthController implements UserServerInterface {
}
}
async generateRecoveryCodes(params: GenerateRecoveryCodesRequestParams): Promise<GenerateRecoveryCodesResponse> {
async generateRecoveryCodes(
params: GenerateRecoveryCodesRequestParams,
): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
const result = await this.doGenerateRecoveryCodes.execute({
userUuid: params.userUuid,
})
@@ -119,7 +121,7 @@ export class AuthController implements UserServerInterface {
async signInWithRecoveryCodes(
params: SignInWithRecoveryCodesRequestParams,
): Promise<SignInWithRecoveryCodesResponse> {
): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>> {
if (params.apiVersion !== ApiVersion.v0) {
return {
status: HttpStatusCode.BadRequest,
@@ -158,7 +160,9 @@ export class AuthController implements UserServerInterface {
}
}
async recoveryKeyParams(params: RecoveryKeyParamsRequestParams): Promise<RecoveryKeyParamsResponse> {
async recoveryKeyParams(
params: RecoveryKeyParamsRequestParams,
): Promise<HttpResponse<RecoveryKeyParamsResponseBody>> {
if (params.apiVersion !== ApiVersion.v0) {
return {
status: HttpStatusCode.BadRequest,

View File

@@ -1,4 +1,4 @@
import { HttpStatusCode } from '@standardnotes/api'
import { HttpResponse, HttpStatusCode } from '@standardnotes/responses'
import { MapperInterface } from '@standardnotes/domain-core'
import { Authenticator } from '../Domain/Authenticator/Authenticator'
import { DeleteAuthenticator } from '../Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator'
@@ -13,11 +13,11 @@ import { GenerateAuthenticatorAuthenticationOptionsRequestParams } from '../Infr
import { GenerateAuthenticatorRegistrationOptionsRequestParams } from '../Infra/Http/Request/GenerateAuthenticatorRegistrationOptionsRequestParams'
import { ListAuthenticatorsRequestParams } from '../Infra/Http/Request/ListAuthenticatorsRequestParams'
import { VerifyAuthenticatorRegistrationResponseRequestParams } from '../Infra/Http/Request/VerifyAuthenticatorRegistrationResponseRequestParams'
import { DeleteAuthenticatorResponse } from '../Infra/Http/Response/DeleteAuthenticatorResponse'
import { GenerateAuthenticatorAuthenticationOptionsResponse } from '../Infra/Http/Response/GenerateAuthenticatorAuthenticationOptionsResponse'
import { GenerateAuthenticatorRegistrationOptionsResponse } from '../Infra/Http/Response/GenerateAuthenticatorRegistrationOptionsResponse'
import { ListAuthenticatorsResponse } from '../Infra/Http/Response/ListAuthenticatorsResponse'
import { VerifyAuthenticatorRegistrationResponseResponse } from '../Infra/Http/Response/VerifyAuthenticatorRegistrationResponseResponse'
import { DeleteAuthenticatorResponseBody } from '../Infra/Http/Response/DeleteAuthenticatorResponseBody'
import { GenerateAuthenticatorAuthenticationOptionsResponseBody } from '../Infra/Http/Response/GenerateAuthenticatorAuthenticationOptionsResponseBody'
import { GenerateAuthenticatorRegistrationOptionsResponseBody } from '../Infra/Http/Response/GenerateAuthenticatorRegistrationOptionsResponseBody'
import { ListAuthenticatorsResponseBody } from '../Infra/Http/Response/ListAuthenticatorsResponseBody'
import { VerifyAuthenticatorRegistrationResponseResponseBody } from '../Infra/Http/Response/VerifyAuthenticatorRegistrationResponseResponseBody'
export class AuthenticatorsController {
constructor(
@@ -29,7 +29,7 @@ export class AuthenticatorsController {
private authenticatorHttpMapper: MapperInterface<Authenticator, AuthenticatorHttpProjection>,
) {}
async list(params: ListAuthenticatorsRequestParams): Promise<ListAuthenticatorsResponse> {
async list(params: ListAuthenticatorsRequestParams): Promise<HttpResponse<ListAuthenticatorsResponseBody>> {
const result = await this.listAuthenticators.execute({
userUuid: params.userUuid,
})
@@ -44,7 +44,7 @@ export class AuthenticatorsController {
}
}
async delete(params: DeleteAuthenticatorRequestParams): Promise<DeleteAuthenticatorResponse> {
async delete(params: DeleteAuthenticatorRequestParams): Promise<HttpResponse<DeleteAuthenticatorResponseBody>> {
const result = await this.deleteAuthenticator.execute({
userUuid: params.userUuid,
authenticatorId: params.authenticatorId,
@@ -60,7 +60,7 @@ export class AuthenticatorsController {
async generateRegistrationOptions(
params: GenerateAuthenticatorRegistrationOptionsRequestParams,
): Promise<GenerateAuthenticatorRegistrationOptionsResponse> {
): Promise<HttpResponse<GenerateAuthenticatorRegistrationOptionsResponseBody>> {
const result = await this.generateAuthenticatorRegistrationOptions.execute({
userUuid: params.userUuid,
username: params.username,
@@ -85,7 +85,7 @@ export class AuthenticatorsController {
async verifyRegistrationResponse(
params: VerifyAuthenticatorRegistrationResponseRequestParams,
): Promise<VerifyAuthenticatorRegistrationResponseResponse> {
): Promise<HttpResponse<VerifyAuthenticatorRegistrationResponseResponseBody>> {
const result = await this.verifyAuthenticatorRegistrationResponse.execute({
userUuid: params.userUuid,
name: params.name,
@@ -111,7 +111,7 @@ export class AuthenticatorsController {
async generateAuthenticationOptions(
params: GenerateAuthenticatorAuthenticationOptionsRequestParams,
): Promise<GenerateAuthenticatorAuthenticationOptionsResponse> {
): Promise<HttpResponse<GenerateAuthenticatorAuthenticationOptionsResponseBody>> {
const result = await this.generateAuthenticatorAuthenticationOptions.execute({
username: params.username,
})

View File

@@ -4,7 +4,7 @@ import { BaseHttpController, controller, httpPost, results } from 'inversify-exp
import { Request, Response } from 'express'
import TYPES from '../Bootstrap/Types'
import { CreateListedAccount } from '../Domain/UseCase/CreateListedAccount/CreateListedAccount'
import { ErrorTag } from '@standardnotes/api'
import { ErrorTag } from '@standardnotes/responses'
@controller('/listed')
export class ListedController extends BaseHttpController {

View File

@@ -1,4 +1,4 @@
import { ErrorTag } from '@standardnotes/api'
import { ErrorTag } from '@standardnotes/responses'
import { Request, Response } from 'express'
import { inject } from 'inversify'
import {

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

@@ -1,4 +1,4 @@
import { ErrorTag } from '@standardnotes/api'
import { ErrorTag } from '@standardnotes/responses'
import { Request, Response } from 'express'
import { inject } from 'inversify'
import {
@@ -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

@@ -1,19 +1,19 @@
import {
AppleIAPConfirmRequestParams,
AppleIAPConfirmResponse,
HttpStatusCode,
AppleIAPConfirmResponseBody,
SubscriptionInviteAcceptRequestParams,
SubscriptionInviteAcceptResponse,
SubscriptionInviteAcceptResponseBody,
SubscriptionInviteCancelRequestParams,
SubscriptionInviteCancelResponse,
SubscriptionInviteCancelResponseBody,
SubscriptionInviteDeclineRequestParams,
SubscriptionInviteDeclineResponse,
SubscriptionInviteDeclineResponseBody,
SubscriptionInviteListRequestParams,
SubscriptionInviteListResponse,
SubscriptionInviteListResponseBody,
SubscriptionInviteRequestParams,
SubscriptionInviteResponse,
SubscriptionInviteResponseBody,
SubscriptionServerInterface,
} from '@standardnotes/api'
import { HttpResponse, HttpStatusCode } from '@standardnotes/responses'
import { inject, injectable } from 'inversify'
import TYPES from '../Bootstrap/Types'
@@ -37,11 +37,13 @@ export class SubscriptionInvitesController implements SubscriptionServerInterfac
private listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations,
) {}
async confirmAppleIAP(_params: AppleIAPConfirmRequestParams): Promise<AppleIAPConfirmResponse> {
async confirmAppleIAP(_params: AppleIAPConfirmRequestParams): Promise<HttpResponse<AppleIAPConfirmResponseBody>> {
throw new Error('Method implemented on the payments service.')
}
async acceptInvite(params: SubscriptionInviteAcceptRequestParams): Promise<SubscriptionInviteAcceptResponse> {
async acceptInvite(
params: SubscriptionInviteAcceptRequestParams,
): Promise<HttpResponse<SubscriptionInviteAcceptResponseBody>> {
const result = await this.acceptSharedSubscriptionInvitation.execute({
sharedSubscriptionInvitationUuid: params.inviteUuid,
})
@@ -59,7 +61,9 @@ export class SubscriptionInvitesController implements SubscriptionServerInterfac
}
}
async declineInvite(params: SubscriptionInviteDeclineRequestParams): Promise<SubscriptionInviteDeclineResponse> {
async declineInvite(
params: SubscriptionInviteDeclineRequestParams,
): Promise<HttpResponse<SubscriptionInviteDeclineResponseBody>> {
const result = await this.declineSharedSubscriptionInvitation.execute({
sharedSubscriptionInvitationUuid: params.inviteUuid,
})
@@ -77,7 +81,7 @@ export class SubscriptionInvitesController implements SubscriptionServerInterfac
}
}
async invite(params: SubscriptionInviteRequestParams): Promise<SubscriptionInviteResponse> {
async invite(params: SubscriptionInviteRequestParams): Promise<HttpResponse<SubscriptionInviteResponseBody>> {
if (!params.identifier) {
return {
status: HttpStatusCode.BadRequest,
@@ -109,7 +113,9 @@ export class SubscriptionInvitesController implements SubscriptionServerInterfac
}
}
async cancelInvite(params: SubscriptionInviteCancelRequestParams): Promise<SubscriptionInviteCancelResponse> {
async cancelInvite(
params: SubscriptionInviteCancelRequestParams,
): Promise<HttpResponse<SubscriptionInviteCancelResponseBody>> {
const result = await this.cancelSharedSubscriptionInvitation.execute({
sharedSubscriptionInvitationUuid: params.inviteUuid,
inviterEmail: params.inviterEmail as string,
@@ -128,7 +134,9 @@ export class SubscriptionInvitesController implements SubscriptionServerInterfac
}
}
async listInvites(params: SubscriptionInviteListRequestParams): Promise<SubscriptionInviteListResponse> {
async listInvites(
params: SubscriptionInviteListRequestParams,
): Promise<HttpResponse<SubscriptionInviteListResponseBody>> {
const result = await this.listSharedSubscriptionInvitations.execute({
inviterEmail: params.inviterEmail as string,
})

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

@@ -1,5 +1,5 @@
import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
import { ErrorTag } from '@standardnotes/api'
import { ErrorTag } from '@standardnotes/responses'
import { SettingName } from '@standardnotes/settings'
import { Request, Response } from 'express'
import { inject } from 'inversify'
@@ -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

@@ -1,9 +1,5 @@
import {
HttpStatusCode,
UserRequestRequestParams,
UserRequestResponse,
UserRequestServerInterface,
} from '@standardnotes/api'
import { UserRequestRequestParams, UserRequestResponseBody, UserRequestServerInterface } from '@standardnotes/api'
import { HttpResponse, HttpStatusCode } from '@standardnotes/responses'
import { inject, injectable } from 'inversify'
import TYPES from '../Bootstrap/Types'
import { ProcessUserRequest } from '../Domain/UseCase/ProcessUserRequest/ProcessUserRequest'
@@ -12,7 +8,7 @@ import { ProcessUserRequest } from '../Domain/UseCase/ProcessUserRequest/Process
export class UserRequestsController implements UserRequestServerInterface {
constructor(@inject(TYPES.ProcessUserRequest) private processUserRequest: ProcessUserRequest) {}
async submitUserRequest(params: UserRequestRequestParams): Promise<UserRequestResponse> {
async submitUserRequest(params: UserRequestRequestParams): Promise<HttpResponse<UserRequestResponseBody>> {
const result = await this.processUserRequest.execute({
requestType: params.requestType,
userEmail: params.userEmail as string,

View File

@@ -1,6 +1,6 @@
import { Request, Response } from 'express'
import { inject } from 'inversify'
import { ErrorTag } from '@standardnotes/api'
import { ErrorTag } from '@standardnotes/responses'
import {
BaseHttpController,
controller,

View File

@@ -7,8 +7,7 @@ import {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
results,
} from 'inversify-express-utils'
import { CreateValetTokenPayload } from '@standardnotes/responses'
import { ErrorTag } from '@standardnotes/api'
import { CreateValetTokenPayload, ErrorTag } from '@standardnotes/responses'
import { ValetTokenOperation } from '@standardnotes/security'
import { Uuid } from '@standardnotes/domain-core'

View File

@@ -7,6 +7,7 @@ export interface AuthenticatorRepositoryInterface {
findById(id: UniqueEntityId): Promise<Authenticator | null>
findByUserUuidAndCredentialId(userUuid: Uuid, credentialId: string): Promise<Authenticator | null>
save(authenticator: Authenticator): Promise<void>
updateCounter(id: UniqueEntityId, counter: number): Promise<void>
remove(authenticator: Authenticator): Promise<void>
removeByUserUuid(userUuid: Uuid): Promise<void>
}

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

@@ -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
}

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