Compare commits

..

48 Commits

Author SHA1 Message Date
standardci
dd4924c925 chore(release): publish new version
- @standardnotes/analytics@2.19.4
 - @standardnotes/auth-server@1.81.11
 - @standardnotes/event-store@1.6.58
 - @standardnotes/revisions-server@1.10.11
 - @standardnotes/scheduler-server@1.16.8
 - @standardnotes/syncing-server@1.28.9
 - @standardnotes/workspace-server@1.19.7
2023-01-17 12:47:34 +00:00
Karol Sójko
f73129cd7e fix: allow to run typeorm in non-replica mode 2023-01-17 13:45:32 +01:00
standardci
4983c8741e chore(release): publish new version
- @standardnotes/revisions-server@1.10.10
2023-01-17 10:55:57 +00:00
Karol Sójko
c5798640ff fix(revisions): add debug logs for retrieving revisions metadata from mysql 2023-01-17 11:53:57 +01:00
standardci
5803a8018a chore(release): publish new version
- @standardnotes/revisions-server@1.10.9
2023-01-17 10:02:11 +00:00
Karol Sójko
e2aae8ac8a fix(revisions): response structure 2023-01-17 11:00:05 +01:00
Karol Sójko
2917aeeb32 fix: turn some of the applications into a utility publishing workflow 2023-01-17 10:24:45 +01:00
standardci
9377c03c3f chore(release): publish new version
- @standardnotes/syncing-server@1.28.8
2023-01-17 09:09:03 +00:00
Karol Sójko
9b926fbad6 fix(syncing-server-js): creating directory for revision dumps 2023-01-17 10:07:01 +01:00
Karol Sójko
8db19c3e2b fix(syncing-server-js): add debug logs for dumping items for revisions creation 2023-01-17 10:02:17 +01:00
standardci
ca970781c7 chore(release): publish new version
- @standardnotes/analytics@2.19.3
 - @standardnotes/auth-server@1.81.10
 - @standardnotes/domain-core@1.11.1
 - @standardnotes/revisions-server@1.10.8
 - @standardnotes/scheduler-server@1.16.7
 - @standardnotes/syncing-server@1.28.7
 - @standardnotes/workspace-server@1.19.6
2023-01-16 14:36:54 +00:00
Karol Sójko
e7beee2788 fix(revisions): add required role to revisions list response 2023-01-16 15:34:20 +01:00
standardci
d266eada88 chore(release): publish new version
- @standardnotes/revisions-server@1.10.7
2023-01-16 10:22:58 +00:00
Karol Sójko
11b8b078b4 fix(revisions): remove redundant specs 2023-01-16 11:20:58 +01:00
standardci
37912fa29a chore(release): publish new version
- @standardnotes/revisions-server@1.10.6
2023-01-16 10:14:55 +00:00
Karol Sójko
b97dafe6f3 fix(revisions): mapping to snake case 2023-01-16 11:12:29 +01:00
standardci
2a29151395 chore(release): publish new version
- @standardnotes/revisions-server@1.10.5
2023-01-16 10:01:02 +00:00
Karol Sójko
8b988d89c0 fix(revisions): response structure 2023-01-16 10:58:39 +01:00
standardci
c0908f1b58 chore(release): publish new version
- @standardnotes/api-gateway@1.46.0
2023-01-16 09:02:31 +00:00
Karol Sójko
bb46044f7c Merge pull request #366 from standardnotes/switch_revisions
feat(api-gateway): switch to fetching revisions from reivsions server
2023-01-16 10:00:37 +01:00
Karol Sójko
60b3dd6138 feat(api-gateway): add all revisions endpoints on v2 2023-01-16 09:40:07 +01:00
Karol Sójko
22c1f936c3 feat(api-gateway): switch to fetching revisions from reivsions server 2023-01-16 09:33:13 +01:00
standardci
e899874b04 chore(release): publish new version
- @standardnotes/api-gateway@1.45.3
2023-01-16 08:32:47 +00:00
Karol Sójko
04c6888cf6 fix(api-gateway): add noindex robots meta tag to api gateway homepage 2023-01-16 09:30:02 +01:00
standardci
29c56c6919 chore(release): publish new version
- @standardnotes/analytics@2.19.2
 - @standardnotes/api-gateway@1.45.2
 - @standardnotes/auth-server@1.81.9
 - @standardnotes/domain-events-infra@1.9.60
 - @standardnotes/domain-events@2.105.2
 - @standardnotes/event-store@1.6.57
 - @standardnotes/files-server@1.9.5
 - @standardnotes/revisions-server@1.10.4
 - @standardnotes/scheduler-server@1.16.6
 - @standardnotes/security@1.7.3
 - @standardnotes/syncing-server@1.28.6
 - @standardnotes/websockets-server@1.5.4
 - @standardnotes/workspace-server@1.19.5
2023-01-13 09:56:13 +00:00
Karol Sójko
c98ed9cc85 chore: update jsonwebtoken 2023-01-13 10:53:57 +01:00
standardci
88f7530c13 chore(release): publish new version
- @standardnotes/api-gateway@1.45.1
 - @standardnotes/files-server@1.9.4
2023-01-13 09:05:13 +00:00
Karol Sójko
bb820437af fix: add robots.txt setup for api-gateway and files server to disallow indexing 2023-01-13 10:03:03 +01:00
standardci
d1a4bd38e0 chore(release): publish new version
- @standardnotes/auth-server@1.81.8
2023-01-11 12:49:19 +00:00
Karol Sójko
d18f6ccd32 fix(auth): add relying party configuration options 2023-01-11 13:47:13 +01:00
standardci
aa317c964e chore(release): publish new version
- @standardnotes/auth-server@1.81.7
2023-01-09 14:31:00 +00:00
Karol Sójko
7ae8845ae9 fix(auth): failure messages for debug logs upon signing in with recovery codes 2023-01-09 15:28:35 +01:00
standardci
123a6dbe0c chore(release): publish new version
- @standardnotes/auth-server@1.81.6
2023-01-09 13:53:44 +00:00
Karol Sójko
dda8d79526 fix(auth): request parameters names 2023-01-09 14:51:48 +01:00
standardci
de5293955a chore(release): publish new version
- @standardnotes/auth-server@1.81.5
2023-01-09 12:59:21 +00:00
Karol Sójko
96669bff5b fix(auth): debuggin recovery sign in 2023-01-09 13:56:56 +01:00
standardci
a99762f004 chore(release): publish new version
- @standardnotes/auth-server@1.81.4
2023-01-09 12:49:05 +00:00
Karol Sójko
1fc3c9b83e fix(auth): error messages on account recovery 2023-01-09 13:47:11 +01:00
standardci
af86b6f664 chore(release): publish new version
- @standardnotes/auth-server@1.81.3
2023-01-09 11:58:44 +00:00
Karol Sójko
a0208dd5b3 fix(auth): remove mfa settings after recovery sign in 2023-01-09 12:56:50 +01:00
standardci
1c5c8b81d5 chore(release): publish new version
- @standardnotes/scheduler-server@1.16.5
2023-01-06 08:17:13 +00:00
Karol Sójko
79c3e33434 fix(scheduler): change email levels 2023-01-06 09:15:22 +01:00
standardci
5ab8729a31 chore(release): publish new version
- @standardnotes/auth-server@1.81.2
2023-01-05 13:36:08 +00:00
Karol Sójko
db0baf92f1 fix(auth): return type to include user 2023-01-05 14:33:34 +01:00
standardci
a8974094db chore(release): publish new version
- @standardnotes/auth-server@1.81.1
2023-01-05 11:31:08 +00:00
Karol Sójko
13c5c97ba7 fix(auth): allow retrieval of recovery codes setting 2023-01-05 12:28:56 +01:00
standardci
894ebb3edd chore(release): publish new version
- @standardnotes/api-gateway@1.45.0
 - @standardnotes/auth-server@1.81.0
2023-01-05 10:44:51 +00:00
Karol Sójko
cac899a7e5 feat(auth): add recovery sign in with recovery codes 2023-01-05 11:42:55 +01:00
101 changed files with 1976 additions and 319 deletions

View File

@@ -11,19 +11,18 @@ on:
workflow_dispatch:
jobs:
call_server_application_workflow:
name: Server Application
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
call_server_utility_workflow:
name: Server Utility
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
with:
service_name: analytics
workspace_name: "@standardnotes/analytics"
e2e_tag_parameter_name: analytics_image_tag
deploy_web: false
package_path: packages/analytics
secrets: inherit
newrelic:
needs: call_server_application_workflow
needs: call_server_utility_workflow
runs-on: ubuntu-latest

View File

@@ -0,0 +1,164 @@
name: Reusable Server Utility Workflow
on:
workflow_call:
inputs:
service_name:
required: true
type: string
workspace_name:
required: true
type: string
deploy_web:
required: false
default: true
type: boolean
deploy_worker:
required: false
default: true
type: boolean
package_path:
required: true
type: string
secrets:
DOCKER_USERNAME:
required: true
DOCKER_PASSWORD:
required: true
CI_PAT_TOKEN:
required: true
AWS_ACCESS_KEY_ID:
required: true
AWS_SECRET_ACCESS_KEY:
required: true
jobs:
build:
runs-on: ubuntu-latest
outputs:
temp_dir: ${{ steps.bundle-dir.outputs.temp_dir }}
steps:
- uses: actions/checkout@v3
- name: Create Bundle Dir
id: bundle-dir
run: echo "temp_dir=$(mktemp -d -t ${{ inputs.service_name }}-${{ github.sha }}-XXXXXXX)" >> $GITHUB_OUTPUT
- name: Cache build
id: cache-build
uses: actions/cache@v3
with:
path: |
packages/**/dist
${{ steps.bundle-dir.outputs.temp_dir }}
key: ${{ runner.os }}-${{ inputs.service_name }}-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
run: yarn build ${{ inputs.package_path }}
- name: Bundle
run: yarn workspace ${{ inputs.workspace_name }} bundle --no-compress --output-directory ${{ steps.bundle-dir.outputs.temp_dir }}
lint:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v3
- name: Cache build
id: cache-build
uses: actions/cache@v3
with:
path: |
packages/**/dist
${{ needs.build.outputs.temp_dir }}
key: ${{ runner.os }}-${{ inputs.service_name }}-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 ${{ inputs.package_path }}
- name: Lint
run: yarn lint:${{ inputs.service_name }}
test:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v3
- name: Cache build
id: cache-build
uses: actions/cache@v3
with:
path: |
packages/**/dist
${{ needs.build.outputs.temp_dir }}
key: ${{ runner.os }}-${{ inputs.service_name }}-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 ${{ inputs.package_path }}
- name: Test
run: yarn test ${{ inputs.package_path }}
publish:
needs: [ build, test, lint ]
name: Publish Docker Image
uses: standardnotes/server/.github/workflows/common-docker-image.yml@main
with:
service_name: ${{ inputs.service_name }}
bundle_dir: ${{ needs.build.outputs.temp_dir }}
package_path: ${{ inputs.package_path }}
workspace_name: ${{ inputs.workspace_name }}
secrets: inherit
deploy-web:
if: ${{ inputs.deploy_web }}
needs: publish
name: Deploy Web
uses: standardnotes/server/.github/workflows/common-deploy.yml@main
with:
service_name: ${{ inputs.service_name }}
docker_image: ${{ inputs.service_name }}:${{ github.sha }}
secrets: inherit
deploy-worker:
if: ${{ inputs.deploy_worker }}
needs: publish
name: Deploy Worker
uses: standardnotes/server/.github/workflows/common-deploy.yml@main
with:
service_name: ${{ inputs.service_name }}-worker
docker_image: ${{ inputs.service_name }}:${{ github.sha }}
secrets: inherit

View File

@@ -11,19 +11,18 @@ on:
workflow_dispatch:
jobs:
call_server_application_workflow:
name: Server Application
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
call_server_utility_workflow:
name: Server Utility
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
with:
service_name: event-store
workspace_name: "@standardnotes/event-store"
e2e_tag_parameter_name: event_store_image_tag
deploy_web: false
package_path: packages/event-store
secrets: inherit
newrelic:
needs: call_server_application_workflow
needs: call_server_utility_workflow
runs-on: ubuntu-latest

View File

@@ -11,19 +11,18 @@ on:
workflow_dispatch:
jobs:
call_server_application_workflow:
name: Server Application
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
call_server_utility_workflow:
name: Server Utility
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
with:
service_name: scheduler
workspace_name: "@standardnotes/scheduler-server"
e2e_tag_parameter_name: scheduler_image_tag
deploy_web: false
package_path: packages/scheduler
secrets: inherit
newrelic:
needs: call_server_application_workflow
needs: call_server_utility_workflow
runs-on: ubuntu-latest

View File

@@ -11,18 +11,17 @@ on:
workflow_dispatch:
jobs:
call_server_application_workflow:
name: Server Application
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
call_server_utility_workflow:
name: Server Utility
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
with:
service_name: websockets
workspace_name: "@standardnotes/websockets-server"
e2e_tag_parameter_name: websockets_image_tag
package_path: packages/websockets
secrets: inherit
newrelic:
needs: call_server_application_workflow
needs: call_server_utility_workflow
runs-on: ubuntu-latest
steps:

View File

@@ -11,18 +11,17 @@ on:
workflow_dispatch:
jobs:
call_server_application_workflow:
name: Server Application
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
call_server_utility_workflow:
name: Server Utility
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
with:
service_name: workspace
workspace_name: "@standardnotes/workspace-server"
e2e_tag_parameter_name: workspace_image_tag
package_path: packages/workspace
secrets: inherit
newrelic:
needs: call_server_application_workflow
needs: call_server_utility_workflow
runs-on: ubuntu-latest
steps:

65
.pnp.cjs generated
View File

@@ -2690,7 +2690,7 @@ const RAW_RUNTIME_STATE =
["@types/express", "npm:4.17.14"],\
["@types/ioredis", "npm:5.0.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/jsonwebtoken", "npm:8.5.9"],\
["@types/jsonwebtoken", "npm:9.0.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/prettyjson", "npm:0.0.30"],\
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
@@ -2701,12 +2701,13 @@ const RAW_RUNTIME_STATE =
["eslint", "npm:8.25.0"],\
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
["express", "npm:4.18.2"],\
["express-robots-txt", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0"],\
["helmet", "npm:6.0.0"],\
["inversify", "npm:6.0.1"],\
["inversify-express-utils", "npm:6.4.3"],\
["ioredis", "npm:5.2.4"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["jsonwebtoken", "npm:8.5.1"],\
["jsonwebtoken", "npm:9.0.0"],\
["newrelic", "npm:9.6.0"],\
["nodemon", "npm:2.0.20"],\
["npm-check-updates", "npm:16.0.1"],\
@@ -2977,7 +2978,7 @@ const RAW_RUNTIME_STATE =
["@types/express", "npm:4.17.14"],\
["@types/ioredis", "npm:5.0.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/jsonwebtoken", "npm:8.5.9"],\
["@types/jsonwebtoken", "npm:9.0.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/prettyjson", "npm:0.0.30"],\
["@types/uuid", "npm:8.3.4"],\
@@ -2990,13 +2991,14 @@ const RAW_RUNTIME_STATE =
["eslint", "npm:8.25.0"],\
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
["express", "npm:4.18.2"],\
["express-robots-txt", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0"],\
["express-winston", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:4.2.0"],\
["helmet", "npm:6.0.0"],\
["inversify", "npm:6.0.1"],\
["inversify-express-utils", "npm:6.4.3"],\
["ioredis", "npm:5.2.4"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["jsonwebtoken", "npm:8.5.1"],\
["jsonwebtoken", "npm:9.0.0"],\
["newrelic", "npm:9.6.0"],\
["nodemon", "npm:2.0.20"],\
["npm-check-updates", "npm:16.0.1"],\
@@ -3182,11 +3184,11 @@ const RAW_RUNTIME_STATE =
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/common", "workspace:packages/common"],\
["@types/jest", "npm:29.1.1"],\
["@types/jsonwebtoken", "npm:8.5.9"],\
["@types/jsonwebtoken", "npm:9.0.1"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
["eslint-plugin-prettier", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:4.2.1"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["jsonwebtoken", "npm:8.5.1"],\
["jsonwebtoken", "npm:9.0.0"],\
["reflect-metadata", "npm:0.1.13"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"]\
@@ -3287,7 +3289,7 @@ const RAW_RUNTIME_STATE =
["@types/inversify-express-utils", "npm:2.0.0"],\
["@types/ioredis", "npm:5.0.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/jsonwebtoken", "npm:8.5.9"],\
["@types/jsonwebtoken", "npm:9.0.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/prettyjson", "npm:0.0.30"],\
["@types/ua-parser-js", "npm:0.7.36"],\
@@ -3305,7 +3307,7 @@ const RAW_RUNTIME_STATE =
["inversify-express-utils", "npm:6.4.3"],\
["ioredis", "npm:5.2.4"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["jsonwebtoken", "npm:8.5.1"],\
["jsonwebtoken", "npm:9.0.0"],\
["mysql2", "npm:2.3.3"],\
["newrelic", "npm:9.6.0"],\
["nodemon", "npm:2.0.20"],\
@@ -3754,10 +3756,10 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@types/jsonwebtoken", [\
["npm:8.5.9", {\
"packageLocation": "./.yarn/cache/@types-jsonwebtoken-npm-8.5.9-79c2843a81-3f15a76cd5.zip/node_modules/@types/jsonwebtoken/",\
["npm:9.0.1", {\
"packageLocation": "./.yarn/cache/@types-jsonwebtoken-npm-9.0.1-5f660fdf38-44d3fccc6b.zip/node_modules/@types/jsonwebtoken/",\
"packageDependencies": [\
["@types/jsonwebtoken", "npm:8.5.9"],\
["@types/jsonwebtoken", "npm:9.0.1"],\
["@types/node", "npm:18.0.3"]\
],\
"linkType": "HARD"\
@@ -7316,6 +7318,28 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["express-robots-txt", [\
["npm:1.0.0", {\
"packageLocation": "./.yarn/cache/express-robots-txt-npm-1.0.0-dcc8bd8f0a-54f066f6c3.zip/node_modules/express-robots-txt/",\
"packageDependencies": [\
["express-robots-txt", "npm:1.0.0"]\
],\
"linkType": "SOFT"\
}],\
["virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0", {\
"packageLocation": "./.yarn/__virtual__/express-robots-txt-virtual-0a3eb9f2f5/0/cache/express-robots-txt-npm-1.0.0-dcc8bd8f0a-54f066f6c3.zip/node_modules/express-robots-txt/",\
"packageDependencies": [\
["express-robots-txt", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0"],\
["@types/express", "npm:4.17.14"],\
["express", "npm:4.18.2"]\
],\
"packagePeers": [\
"@types/express",\
"express"\
],\
"linkType": "HARD"\
}]\
]],\
["express-winston", [\
["npm:4.2.0", {\
"packageLocation": "./.yarn/cache/express-winston-npm-4.2.0-e4cfb26486-2d4b37671d.zip/node_modules/express-winston/",\
@@ -9783,6 +9807,17 @@ const RAW_RUNTIME_STATE =
["semver", "npm:5.7.1"]\
],\
"linkType": "HARD"\
}],\
["npm:9.0.0", {\
"packageLocation": "./.yarn/cache/jsonwebtoken-npm-9.0.0-36fd1594c0-7ccbd0b7bf.zip/node_modules/jsonwebtoken/",\
"packageDependencies": [\
["jsonwebtoken", "npm:9.0.0"],\
["jws", "npm:3.2.2"],\
["lodash", "npm:4.17.21"],\
["ms", "npm:2.1.3"],\
["semver", "npm:7.3.8"]\
],\
"linkType": "HARD"\
}]\
]],\
["jsrsasign", [\
@@ -12497,6 +12532,14 @@ const RAW_RUNTIME_STATE =
["lru-cache", "npm:6.0.0"]\
],\
"linkType": "HARD"\
}],\
["npm:7.3.8", {\
"packageLocation": "./.yarn/cache/semver-npm-7.3.8-25a996cb4f-94ad80ee14.zip/node_modules/semver/",\
"packageDependencies": [\
["semver", "npm:7.3.8"],\
["lru-cache", "npm:6.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["semver-diff", [\

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.19.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.3...@standardnotes/analytics@2.19.4) (2023-01-17)
### Bug Fixes
* allow to run typeorm in non-replica mode ([f73129c](https://github.com/standardnotes/server/commit/f73129cd7e7d6a9b8a63e5c80284467597557982))
## [2.19.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.2...@standardnotes/analytics@2.19.3) (2023-01-16)
**Note:** Version bump only for package @standardnotes/analytics
## [2.19.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.1...@standardnotes/analytics@2.19.2) (2023-01-13)
**Note:** Version bump only for package @standardnotes/analytics
## [2.19.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.0...@standardnotes/analytics@2.19.1) (2022-12-30)
### Bug Fixes

View File

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

View File

@@ -12,31 +12,41 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false
const replicationConfig = {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST', true),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
restoreNodeTimeout: 5,
}
export const AppDataSource = new DataSource({
type: 'mysql',
charset: 'utf8mb4',
supportBigNumbers: true,
bigNumberStrings: false,
maxQueryExecutionTime,
replication: {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
},
replication: inReplicaMode ? replicationConfig : undefined,
host: inReplicaMode ? undefined : env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')),
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
entities: [AnalyticsEntity, TypeORMRevenueModification],
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
migrationsRun: true,

View File

@@ -3,6 +3,35 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.46.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.45.3...@standardnotes/api-gateway@1.46.0) (2023-01-16)
### Features
* **api-gateway:** add all revisions endpoints on v2 ([60b3dd6](https://github.com/standardnotes/api-gateway/commit/60b3dd6138ef9b8e9a717873548afc2d3924a0d7))
* **api-gateway:** switch to fetching revisions from reivsions server ([22c1f93](https://github.com/standardnotes/api-gateway/commit/22c1f936c3a770a82dc1a1e6aa136e183d308aa6))
## [1.45.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.45.2...@standardnotes/api-gateway@1.45.3) (2023-01-16)
### Bug Fixes
* **api-gateway:** add noindex robots meta tag to api gateway homepage ([04c6888](https://github.com/standardnotes/api-gateway/commit/04c6888cf65f9f1315fc2fb8af069d26bfbc31b1))
## [1.45.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.45.1...@standardnotes/api-gateway@1.45.2) (2023-01-13)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.45.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.45.0...@standardnotes/api-gateway@1.45.1) (2023-01-13)
### Bug Fixes
* add robots.txt setup for api-gateway and files server to disallow indexing ([bb82043](https://github.com/standardnotes/api-gateway/commit/bb820437af2b9644d7597de045b5840037b81db3))
# [1.45.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.44.0...@standardnotes/api-gateway@1.45.0) (2023-01-05)
### Features
* **auth:** add recovery sign in with recovery codes ([cac899a](https://github.com/standardnotes/api-gateway/commit/cac899a7e558d066895dfb3ba28418d94072f2b7))
# [1.44.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.43.0...@standardnotes/api-gateway@1.44.0) (2022-12-29)
### Features

View File

@@ -31,6 +31,8 @@ import helmet from 'helmet'
import * as cors from 'cors'
import { text, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
import * as winston from 'winston'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const robots = require('express-robots-txt')
import { InversifyExpressServer } from 'inversify-express-utils'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
@@ -78,6 +80,12 @@ void container.load().then((container) => {
}),
)
app.use(cors())
app.use(
robots({
UserAgent: '*',
Disallow: '/',
}),
)
if (env.get('SENTRY_DSN', true)) {
Sentry.init({

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.44.0",
"version": "1.46.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -32,11 +32,12 @@
"cors": "2.8.5",
"dotenv": "^16.0.1",
"express": "^4.18.2",
"express-robots-txt": "^1.0.0",
"helmet": "^6.0.0",
"inversify": "^6.0.1",
"inversify-express-utils": "^6.4.3",
"ioredis": "^5.2.4",
"jsonwebtoken": "8.5.1",
"jsonwebtoken": "^9.0.0",
"newrelic": "^9.6.0",
"prettyjson": "^1.2.5",
"reflect-metadata": "0.1.13",
@@ -47,7 +48,7 @@
"@types/express": "^4.17.14",
"@types/ioredis": "^5.0.0",
"@types/jest": "^29.1.1",
"@types/jsonwebtoken": "^8.5.0",
"@types/jsonwebtoken": "^9.0.1",
"@types/newrelic": "^7.0.4",
"@types/prettyjson": "^0.0.30",
"@typescript-eslint/eslint-plugin": "^5.29.0",

View File

@@ -57,7 +57,9 @@ export class LegacyController extends BaseHttpController {
@all('*')
async legacyProxyToSyncingServer(request: Request, response: Response): Promise<void> {
if (request.path === '/') {
response.send('Welcome to the Standard Notes server infrastructure. Learn more at https://docs.standardnotes.com')
response.send(
'<!DOCTYPE html><html lang="en"><head><meta name="robots" content="noindex"></head><body>Welcome to the Standard Notes server infrastructure. Learn more at https://docs.standardnotes.com</body></html>',
)
return
}

View File

@@ -39,4 +39,19 @@ export class ActionsController extends BaseHttpController {
request.body,
)
}
@httpPost('/recovery/codes', TYPES.AuthMiddleware)
async recoveryCodes(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'auth/recovery/codes', request.body)
}
@httpPost('/recovery/login')
async recoveryLogin(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'auth/recovery/login', request.body)
}
@httpPost('/recovery/login-params')
async recoveryParams(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'auth/recovery/params', request.body)
}
}

View File

@@ -1,10 +1,11 @@
import { Request, Response } from 'express'
import { inject } from 'inversify'
import { BaseHttpController, controller, httpGet } from 'inversify-express-utils'
import { BaseHttpController, controller, httpDelete, httpGet } from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
@controller('/v2/items/:item_id/revisions', TYPES.AuthMiddleware)
@controller('/v2/items/:itemUuid/revisions', TYPES.AuthMiddleware)
export class RevisionsControllerV2 extends BaseHttpController {
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
super()
@@ -12,6 +13,24 @@ export class RevisionsControllerV2 extends BaseHttpController {
@httpGet('/')
async getRevisions(request: Request, response: Response): Promise<void> {
await this.httpService.callRevisionsServer(request, response, `items/${request.params.item_id}/revisions`)
await this.httpService.callRevisionsServer(request, response, `items/${request.params.itemUuid}/revisions`)
}
@httpGet('/:id')
async getRevision(request: Request, response: Response): Promise<void> {
await this.httpService.callRevisionsServer(
request,
response,
`items/${request.params.itemUuid}/revisions/${request.params.id}`,
)
}
@httpDelete('/:id')
async deleteRevision(request: Request, response: Response): Promise<void> {
await this.httpService.callRevisionsServer(
request,
response,
`items/${request.params.itemUuid}/revisions/${request.params.id}`,
)
}
}

View File

@@ -67,3 +67,7 @@ VALET_TOKEN_SECRET=
VALET_TOKEN_TTL=
WEB_SOCKET_CONNECTION_TOKEN_SECRET=
# (Optional) U2F Setup
U2F_RELYING_PARTY_ID=
U2F_RELYING_PARTY_NAME=

View File

@@ -3,6 +3,74 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.81.11](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.10...@standardnotes/auth-server@1.81.11) (2023-01-17)
### Bug Fixes
* allow to run typeorm in non-replica mode ([f73129c](https://github.com/standardnotes/server/commit/f73129cd7e7d6a9b8a63e5c80284467597557982))
## [1.81.10](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.9...@standardnotes/auth-server@1.81.10) (2023-01-16)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.81.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.8...@standardnotes/auth-server@1.81.9) (2023-01-13)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.81.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.7...@standardnotes/auth-server@1.81.8) (2023-01-11)
### Bug Fixes
* **auth:** add relying party configuration options ([d18f6cc](https://github.com/standardnotes/server/commit/d18f6ccd32fa97c927781c17659cf7a8e662ee07))
## [1.81.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.6...@standardnotes/auth-server@1.81.7) (2023-01-09)
### Bug Fixes
* **auth:** failure messages for debug logs upon signing in with recovery codes ([7ae8845](https://github.com/standardnotes/server/commit/7ae8845ae9ff9c208d192aea48e5517a16c8338f))
## [1.81.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.5...@standardnotes/auth-server@1.81.6) (2023-01-09)
### Bug Fixes
* **auth:** request parameters names ([dda8d79](https://github.com/standardnotes/server/commit/dda8d795262d6629493377ae5a6143263a792378))
## [1.81.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.4...@standardnotes/auth-server@1.81.5) (2023-01-09)
### Bug Fixes
* **auth:** debuggin recovery sign in ([96669bf](https://github.com/standardnotes/server/commit/96669bff5bc0903f28c51628e9289626622e674c))
## [1.81.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.3...@standardnotes/auth-server@1.81.4) (2023-01-09)
### Bug Fixes
* **auth:** error messages on account recovery ([1fc3c9b](https://github.com/standardnotes/server/commit/1fc3c9b83ee2239b618dfb609b1dc2d68d063331))
## [1.81.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.2...@standardnotes/auth-server@1.81.3) (2023-01-09)
### Bug Fixes
* **auth:** remove mfa settings after recovery sign in ([a0208dd](https://github.com/standardnotes/server/commit/a0208dd5b3ce54ccfa96b3497cb36e16ccb4cf89))
## [1.81.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.1...@standardnotes/auth-server@1.81.2) (2023-01-05)
### Bug Fixes
* **auth:** return type to include user ([db0baf9](https://github.com/standardnotes/server/commit/db0baf92f1336071a8602a1a20ba6439f10028e6))
## [1.81.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.0...@standardnotes/auth-server@1.81.1) (2023-01-05)
### Bug Fixes
* **auth:** allow retrieval of recovery codes setting ([13c5c97](https://github.com/standardnotes/server/commit/13c5c97ba73fcf1f4bd2905a1d8668ef5b468016))
# [1.81.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.80.0...@standardnotes/auth-server@1.81.0) (2023-01-05)
### Features
* **auth:** add recovery sign in with recovery codes ([cac899a](https://github.com/standardnotes/server/commit/cac899a7e558d066895dfb3ba28418d94072f2b7))
# [1.80.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.79.1...@standardnotes/auth-server@1.80.0) (2023-01-04)
### Features

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.80.0",
"version": "1.81.11",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -223,6 +223,8 @@ import { AuthenticatorHttpProjection } from '../Infra/Http/Projection/Authentica
import { AuthenticatorHttpMapper } from '../Mapping/AuthenticatorHttpMapper'
import { DeleteAuthenticator } from '../Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator'
import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes'
import { SignInWithRecoveryCodes } from '../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
import { GetUserKeyParamsRecovery } from '../Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -461,7 +463,12 @@ export class ContainerConfigLoader {
container
.bind(TYPES.SESSION_TRACE_DAYS_TTL)
.toConstantValue(env.get('SESSION_TRACE_DAYS_TTL', true) ? +env.get('SESSION_TRACE_DAYS_TTL', true) : 90)
container
.bind(TYPES.U2F_RELYING_PARTY_NAME)
.toConstantValue(env.get('U2F_RELYING_PARTY_NAME', true) ?? 'Standard Notes')
container
.bind(TYPES.U2F_RELYING_PARTY_ID)
.toConstantValue(env.get('U2F_RELYING_PARTY_ID', true) ?? 'standardnotes.com')
// Services
container.bind<UAParser>(TYPES.DeviceDetector).toConstantValue(new UAParser())
container.bind<SessionService>(TYPES.SessionService).to(SessionService)
@@ -565,6 +572,8 @@ export class ContainerConfigLoader {
new GenerateAuthenticatorRegistrationOptions(
container.get(TYPES.AuthenticatorRepository),
container.get(TYPES.AuthenticatorChallengeRepository),
container.get(TYPES.U2F_RELYING_PARTY_NAME),
container.get(TYPES.U2F_RELYING_PARTY_ID),
),
)
container
@@ -573,6 +582,7 @@ export class ContainerConfigLoader {
new VerifyAuthenticatorRegistrationResponse(
container.get(TYPES.AuthenticatorRepository),
container.get(TYPES.AuthenticatorChallengeRepository),
container.get(TYPES.U2F_RELYING_PARTY_ID),
),
)
container
@@ -589,6 +599,7 @@ export class ContainerConfigLoader {
new VerifyAuthenticatorAuthenticationResponse(
container.get(TYPES.AuthenticatorRepository),
container.get(TYPES.AuthenticatorChallengeRepository),
container.get(TYPES.U2F_RELYING_PARTY_ID),
),
)
container
@@ -617,6 +628,16 @@ export class ContainerConfigLoader {
container.bind<VerifyMFA>(TYPES.VerifyMFA).to(VerifyMFA)
container.bind<ClearLoginAttempts>(TYPES.ClearLoginAttempts).to(ClearLoginAttempts)
container.bind<IncreaseLoginAttempts>(TYPES.IncreaseLoginAttempts).to(IncreaseLoginAttempts)
container
.bind<GetUserKeyParamsRecovery>(TYPES.GetUserKeyParamsRecovery)
.toConstantValue(
new GetUserKeyParamsRecovery(
container.get(TYPES.KeyParamsFactory),
container.get(TYPES.UserRepository),
container.get(TYPES.PKCERepository),
container.get(TYPES.SettingService),
),
)
container.bind<GetUserKeyParams>(TYPES.GetUserKeyParams).to(GetUserKeyParams)
container.bind<UpdateUser>(TYPES.UpdateUser).to(UpdateUser)
container.bind<Register>(TYPES.Register).to(Register)
@@ -629,6 +650,21 @@ export class ContainerConfigLoader {
container.bind<GetUserFeatures>(TYPES.GetUserFeatures).to(GetUserFeatures)
container.bind<UpdateSetting>(TYPES.UpdateSetting).to(UpdateSetting)
container.bind<DeleteSetting>(TYPES.DeleteSetting).to(DeleteSetting)
container
.bind<SignInWithRecoveryCodes>(TYPES.SignInWithRecoveryCodes)
.toConstantValue(
new SignInWithRecoveryCodes(
container.get(TYPES.UserRepository),
container.get(TYPES.AuthResponseFactory20200115),
container.get(TYPES.PKCERepository),
container.get(TYPES.Crypter),
container.get(TYPES.SettingService),
container.get(TYPES.GenerateRecoveryCodes),
container.get(TYPES.IncreaseLoginAttempts),
container.get(TYPES.ClearLoginAttempts),
container.get(TYPES.DeleteSetting),
),
)
container.bind<DeleteAccount>(TYPES.DeleteAccount).to(DeleteAccount)
container.bind<GetUserSubscription>(TYPES.GetUserSubscription).to(GetUserSubscription)
container.bind<GetUserOfflineSubscription>(TYPES.GetUserOfflineSubscription).to(GetUserOfflineSubscription)

View File

@@ -22,31 +22,41 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false
const replicationConfig = {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST', true),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
restoreNodeTimeout: 5,
}
export const AppDataSource = new DataSource({
type: 'mysql',
charset: 'utf8mb4',
supportBigNumbers: true,
bigNumberStrings: false,
maxQueryExecutionTime,
replication: {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
},
replication: inReplicaMode ? replicationConfig : undefined,
host: inReplicaMode ? undefined : env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')),
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
entities: [
User,
UserSubscription,

View File

@@ -94,6 +94,8 @@ const TYPES = {
VERSION: Symbol.for('VERSION'),
PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
SESSION_TRACE_DAYS_TTL: Symbol.for('SESSION_TRACE_DAYS_TTL'),
U2F_RELYING_PARTY_ID: Symbol.for('U2F_RELYING_PARTY_ID'),
U2F_RELYING_PARTY_NAME: Symbol.for('U2F_RELYING_PARTY_NAME'),
// use cases
AuthenticateUser: Symbol.for('AuthenticateUser'),
AuthenticateRequest: Symbol.for('AuthenticateRequest'),
@@ -142,6 +144,8 @@ const TYPES = {
ListAuthenticators: Symbol.for('ListAuthenticators'),
DeleteAuthenticator: Symbol.for('DeleteAuthenticator'),
GenerateRecoveryCodes: Symbol.for('GenerateRecoveryCodes'),
SignInWithRecoveryCodes: Symbol.for('SignInWithRecoveryCodes'),
GetUserKeyParamsRecovery: Symbol.for('GetUserKeyParamsRecovery'),
// Handlers
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),

View File

@@ -9,6 +9,10 @@ import { Register } from '../Domain/UseCase/Register'
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
import { ApiVersion } from '@standardnotes/api'
import { SignInWithRecoveryCodes } from '../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
import { GetUserKeyParamsRecovery } from '../Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery'
import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes'
import { Logger } from 'winston'
describe('AuthController', () => {
let clearLoginAttempts: ClearLoginAttempts
@@ -17,9 +21,22 @@ describe('AuthController', () => {
let domainEventFactory: DomainEventFactoryInterface
let event: DomainEventInterface
let user: User
let doSignInWithRecoveryCodes: SignInWithRecoveryCodes
let getUserKeyParamsRecovery: GetUserKeyParamsRecovery
let doGenerateRecoveryCodes: GenerateRecoveryCodes
let logger: Logger
const createController = () =>
new AuthController(clearLoginAttempts, register, domainEventPublisher, domainEventFactory)
new AuthController(
clearLoginAttempts,
register,
domainEventPublisher,
domainEventFactory,
doSignInWithRecoveryCodes,
getUserKeyParamsRecovery,
doGenerateRecoveryCodes,
logger,
)
beforeEach(() => {
register = {} as jest.Mocked<Register>
@@ -38,6 +55,9 @@ describe('AuthController', () => {
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createUserRegisteredEvent = jest.fn().mockReturnValue(event)
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
})
it('should register a user', async () => {
@@ -113,7 +133,7 @@ describe('AuthController', () => {
it('should throw error on the delete user method as it is still a part of the payments server', async () => {
let caughtError = null
try {
await createController().deleteAccount({ userUuid: '1-2-3' })
await createController().deleteAccount({} as never)
} catch (error) {
caughtError = error
}

View File

@@ -1,19 +1,29 @@
import { inject, injectable } from 'inversify'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import {
ApiVersion,
HttpStatusCode,
UserDeletionResponse,
UserRegistrationRequestParams,
UserRegistrationResponse,
UserServerInterface,
} from '@standardnotes/api'
import { ProtocolVersion } from '@standardnotes/common'
import TYPES from '../Bootstrap/Types'
import { ClearLoginAttempts } from '../Domain/UseCase/ClearLoginAttempts'
import { Register } from '../Domain/UseCase/Register'
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
import { ProtocolVersion } from '@standardnotes/common'
import { UserDeletionRequestParams } from '@standardnotes/api/dist/Domain/Request/User/UserDeletionRequestParams'
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 { 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()
export class AuthController implements UserServerInterface {
@@ -22,9 +32,13 @@ export class AuthController implements UserServerInterface {
@inject(TYPES.Register) private registerUser: Register,
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.SignInWithRecoveryCodes) private doSignInWithRecoveryCodes: SignInWithRecoveryCodes,
@inject(TYPES.GetUserKeyParamsRecovery) private getUserKeyParamsRecovery: GetUserKeyParamsRecovery,
@inject(TYPES.GenerateRecoveryCodes) private doGenerateRecoveryCodes: GenerateRecoveryCodes,
@inject(TYPES.Logger) private logger: Logger,
) {}
async deleteAccount(_params: UserDeletionRequestParams): Promise<UserDeletionResponse> {
async deleteAccount(_params: never): Promise<UserDeletionResponse> {
throw new Error('This method is implemented on the payments server.')
}
@@ -78,4 +92,108 @@ export class AuthController implements UserServerInterface {
data: registerResult.authResponse,
}
}
async generateRecoveryCodes(params: GenerateRecoveryCodesRequestParams): Promise<GenerateRecoveryCodesResponse> {
const result = await this.doGenerateRecoveryCodes.execute({
userUuid: params.userUuid,
})
if (result.isFailed()) {
return {
status: HttpStatusCode.BadRequest,
data: {
error: {
message: 'Could not generate recovery codes.',
},
},
}
}
return {
status: HttpStatusCode.Success,
data: {
recoveryCodes: result.getValue(),
},
}
}
async signInWithRecoveryCodes(
params: SignInWithRecoveryCodesRequestParams,
): Promise<SignInWithRecoveryCodesResponse> {
if (params.apiVersion !== ApiVersion.v0) {
return {
status: HttpStatusCode.BadRequest,
data: {
error: {
message: 'Invalid API version.',
},
},
}
}
const result = await this.doSignInWithRecoveryCodes.execute({
userAgent: params.userAgent,
username: params.username,
password: params.password,
codeVerifier: params.codeVerifier,
recoveryCodes: params.recoveryCodes,
})
if (result.isFailed()) {
this.logger.debug(`Failed to sign in with recovery codes: ${result.getError()}`)
return {
status: HttpStatusCode.Unauthorized,
data: {
error: {
message: 'Invalid login credentials.',
},
},
}
}
return {
status: HttpStatusCode.Success,
data: result.getValue(),
}
}
async recoveryKeyParams(params: RecoveryKeyParamsRequestParams): Promise<RecoveryKeyParamsResponse> {
if (params.apiVersion !== ApiVersion.v0) {
return {
status: HttpStatusCode.BadRequest,
data: {
error: {
message: 'Invalid API version.',
},
},
}
}
const result = await this.getUserKeyParamsRecovery.execute({
username: params.username,
codeChallenge: params.codeChallenge,
recoveryCodes: params.recoveryCodes,
})
if (result.isFailed()) {
this.logger.debug(`Failed to get recovery key params: ${result.getError()}`)
return {
status: HttpStatusCode.Unauthorized,
data: {
error: {
message: 'Invalid login credentials.',
},
},
}
}
return {
status: HttpStatusCode.Success,
data: {
keyParams: result.getValue(),
},
}
}
}

View File

@@ -1,4 +0,0 @@
export enum RelyingParty {
RP_NAME = 'Standard Notes',
RP_ID = 'standardnotes.com',
}

View File

@@ -11,7 +11,12 @@ describe('GenerateAuthenticatorRegistrationOptions', () => {
let authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface
const createUseCase = () =>
new GenerateAuthenticatorRegistrationOptions(authenticatorRepository, authenticatorChallengeRepository)
new GenerateAuthenticatorRegistrationOptions(
authenticatorRepository,
authenticatorChallengeRepository,
'Standard Notes',
'standardnotes.com',
)
beforeEach(() => {
const authenticator = Authenticator.create({

View File

@@ -5,12 +5,13 @@ import { GenerateAuthenticatorRegistrationOptionsDTO } from './GenerateAuthentic
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge'
import { RelyingParty } from '../../Authenticator/RelyingParty'
export class GenerateAuthenticatorRegistrationOptions implements UseCaseInterface<Record<string, unknown>> {
constructor(
private authenticatorRepository: AuthenticatorRepositoryInterface,
private authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface,
private relyingPartyName: string,
private relyingPartyId: string,
) {}
async execute(dto: GenerateAuthenticatorRegistrationOptionsDTO): Promise<Result<Record<string, unknown>>> {
@@ -28,8 +29,8 @@ export class GenerateAuthenticatorRegistrationOptions implements UseCaseInterfac
const authenticators = await this.authenticatorRepository.findByUserUuid(userUuid)
const options = generateRegistrationOptions({
rpID: RelyingParty.RP_ID,
rpName: RelyingParty.RP_NAME,
rpID: this.relyingPartyId,
rpName: this.relyingPartyName,
userID: userUuid.value,
userName: username.value,
attestationType: 'none',

View File

@@ -38,7 +38,7 @@ export class GenerateRecoveryCodes implements UseCaseInterface<string> {
name: SettingName.RecoveryCodes,
unencryptedValue: recoveryCodes,
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: true,
sensitive: false,
},
})

View File

@@ -0,0 +1,116 @@
import { Setting } from '../../Setting/Setting'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
import { KeyParamsFactoryInterface } from '../../User/KeyParamsFactoryInterface'
import { PKCERepositoryInterface } from '../../User/PKCERepositoryInterface'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GetUserKeyParamsRecovery } from './GetUserKeyParamsRecovery'
describe('GetUserKeyParamsRecovery', () => {
let keyParamsFactory: KeyParamsFactoryInterface
let userRepository: UserRepositoryInterface
let settingService: SettingServiceInterface
let user: User
let pkceRepository: PKCERepositoryInterface
const createUseCase = () =>
new GetUserKeyParamsRecovery(keyParamsFactory, userRepository, pkceRepository, settingService)
beforeEach(() => {
keyParamsFactory = {} as jest.Mocked<KeyParamsFactoryInterface>
keyParamsFactory.create = jest.fn().mockReturnValue({ foo: 'bar' })
keyParamsFactory.createPseudoParams = jest.fn().mockReturnValue({ bar: 'baz' })
user = {} as jest.Mocked<User>
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
settingService = {} as jest.Mocked<SettingServiceInterface>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({ value: 'foo' } as Setting)
pkceRepository = {} as jest.Mocked<PKCERepositoryInterface>
pkceRepository.storeCodeChallenge = jest.fn()
})
it('should return error if code challenge is not provided', async () => {
const result = await createUseCase().execute({
username: 'username',
codeChallenge: '',
recoveryCodes: '1234 5678',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Invalid code challenge')
})
it('should return error if username is not provided', async () => {
const result = await createUseCase().execute({
username: '',
codeChallenge: 'code-challenge',
recoveryCodes: '1234 5678',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Could not sign in with recovery codes: Username cannot be empty')
})
it('should return error if recovery codes are not provided', async () => {
const result = await createUseCase().execute({
username: 'username',
codeChallenge: 'codeChallenge',
recoveryCodes: '',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Invalid recovery codes')
})
it('should return pseudo params if user does not exist', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
const result = await createUseCase().execute({
username: 'username',
codeChallenge: 'codeChallenge',
recoveryCodes: '1234 5678',
})
expect(keyParamsFactory.createPseudoParams).toHaveBeenCalled()
expect(result.isFailed()).toBe(false)
})
it('should return error if user has no recovery codes generated', async () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
const result = await createUseCase().execute({
username: 'username',
codeChallenge: 'codeChallenge',
recoveryCodes: '1234 5678',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('User does not have recovery codes generated')
})
it('should return error if recovery codes do not match', async () => {
const result = await createUseCase().execute({
username: 'username',
codeChallenge: 'codeChallenge',
recoveryCodes: '1234 5678',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Invalid recovery codes')
})
it('should return key params if recovery codes match', async () => {
const result = await createUseCase().execute({
username: 'username',
codeChallenge: 'codeChallenge',
recoveryCodes: 'foo',
})
expect(keyParamsFactory.create).toHaveBeenCalled()
expect(result.isFailed()).toBe(false)
})
})

View File

@@ -0,0 +1,64 @@
import { KeyParamsData } from '@standardnotes/responses'
import { Result, UseCaseInterface, Username, Validator } from '@standardnotes/domain-core'
import { SettingName } from '@standardnotes/settings'
import { KeyParamsFactoryInterface } from '../../User/KeyParamsFactoryInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GetUserKeyParamsRecoveryDTO } from './GetUserKeyParamsRecoveryDTO'
import { User } from '../../User/User'
import { PKCERepositoryInterface } from '../../User/PKCERepositoryInterface'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
export class GetUserKeyParamsRecovery implements UseCaseInterface<KeyParamsData> {
constructor(
private keyParamsFactory: KeyParamsFactoryInterface,
private userRepository: UserRepositoryInterface,
private pkceRepository: PKCERepositoryInterface,
private settingService: SettingServiceInterface,
) {}
async execute(dto: GetUserKeyParamsRecoveryDTO): Promise<Result<KeyParamsData>> {
const usernameOrError = Username.create(dto.username)
if (usernameOrError.isFailed()) {
return Result.fail(`Could not sign in with recovery codes: ${usernameOrError.getError()}`)
}
const username = usernameOrError.getValue()
const recoveryCodesValidationResult = Validator.isNotEmpty(dto.recoveryCodes)
if (recoveryCodesValidationResult.isFailed()) {
return Result.fail('Invalid recovery codes')
}
const codeChallengeValidationResult = Validator.isNotEmpty(dto.codeChallenge)
if (codeChallengeValidationResult.isFailed()) {
return Result.fail('Invalid code challenge')
}
const user = await this.userRepository.findOneByEmail(username.value)
if (!user) {
return Result.ok(this.keyParamsFactory.createPseudoParams(username.value))
}
const recoveryCodesSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.RecoveryCodes,
userUuid: user.uuid,
})
if (!recoveryCodesSetting) {
return Result.fail('User does not have recovery codes generated')
}
if (recoveryCodesSetting.value !== dto.recoveryCodes) {
return Result.fail('Invalid recovery codes')
}
const keyParams = await this.createKeyParams(dto.codeChallenge, user)
return Result.ok(keyParams)
}
private async createKeyParams(codeChallenge: string, user: User): Promise<KeyParamsData> {
await this.pkceRepository.storeCodeChallenge(codeChallenge)
return this.keyParamsFactory.create(user, false)
}
}

View File

@@ -0,0 +1,5 @@
export interface GetUserKeyParamsRecoveryDTO {
codeChallenge: string
username: string
recoveryCodes: string
}

View File

@@ -0,0 +1,225 @@
import { Result } from '@standardnotes/domain-core'
import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
import { AuthResponseFactory20200115 } from '../../Auth/AuthResponseFactory20200115'
import { CrypterInterface } from '../../Encryption/CrypterInterface'
import { Setting } from '../../Setting/Setting'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
import { PKCERepositoryInterface } from '../../User/PKCERepositoryInterface'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { ClearLoginAttempts } from '../ClearLoginAttempts'
import { DeleteSetting } from '../DeleteSetting/DeleteSetting'
import { GenerateRecoveryCodes } from '../GenerateRecoveryCodes/GenerateRecoveryCodes'
import { IncreaseLoginAttempts } from '../IncreaseLoginAttempts'
import { SignInWithRecoveryCodes } from './SignInWithRecoveryCodes'
describe('SignInWithRecoveryCodes', () => {
let userRepository: UserRepositoryInterface
let authResponseFactory: AuthResponseFactory20200115
let pkceRepository: PKCERepositoryInterface
let crypter: CrypterInterface
let settingService: SettingServiceInterface
let generateRecoveryCodes: GenerateRecoveryCodes
let increaseLoginAttempts: IncreaseLoginAttempts
let clearLoginAttempts: ClearLoginAttempts
let deleteSetting: DeleteSetting
const createUseCase = () =>
new SignInWithRecoveryCodes(
userRepository,
authResponseFactory,
pkceRepository,
crypter,
settingService,
generateRecoveryCodes,
increaseLoginAttempts,
clearLoginAttempts,
deleteSetting,
)
beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByEmail = jest.fn().mockReturnValue({
uuid: '1-2-3',
encryptedPassword: '$2a$11$K3g6XoTau8VmLJcai1bB0eD9/YvBSBRtBhMprJOaVZ0U3SgasZH3a',
} as jest.Mocked<User>)
authResponseFactory = {} as jest.Mocked<AuthResponseFactory20200115>
authResponseFactory.createResponse = jest.fn().mockReturnValue({} as jest.Mocked<AuthResponse20200115>)
pkceRepository = {} as jest.Mocked<PKCERepositoryInterface>
pkceRepository.removeCodeChallenge = jest.fn().mockReturnValue(true)
crypter = {} as jest.Mocked<CrypterInterface>
crypter.base64URLEncode = jest.fn().mockReturnValue('base64-url-encoded')
crypter.sha256Hash = jest.fn().mockReturnValue('sha256-hashed')
settingService = {} as jest.Mocked<SettingServiceInterface>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({ value: 'foo' } as Setting)
generateRecoveryCodes = {} as jest.Mocked<GenerateRecoveryCodes>
generateRecoveryCodes.execute = jest.fn().mockReturnValue(Result.ok('1234 5678'))
increaseLoginAttempts = {} as jest.Mocked<IncreaseLoginAttempts>
increaseLoginAttempts.execute = jest.fn()
clearLoginAttempts = {} as jest.Mocked<ClearLoginAttempts>
clearLoginAttempts.execute = jest.fn()
deleteSetting = {} as jest.Mocked<DeleteSetting>
deleteSetting.execute = jest.fn()
})
it('should return error if password is not provided', async () => {
const result = await createUseCase().execute({
userAgent: 'user-agent',
username: 'test@test.te',
password: '',
codeVerifier: 'code-verifier',
recoveryCodes: '1234 5678',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Empty password')
})
it('should return error if username is not provided', async () => {
const result = await createUseCase().execute({
userAgent: 'user-agent',
username: '',
password: 'qweqwe123123',
codeVerifier: 'code-verifier',
recoveryCodes: '1234 5678',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Could not sign in with recovery codes: Username cannot be empty')
})
it('should return error if code verifier is not provided', async () => {
const result = await createUseCase().execute({
userAgent: 'user-agent',
username: 'username',
password: 'qweqwe123123',
codeVerifier: '',
recoveryCodes: '1234 5678',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Invalid code verifier')
})
it('should return error if recovery codes are not provided', async () => {
const result = await createUseCase().execute({
userAgent: 'user-agent',
username: 'username',
password: 'qweqwe123123',
codeVerifier: 'code-verifier',
recoveryCodes: '',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Empty recovery codes')
})
it('should return error if code verifier is invalid', async () => {
pkceRepository.removeCodeChallenge = jest.fn().mockReturnValue(false)
const result = await createUseCase().execute({
userAgent: 'user-agent',
username: 'test@test.te',
password: 'qweqwe123123',
codeVerifier: 'code-verifier',
recoveryCodes: '1234 5678',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Invalid code verifier')
})
it('should return error if user is not found', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(undefined)
const result = await createUseCase().execute({
userAgent: 'user-agent',
username: 'test@test.te',
password: 'qweqwe123123',
codeVerifier: 'code-verifier',
recoveryCodes: '1234 5678',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Could not find user')
})
it('should return error if recovery codes are invalid', async () => {
const result = await createUseCase().execute({
userAgent: 'user-agent',
username: 'test@test.te',
password: 'qweqwe123123',
codeVerifier: 'code-verifier',
recoveryCodes: '1234 5678',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Invalid recovery codes')
})
it('should return error if password does not match', async () => {
const result = await createUseCase().execute({
userAgent: 'user-agent',
username: 'test@test.te',
password: 'asdasd123123',
codeVerifier: 'code-verifier',
recoveryCodes: '1234 5678',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Invalid password')
})
it('should return error if recovery codes are not generated for user', async () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
const result = await createUseCase().execute({
userAgent: 'user-agent',
username: 'test@test.te',
password: 'qweqwe123123',
codeVerifier: 'code-verifier',
recoveryCodes: '1234 5678',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('User does not have recovery codes generated')
})
it('should return error if generating new recovery codes fails', async () => {
generateRecoveryCodes.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const result = await createUseCase().execute({
userAgent: 'user-agent',
username: 'test@test.te',
password: 'qweqwe123123',
codeVerifier: 'code-verifier',
recoveryCodes: 'foo',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Could not sign in with recovery codes: Oops')
})
it('should return auth response', async () => {
const result = await createUseCase().execute({
userAgent: 'user-agent',
username: 'test@test.te',
password: 'qweqwe123123',
codeVerifier: 'code-verifier',
recoveryCodes: 'foo',
})
expect(clearLoginAttempts.execute).toHaveBeenCalled()
expect(deleteSetting.execute).toHaveBeenCalled()
expect(result.isFailed()).toBe(false)
})
})

View File

@@ -0,0 +1,130 @@
import * as bcrypt from 'bcryptjs'
import { Result, UseCaseInterface, Username, Validator } from '@standardnotes/domain-core'
import { SettingName } from '@standardnotes/settings'
import { ApiVersion } from '@standardnotes/api'
import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
import { CrypterInterface } from '../../Encryption/CrypterInterface'
import { PKCERepositoryInterface } from '../../User/PKCERepositoryInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GenerateRecoveryCodes } from '../GenerateRecoveryCodes/GenerateRecoveryCodes'
import { SignInWithRecoveryCodesDTO } from './SignInWithRecoveryCodesDTO'
import { AuthResponseFactory20200115 } from '../../Auth/AuthResponseFactory20200115'
import { IncreaseLoginAttempts } from '../IncreaseLoginAttempts'
import { ClearLoginAttempts } from '../ClearLoginAttempts'
import { DeleteSetting } from '../DeleteSetting/DeleteSetting'
export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse20200115> {
constructor(
private userRepository: UserRepositoryInterface,
private authResponseFactory: AuthResponseFactory20200115,
private pkceRepository: PKCERepositoryInterface,
private crypter: CrypterInterface,
private settingService: SettingServiceInterface,
private generateRecoveryCodes: GenerateRecoveryCodes,
private increaseLoginAttempts: IncreaseLoginAttempts,
private clearLoginAttempts: ClearLoginAttempts,
private deleteSetting: DeleteSetting,
) {}
async execute(dto: SignInWithRecoveryCodesDTO): Promise<Result<AuthResponse20200115>> {
const usernameOrError = Username.create(dto.username)
if (usernameOrError.isFailed()) {
return Result.fail(`Could not sign in with recovery codes: ${usernameOrError.getError()}`)
}
const username = usernameOrError.getValue()
const validCodeVerifier = await this.validateCodeVerifier(dto.codeVerifier)
if (!validCodeVerifier) {
await this.increaseLoginAttempts.execute({ email: username.value })
return Result.fail('Invalid code verifier')
}
const passwordValidationResult = Validator.isNotEmpty(dto.password)
if (passwordValidationResult.isFailed()) {
await this.increaseLoginAttempts.execute({ email: username.value })
return Result.fail('Empty password')
}
const recoveryCodesValidationResult = Validator.isNotEmpty(dto.recoveryCodes)
if (recoveryCodesValidationResult.isFailed()) {
await this.increaseLoginAttempts.execute({ email: username.value })
return Result.fail('Empty recovery codes')
}
const user = await this.userRepository.findOneByEmail(username.value)
if (!user) {
await this.increaseLoginAttempts.execute({ email: username.value })
return Result.fail('Could not find user')
}
const passwordMatches = await bcrypt.compare(dto.password, user.encryptedPassword)
if (!passwordMatches) {
await this.increaseLoginAttempts.execute({ email: username.value })
return Result.fail('Invalid password')
}
const recoveryCodesSetting = await this.settingService.findSettingWithDecryptedValue({
settingName: SettingName.RecoveryCodes,
userUuid: user.uuid,
})
if (!recoveryCodesSetting) {
await this.increaseLoginAttempts.execute({ email: username.value })
return Result.fail('User does not have recovery codes generated')
}
if (recoveryCodesSetting.value !== dto.recoveryCodes) {
await this.increaseLoginAttempts.execute({ email: username.value })
return Result.fail('Invalid recovery codes')
}
const authResponse = await this.authResponseFactory.createResponse({
user,
apiVersion: ApiVersion.v0,
userAgent: dto.userAgent,
ephemeralSession: false,
readonlyAccess: false,
})
const generateNewRecoveryCodesResult = await this.generateRecoveryCodes.execute({
userUuid: user.uuid,
})
if (generateNewRecoveryCodesResult.isFailed()) {
await this.increaseLoginAttempts.execute({ email: username.value })
return Result.fail(`Could not sign in with recovery codes: ${generateNewRecoveryCodesResult.getError()}`)
}
await this.deleteSetting.execute({
settingName: SettingName.MfaSecret,
userUuid: user.uuid,
})
await this.clearLoginAttempts.execute({ email: username.value })
return Result.ok(authResponse as AuthResponse20200115)
}
private async validateCodeVerifier(codeVerifier: string): Promise<boolean> {
const codeEmptinessVerificationResult = Validator.isNotEmpty(codeVerifier)
if (codeEmptinessVerificationResult.isFailed()) {
return false
}
const codeChallenge = this.crypter.base64URLEncode(this.crypter.sha256Hash(codeVerifier))
const matchingCodeChallengeWasPresentAndRemoved = await this.pkceRepository.removeCodeChallenge(codeChallenge)
return matchingCodeChallengeWasPresentAndRemoved
}
}

View File

@@ -0,0 +1,7 @@
export interface SignInWithRecoveryCodesDTO {
userAgent: string
username: string
password: string
codeVerifier: string
recoveryCodes: string
}

View File

@@ -13,7 +13,11 @@ describe('VerifyAuthenticatorAuthenticationResponse', () => {
let authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface
const createUseCase = () =>
new VerifyAuthenticatorAuthenticationResponse(authenticatorRepository, authenticatorChallengeRepository)
new VerifyAuthenticatorAuthenticationResponse(
authenticatorRepository,
authenticatorChallengeRepository,
'standardnotes.com',
)
beforeEach(() => {
const authenticator = Authenticator.create({

View File

@@ -5,12 +5,12 @@ import { AuthenticatorDevice } from '@simplewebauthn/typescript-types'
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { VerifyAuthenticatorAuthenticationResponseDTO } from './VerifyAuthenticatorAuthenticationResponseDTO'
import { RelyingParty } from '../../Authenticator/RelyingParty'
export class VerifyAuthenticatorAuthenticationResponse implements UseCaseInterface<boolean> {
constructor(
private authenticatorRepository: AuthenticatorRepositoryInterface,
private authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface,
private relyingPartyId: string,
) {}
async execute(dto: VerifyAuthenticatorAuthenticationResponseDTO): Promise<Result<boolean>> {
@@ -40,8 +40,8 @@ export class VerifyAuthenticatorAuthenticationResponse implements UseCaseInterfa
verification = await verifyAuthenticationResponse({
credential: dto.authenticationCredential,
expectedChallenge: authenticatorChallenge.props.challenge.toString(),
expectedOrigin: `https://${RelyingParty.RP_ID}`,
expectedRPID: RelyingParty.RP_ID,
expectedOrigin: `https://${this.relyingPartyId}`,
expectedRPID: this.relyingPartyId,
authenticator: {
counter: authenticator.props.counter,
credentialID: authenticator.props.credentialId,

View File

@@ -13,7 +13,11 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
let authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface
const createUseCase = () =>
new VerifyAuthenticatorRegistrationResponse(authenticatorRepository, authenticatorChallengeRepository)
new VerifyAuthenticatorRegistrationResponse(
authenticatorRepository,
authenticatorChallengeRepository,
'standardnotes.com',
)
beforeEach(() => {
authenticatorRepository = {} as jest.Mocked<AuthenticatorRepositoryInterface>

View File

@@ -2,7 +2,6 @@ import { Dates, Result, UseCaseInterface, Uuid, Validator } from '@standardnotes
import { VerifiedRegistrationResponse, verifyRegistrationResponse } from '@simplewebauthn/server'
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
import { RelyingParty } from '../../Authenticator/RelyingParty'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { Authenticator } from '../../Authenticator/Authenticator'
import { VerifyAuthenticatorRegistrationResponseDTO } from './VerifyAuthenticatorRegistrationResponseDTO'
@@ -11,6 +10,7 @@ export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface
constructor(
private authenticatorRepository: AuthenticatorRepositoryInterface,
private authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface,
private relyingPartyId: string,
) {}
async execute(dto: VerifyAuthenticatorRegistrationResponseDTO): Promise<Result<boolean>> {
@@ -35,8 +35,8 @@ export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface
verification = await verifyRegistrationResponse({
credential: dto.registrationCredential,
expectedChallenge: authenticatorChallenge.props.challenge.toString(),
expectedOrigin: `https://${RelyingParty.RP_ID}`,
expectedRPID: RelyingParty.RP_ID,
expectedOrigin: `https://${this.relyingPartyId}`,
expectedRPID: this.relyingPartyId,
})
if (!verification.verified) {

View File

@@ -0,0 +1,3 @@
export interface GenerateRecoveryCodesRequestParams {
userUuid: string
}

View File

@@ -0,0 +1,6 @@
export interface RecoveryKeyParamsRequestParams {
apiVersion: string
username: string
codeChallenge: string
recoveryCodes: string
}

View File

@@ -0,0 +1,8 @@
export interface SignInWithRecoveryCodesRequestParams {
apiVersion: string
userAgent: string
username: string
password: string
codeVerifier: string
recoveryCodes: string
}

View File

@@ -0,0 +1,8 @@
import { HttpErrorResponseBody, HttpResponse } from '@standardnotes/api'
import { Either } from '@standardnotes/common'
import { GenerateRecoveryCodesResponseBody } from './GenerateRecoveryCodesResponseBody'
export interface GenerateRecoveryCodesResponse extends HttpResponse {
data: Either<GenerateRecoveryCodesResponseBody, HttpErrorResponseBody>
}

View File

@@ -0,0 +1,3 @@
export interface GenerateRecoveryCodesResponseBody {
recoveryCodes: string
}

View File

@@ -0,0 +1,8 @@
import { HttpErrorResponseBody, HttpResponse } from '@standardnotes/api'
import { Either } from '@standardnotes/common'
import { RecoveryKeyParamsResponseBody } from './RecoveryKeyParamsResponseBody'
export interface RecoveryKeyParamsResponse extends HttpResponse {
data: Either<RecoveryKeyParamsResponseBody, HttpErrorResponseBody>
}

View File

@@ -0,0 +1,5 @@
import { KeyParamsData } from '@standardnotes/responses'
export interface RecoveryKeyParamsResponseBody {
keyParams: KeyParamsData
}

View File

@@ -0,0 +1,8 @@
import { HttpErrorResponseBody, HttpResponse } from '@standardnotes/api'
import { Either } from '@standardnotes/common'
import { SignInWithRecoveryCodesResponseBody } from './SignInWithRecoveryCodesResponseBody'
export interface SignInWithRecoveryCodesResponse extends HttpResponse {
data: Either<SignInWithRecoveryCodesResponseBody, HttpErrorResponseBody>
}

View File

@@ -0,0 +1,11 @@
import { KeyParamsData, SessionBody } from '@standardnotes/responses'
export interface SignInWithRecoveryCodesResponseBody {
session: SessionBody
key_params: KeyParamsData
user: {
uuid: string
email: string
protocolVersion: string
}
}

View File

@@ -252,6 +252,41 @@ export class InversifyExpressAuthController extends BaseHttpController {
return this.json(signInResult.authResponse)
}
@httpPost('/recovery/codes', TYPES.ApiGatewayAuthMiddleware)
async generateRecoveryCodes(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authController.generateRecoveryCodes({
userUuid: response.locals.user.uuid,
})
return this.json(result.data, result.status)
}
@httpPost('/recovery/login', TYPES.LockMiddleware)
async recoveryLogin(request: Request): Promise<results.JsonResult> {
const result = await this.authController.signInWithRecoveryCodes({
apiVersion: request.body.api_version,
userAgent: <string>request.headers['user-agent'],
codeVerifier: request.body.code_verifier,
username: request.body.username,
recoveryCodes: request.body.recovery_codes,
password: request.body.password,
})
return this.json(result.data, result.status)
}
@httpPost('/recovery/params')
async recoveryParams(request: Request): Promise<results.JsonResult> {
const result = await this.authController.recoveryKeyParams({
apiVersion: request.body.api_version,
username: request.body.username,
codeChallenge: request.body.code_challenge,
recoveryCodes: request.body.recovery_codes,
})
return this.json(result.data, result.status)
}
@httpPost('/sign_out', TYPES.AuthMiddlewareWithoutResponse)
async signOut(request: Request, response: Response): Promise<results.JsonResult | void> {
if (response.locals.readOnlyAccess) {

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.11.0...@standardnotes/domain-core@1.11.1) (2023-01-16)
### Bug Fixes
* **revisions:** add required role to revisions list response ([e7beee2](https://github.com/standardnotes/server/commit/e7beee278871d2939b058d842404fd6980d7f48a))
# [1.11.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.10.0...@standardnotes/domain-core@1.11.0) (2022-12-15)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-core",
"version": "1.11.0",
"version": "1.11.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -0,0 +1,5 @@
import { Result } from '../Core/Result'
export interface SyncUseCaseInterface<T> {
execute(...args: any[]): Result<T>
}

View File

@@ -35,4 +35,5 @@ export * from './Mapping/MapperInterface'
export * from './Subscription/SubscriptionPlanName'
export * from './Subscription/SubscriptionPlanNameProps'
export * from './UseCase/SyncUseCaseInterface'
export * from './UseCase/UseCaseInterface'

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.60](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.59...@standardnotes/domain-events-infra@1.9.60) (2023-01-13)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.59](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.58...@standardnotes/domain-events-infra@1.9.59) (2022-12-20)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

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

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.105.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.105.1...@standardnotes/domain-events@2.105.2) (2023-01-13)
**Note:** Version bump only for package @standardnotes/domain-events
## [2.105.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.105.0...@standardnotes/domain-events@2.105.1) (2022-12-20)
### Bug Fixes

View File

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

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.
## [1.6.58](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.57...@standardnotes/event-store@1.6.58) (2023-01-17)
### Bug Fixes
* allow to run typeorm in non-replica mode ([f73129c](https://github.com/standardnotes/server/commit/f73129cd7e7d6a9b8a63e5c80284467597557982))
## [1.6.57](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.56...@standardnotes/event-store@1.6.57) (2023-01-13)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.56](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.55...@standardnotes/event-store@1.6.56) (2022-12-20)
**Note:** Version bump only for package @standardnotes/event-store

View File

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

View File

@@ -9,31 +9,41 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false
const replicationConfig = {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST', true),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
restoreNodeTimeout: 5,
}
export const AppDataSource = new DataSource({
type: 'mysql',
charset: 'utf8mb4',
supportBigNumbers: true,
bigNumberStrings: false,
maxQueryExecutionTime,
replication: {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
},
replication: inReplicaMode ? replicationConfig : undefined,
host: inReplicaMode ? undefined : env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')),
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
entities: [Event],
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
migrationsRun: true,

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.
## [1.9.5](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.9.4...@standardnotes/files-server@1.9.5) (2023-01-13)
**Note:** Version bump only for package @standardnotes/files-server
## [1.9.4](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.9.3...@standardnotes/files-server@1.9.4) (2023-01-13)
### Bug Fixes
* add robots.txt setup for api-gateway and files server to disallow indexing ([bb82043](https://github.com/standardnotes/files/commit/bb820437af2b9644d7597de045b5840037b81db3))
## [1.9.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.9.2...@standardnotes/files-server@1.9.3) (2022-12-28)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -12,6 +12,8 @@ import helmet from 'helmet'
import * as cors from 'cors'
import { urlencoded, json, raw, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
import * as winston from 'winston'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const robots = require('express-robots-txt')
import { InversifyExpressServer } from 'inversify-express-utils'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
@@ -65,6 +67,12 @@ void container.load().then((container) => {
exposedHeaders: ['Content-Range', 'Accept-Ranges'],
}),
)
app.use(
robots({
UserAgent: '*',
Disallow: '/',
}),
)
if (env.get('SENTRY_DSN', true)) {
Sentry.init({

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.9.3",
"version": "1.9.5",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -39,12 +39,13 @@
"dayjs": "^1.11.6",
"dotenv": "^16.0.1",
"express": "^4.18.2",
"express-robots-txt": "^1.0.0",
"express-winston": "^4.0.5",
"helmet": "^6.0.0",
"inversify": "^6.0.1",
"inversify-express-utils": "^6.4.3",
"ioredis": "^5.2.4",
"jsonwebtoken": "^8.5.1",
"jsonwebtoken": "^9.0.0",
"newrelic": "^9.6.0",
"nodemon": "^2.0.19",
"prettyjson": "^1.2.5",
@@ -59,7 +60,7 @@
"@types/express": "^4.17.14",
"@types/ioredis": "^5.0.0",
"@types/jest": "^29.1.1",
"@types/jsonwebtoken": "^8.5.0",
"@types/jsonwebtoken": "^9.0.1",
"@types/newrelic": "^7.0.4",
"@types/prettyjson": "^0.0.30",
"@types/uuid": "^8.3.0",

View File

@@ -3,6 +3,52 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.11](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.10...@standardnotes/revisions-server@1.10.11) (2023-01-17)
### Bug Fixes
* allow to run typeorm in non-replica mode ([f73129c](https://github.com/standardnotes/server/commit/f73129cd7e7d6a9b8a63e5c80284467597557982))
## [1.10.10](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.9...@standardnotes/revisions-server@1.10.10) (2023-01-17)
### Bug Fixes
* **revisions:** add debug logs for retrieving revisions metadata from mysql ([c579864](https://github.com/standardnotes/server/commit/c5798640fffbbf29f30db11dcc10b8cd3f11839e))
## [1.10.9](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.8...@standardnotes/revisions-server@1.10.9) (2023-01-17)
### Bug Fixes
* **revisions:** response structure ([e2aae8a](https://github.com/standardnotes/server/commit/e2aae8ac8a98dff9f618709c33f7b80479747ec9))
## [1.10.8](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.7...@standardnotes/revisions-server@1.10.8) (2023-01-16)
### Bug Fixes
* **revisions:** add required role to revisions list response ([e7beee2](https://github.com/standardnotes/server/commit/e7beee278871d2939b058d842404fd6980d7f48a))
## [1.10.7](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.6...@standardnotes/revisions-server@1.10.7) (2023-01-16)
### Bug Fixes
* **revisions:** remove redundant specs ([11b8b07](https://github.com/standardnotes/server/commit/11b8b078b4c72f393fd4e555501242ffe22cc06f))
## [1.10.6](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.5...@standardnotes/revisions-server@1.10.6) (2023-01-16)
### Bug Fixes
* **revisions:** mapping to snake case ([b97dafe](https://github.com/standardnotes/server/commit/b97dafe6f35ad0f9e42a228f354c4bceb1a2874d))
## [1.10.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.4...@standardnotes/revisions-server@1.10.5) (2023-01-16)
### Bug Fixes
* **revisions:** response structure ([8b988d8](https://github.com/standardnotes/server/commit/8b988d89c09ee3d9df52d9922550f688bf16a9f4))
## [1.10.4](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.3...@standardnotes/revisions-server@1.10.4) (2023-01-13)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.10.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.2...@standardnotes/revisions-server@1.10.3) (2022-12-28)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

@@ -7,5 +7,5 @@ module.exports = {
transform: {
...tsjPreset.transform,
},
coveragePathIgnorePatterns: ['/Bootstrap/', 'HealthCheckController', '/Infra/', '/Mapping/'],
coveragePathIgnorePatterns: ['/Bootstrap/', '/Controller/', 'HealthCheckController', '/Infra/', '/Mapping/'],
}

View File

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

View File

@@ -42,6 +42,10 @@ import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountD
import { RevisionsCopyRequestedEventHandler } from '../Domain/Handler/RevisionsCopyRequestedEventHandler'
import { CopyRevisions } from '../Domain/UseCase/CopyRevisions/CopyRevisions'
import { RevisionsOwnershipUpdateRequestedEventHandler } from '../Domain/Handler/RevisionsOwnershipUpdateRequestedEventHandler'
import { RevisionHttpMapper } from '../Mapping/RevisionHttpMapper'
import { RevisionMetadataHttpMapper } from '../Mapping/RevisionMetadataHttpMapper'
import { GetRequiredRoleToViewRevision } from '../Domain/UseCase/GetRequiredRoleToViewRevision/GetRequiredRoleToViewRevision'
import { Timer, TimerInterface } from '@standardnotes/time'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -102,6 +106,12 @@ export class ContainerConfigLoader {
}
container.bind<AWS.S3 | undefined>(TYPES.S3).toConstantValue(s3Client)
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
container
.bind<GetRequiredRoleToViewRevision>(TYPES.GetRequiredRoleToViewRevision)
.toConstantValue(new GetRequiredRoleToViewRevision(container.get(TYPES.Timer)))
// Map
container
.bind<MapperInterface<RevisionMetadata, TypeORMRevision>>(TYPES.RevisionMetadataPersistenceMapper)
@@ -112,6 +122,37 @@ export class ContainerConfigLoader {
container
.bind<MapperInterface<Revision, string>>(TYPES.RevisionItemStringMapper)
.toConstantValue(new RevisionItemStringMapper())
container
.bind<
MapperInterface<
Revision,
{
uuid: string
item_uuid: string
content: string | null
content_type: string
items_key_id: string | null
enc_item_key: string | null
auth_hash: string | null
created_at: string
updated_at: string
}
>
>(TYPES.RevisionHttpMapper)
.toConstantValue(new RevisionHttpMapper())
container
.bind<
MapperInterface<
RevisionMetadata,
{
uuid: string
content_type: string
created_at: string
updated_at: string
}
>
>(TYPES.RevisionMetadataHttpMapper)
.toConstantValue(new RevisionMetadataHttpMapper(container.get(TYPES.GetRequiredRoleToViewRevision)))
// ORM
container
@@ -136,6 +177,7 @@ export class ContainerConfigLoader {
container.get(TYPES.ORMRevisionRepository),
container.get(TYPES.RevisionMetadataPersistenceMapper),
container.get(TYPES.RevisionPersistenceMapper),
container.get(TYPES.Logger),
),
)
if (env.get('S3_AWS_REGION', true)) {
@@ -176,6 +218,8 @@ export class ContainerConfigLoader {
container.get(TYPES.GetRevisionsMetada),
container.get(TYPES.GetRevision),
container.get(TYPES.DeleteRevision),
container.get(TYPES.RevisionHttpMapper),
container.get(TYPES.RevisionMetadataHttpMapper),
container.get(TYPES.Logger),
),
)

View File

@@ -11,34 +11,45 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
export const AppDataSource = new DataSource({
type: 'mysql',
charset: 'utf8mb4',
supportBigNumbers: true,
bigNumberStrings: false,
maxQueryExecutionTime,
replication: {
master: {
host: env.get('DB_HOST'),
const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false
const replicationConfig = {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST', true),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
restoreNodeTimeout: 5,
},
],
removeNodeErrorCount: 10,
restoreNodeTimeout: 5,
}
const dataSource = new DataSource({
type: 'mysql',
charset: 'utf8mb4',
supportBigNumbers: true,
bigNumberStrings: false,
maxQueryExecutionTime,
replication: inReplicaMode ? replicationConfig : undefined,
host: inReplicaMode ? undefined : env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')),
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
entities: [TypeORMRevision],
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
migrationsRun: true,
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),
})
export const AppDataSource = dataSource

View File

@@ -8,6 +8,8 @@ const TYPES = {
RevisionMetadataPersistenceMapper: Symbol.for('RevisionMetadataPersistenceMapper'),
RevisionPersistenceMapper: Symbol.for('RevisionPersistenceMapper'),
RevisionItemStringMapper: Symbol.for('RevisionItemStringMapper'),
RevisionHttpMapper: Symbol.for('RevisionHttpMapper'),
RevisionMetadataHttpMapper: Symbol.for('RevisionMetadataHttpMapper'),
// ORM
ORMRevisionRepository: Symbol.for('ORMRevisionRepository'),
// Repositories
@@ -28,6 +30,7 @@ const TYPES = {
GetRevision: Symbol.for('GetRevision'),
DeleteRevision: Symbol.for('DeleteRevision'),
CopyRevisions: Symbol.for('CopyRevisions'),
GetRequiredRoleToViewRevision: Symbol.for('GetRequiredRoleToViewRevision'),
// Controller
RevisionsController: Symbol.for('RevisionsController'),
// Handlers

View File

@@ -1,70 +0,0 @@
import { Result } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision'
import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision'
import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
import { RevisionsController } from './RevisionsController'
describe('RevisionsController', () => {
let getRevisionsMetadata: GetRevisionsMetada
let getRevision: GetRevision
let deleteRevision: DeleteRevision
let logger: Logger
const createController = () => new RevisionsController(getRevisionsMetadata, getRevision, deleteRevision, logger)
beforeEach(() => {
getRevisionsMetadata = {} as jest.Mocked<GetRevisionsMetada>
getRevisionsMetadata.execute = jest.fn().mockReturnValue(Result.ok())
getRevision = {} as jest.Mocked<GetRevision>
getRevision.execute = jest.fn().mockReturnValue(Result.ok())
deleteRevision = {} as jest.Mocked<DeleteRevision>
deleteRevision.execute = jest.fn().mockReturnValue(Result.ok())
logger = {} as jest.Mocked<Logger>
logger.warn = jest.fn()
})
it('should get revisions list', async () => {
const response = await createController().getRevisions({ itemUuid: '1-2-3', userUuid: '1-2-3' })
expect(response.status).toEqual(200)
})
it('should indicate failure to get revisions list', async () => {
getRevisionsMetadata.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const response = await createController().getRevisions({ itemUuid: '1-2-3', userUuid: '1-2-3' })
expect(response.status).toEqual(400)
})
it('should get revision', async () => {
const response = await createController().getRevision({ revisionUuid: '1-2-3', userUuid: '1-2-3' })
expect(response.status).toEqual(200)
})
it('should indicate failure to get revision', async () => {
getRevision.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const response = await createController().getRevision({ revisionUuid: '1-2-3', userUuid: '1-2-3' })
expect(response.status).toEqual(400)
})
it('should delete revision', async () => {
const response = await createController().deleteRevision({ revisionUuid: '1-2-3', userUuid: '1-2-3' })
expect(response.status).toEqual(200)
})
it('should indicate failure to delete revision', async () => {
deleteRevision.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const response = await createController().deleteRevision({ revisionUuid: '1-2-3', userUuid: '1-2-3' })
expect(response.status).toEqual(400)
})
})

View File

@@ -9,12 +9,38 @@ import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision'
import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision'
import { GetRevisionsMetadataResponse } from '../Infra/Http/Response/GetRevisionsMetadataResponse'
import { GetRevisionResponse } from '../Infra/Http/Response/GetRevisionResponse'
import { MapperInterface } from '@standardnotes/domain-core'
import { Revision } from '../Domain/Revision/Revision'
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
export class RevisionsController {
constructor(
private getRevisionsMetadata: GetRevisionsMetada,
private doGetRevision: GetRevision,
private doDeleteRevision: DeleteRevision,
private revisionHttpMapper: MapperInterface<
Revision,
{
uuid: string
itemUuid: string
content: string | null
contentType: string
itemsKeyId: string | null
encItemKey: string | null
authHash: string | null
createAt: string
updateAt: string
}
>,
private revisionMetadataHttpMapper: MapperInterface<
RevisionMetadata,
{
uuid: string
contentType: string
createdAt: string
updatedAt: string
}
>,
private logger: Logger,
) {}
@@ -37,9 +63,15 @@ export class RevisionsController {
}
}
const revisions = revisionMetadataOrError.getValue()
this.logger.debug(`Found ${revisions.length} revisions for item ${params.itemUuid}`)
return {
status: HttpStatusCode.Success,
data: { revisions: revisionMetadataOrError.getValue() },
data: {
revisions: revisions.map((revision) => this.revisionMetadataHttpMapper.toProjection(revision)),
},
}
}
@@ -64,7 +96,7 @@ export class RevisionsController {
return {
status: HttpStatusCode.Success,
data: { revision: revisionOrError.getValue() },
data: { revision: this.revisionHttpMapper.toProjection(revisionOrError.getValue()) },
}
}

View File

@@ -0,0 +1,43 @@
import { TimerInterface } from '@standardnotes/time'
import { GetRequiredRoleToViewRevision } from './GetRequiredRoleToViewRevision'
describe('GetRequiredRoleToViewRevision', () => {
let timer: TimerInterface
const createUseCase = () => new GetRequiredRoleToViewRevision(timer)
beforeEach(() => {
timer = {} as jest.Mocked<TimerInterface>
})
it('should return CoreUser if revision was created less than 30 days ago', () => {
timer.dateWasNDaysAgo = jest.fn().mockReturnValue(29)
const useCase = createUseCase()
const result = useCase.execute({ createdAt: new Date() })
expect(result.getValue()).toEqual('CORE_USER')
})
it('should return PlusUser if revision was created more than 30 days ago and less than 365 days ago', () => {
timer.dateWasNDaysAgo = jest.fn().mockReturnValue(31)
const useCase = createUseCase()
const result = useCase.execute({ createdAt: new Date() })
expect(result.getValue()).toEqual('PLUS_USER')
})
it('should return ProUser if revision was created more than 365 days ago', () => {
timer.dateWasNDaysAgo = jest.fn().mockReturnValue(366)
const useCase = createUseCase()
const result = useCase.execute({ createdAt: new Date() })
expect(result.getValue()).toEqual('PRO_USER')
})
})

View File

@@ -0,0 +1,22 @@
import { Result, RoleName, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { GetRequiredRoleToViewRevisionDTO } from './GetRequiredRoleToViewRevisionDTO'
export class GetRequiredRoleToViewRevision implements SyncUseCaseInterface<string> {
constructor(private timer: TimerInterface) {}
execute(dto: GetRequiredRoleToViewRevisionDTO): Result<string> {
const revisionCreatedNDaysAgo = this.timer.dateWasNDaysAgo(dto.createdAt)
if (revisionCreatedNDaysAgo > 30 && revisionCreatedNDaysAgo < 365) {
return Result.ok(RoleName.NAMES.PlusUser)
}
if (revisionCreatedNDaysAgo > 365) {
return Result.ok(RoleName.NAMES.ProUser)
}
return Result.ok(RoleName.NAMES.CoreUser)
}
}

View File

@@ -0,0 +1,3 @@
export interface GetRequiredRoleToViewRevisionDTO {
createdAt: Date
}

View File

@@ -1,5 +1,13 @@
import { Revision } from '../../../Domain/Revision/Revision'
export interface GetRevisionResponseBody {
revision: Revision
revision: {
uuid: string
itemUuid: string
content: string | null
contentType: string
itemsKeyId: string | null
encItemKey: string | null
authHash: string | null
createAt: string
updateAt: string
}
}

View File

@@ -1,5 +1,8 @@
import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
export interface GetRevisionsMetadataResponseBody {
revisions: Array<RevisionMetadata>
revisions: Array<{
uuid: string
contentType: string
createdAt: string
updatedAt: string
}>
}

View File

@@ -18,7 +18,7 @@ export class InversifyExpressRevisionsController extends BaseHttpController {
userUuid: response.locals.user.uuid,
})
return this.json(result.data.error ? result.data : result.data.revisions, result.status)
return this.json(result.data, result.status)
}
@httpGet('/:uuid')
@@ -28,7 +28,7 @@ export class InversifyExpressRevisionsController extends BaseHttpController {
userUuid: response.locals.user.uuid,
})
return this.json(result.data.error ? result.data : result.data.revision, result.status)
return this.json(result.data, result.status)
}
@httpDelete('/:uuid')

View File

@@ -1,5 +1,6 @@
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { Repository } from 'typeorm'
import { Logger } from 'winston'
import { Revision } from '../../Domain/Revision/Revision'
import { RevisionMetadata } from '../../Domain/Revision/RevisionMetadata'
@@ -11,6 +12,7 @@ export class MySQLRevisionRepository implements RevisionRepositoryInterface {
private ormRepository: Repository<TypeORMRevision>,
private revisionMetadataMapper: MapperInterface<RevisionMetadata, TypeORMRevision>,
private revisionMapper: MapperInterface<Revision, TypeORMRevision>,
private logger: Logger,
) {}
async updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void> {
@@ -94,6 +96,8 @@ export class MySQLRevisionRepository implements RevisionRepositoryInterface {
const simplifiedRevisions = await queryBuilder.getMany()
this.logger.debug(`Found ${simplifiedRevisions.length} revisions MySQL entries for item ${itemUuid.value}`)
const metadata = []
for (const simplifiedRevision of simplifiedRevisions) {
metadata.push(this.revisionMetadataMapper.toDomain(simplifiedRevision))

View File

@@ -0,0 +1,59 @@
import { MapperInterface } from '@standardnotes/domain-core'
import { Revision } from '../Domain/Revision/Revision'
export class RevisionHttpMapper
implements
MapperInterface<
Revision,
{
uuid: string
item_uuid: string
content: string | null
content_type: string
items_key_id: string | null
enc_item_key: string | null
auth_hash: string | null
created_at: string
updated_at: string
}
>
{
toDomain(_projection: {
uuid: string
item_uuid: string
content: string | null
content_type: string
items_key_id: string | null
enc_item_key: string | null
auth_hash: string | null
created_at: string
updated_at: string
}): Revision {
throw new Error('Method not implemented.')
}
toProjection(domain: Revision): {
uuid: string
item_uuid: string
content: string | null
content_type: string
items_key_id: string | null
enc_item_key: string | null
auth_hash: string | null
created_at: string
updated_at: string
} {
return {
uuid: domain.id.toString(),
item_uuid: domain.props.itemUuid.value,
content: domain.props.content,
content_type: domain.props.contentType.value as string,
items_key_id: domain.props.itemsKeyId,
enc_item_key: domain.props.encItemKey,
auth_hash: domain.props.authHash,
created_at: domain.props.dates.createdAt.toISOString(),
updated_at: domain.props.dates.updatedAt.toISOString(),
}
}
}

View File

@@ -0,0 +1,45 @@
import { MapperInterface, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
export class RevisionMetadataHttpMapper
implements
MapperInterface<
RevisionMetadata,
{
uuid: string
content_type: string
created_at: string
updated_at: string
required_role: string
}
>
{
constructor(private getRequiredRoleToViewRevision: SyncUseCaseInterface<string>) {}
toDomain(_projection: {
uuid: string
content_type: string
created_at: string
updated_at: string
required_role: string
}): RevisionMetadata {
throw new Error('Method not implemented.')
}
toProjection(domain: RevisionMetadata): {
uuid: string
content_type: string
created_at: string
updated_at: string
required_role: string
} {
return {
uuid: domain.id.toString(),
content_type: domain.props.contentType.value as string,
created_at: domain.props.dates.createdAt.toISOString(),
updated_at: domain.props.dates.updatedAt.toISOString(),
required_role: this.getRequiredRoleToViewRevision.execute({ createdAt: domain.props.dates.createdAt }).getValue(),
}
}
}

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.16.8](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.16.7...@standardnotes/scheduler-server@1.16.8) (2023-01-17)
### Bug Fixes
* allow to run typeorm in non-replica mode ([f73129c](https://github.com/standardnotes/server/commit/f73129cd7e7d6a9b8a63e5c80284467597557982))
## [1.16.7](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.16.6...@standardnotes/scheduler-server@1.16.7) (2023-01-16)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.16.6](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.16.5...@standardnotes/scheduler-server@1.16.6) (2023-01-13)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.16.5](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.16.4...@standardnotes/scheduler-server@1.16.5) (2023-01-06)
### Bug Fixes
* **scheduler:** change email levels ([79c3e33](https://github.com/standardnotes/server/commit/79c3e33434d186df7240d17cb598914361b1c9fe))
## [1.16.4](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.16.3...@standardnotes/scheduler-server@1.16.4) (2022-12-28)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.16.4",
"version": "1.16.8",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -10,31 +10,41 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false
const replicationConfig = {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST', true),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
restoreNodeTimeout: 5,
}
export const AppDataSource = new DataSource({
type: 'mysql',
charset: 'utf8mb4',
supportBigNumbers: true,
bigNumberStrings: false,
maxQueryExecutionTime,
replication: {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
},
replication: inReplicaMode ? replicationConfig : undefined,
host: inReplicaMode ? undefined : env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')),
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
entities: [Job, Predicate],
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
migrationsRun: true,

View File

@@ -97,7 +97,7 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
subject: getEncourageEmailBackupsSubject(),
body: getEncourageEmailBackupsBody(),
level: EmailLevel.LEVELS.System,
level: EmailLevel.LEVELS.Marketing,
}),
)
}
@@ -113,7 +113,7 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
body: getEncourageSubscriptionPurchasingBody(
this.timer.convertMicrosecondsToDate(job.createdAt).toLocaleString(),
),
level: EmailLevel.LEVELS.System,
level: EmailLevel.LEVELS.Marketing,
}),
)
}
@@ -127,7 +127,7 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
messageIdentifier: 'EXIT_INTERVIEW',
subject: getExitInterviewSubject(),
body: getExitInterviewBody(),
level: EmailLevel.LEVELS.System,
level: EmailLevel.LEVELS.Marketing,
}),
)
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.7.3](https://github.com/standardnotes/server/compare/@standardnotes/security@1.7.2...@standardnotes/security@1.7.3) (2023-01-13)
**Note:** Version bump only for package @standardnotes/security
## [1.7.2](https://github.com/standardnotes/server/compare/@standardnotes/security@1.7.1...@standardnotes/security@1.7.2) (2022-11-25)
**Note:** Version bump only for package @standardnotes/security

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/security",
"version": "1.7.2",
"version": "1.7.3",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -26,12 +26,12 @@
},
"dependencies": {
"@standardnotes/common": "workspace:*",
"jsonwebtoken": "^8.5.1",
"jsonwebtoken": "^9.0.0",
"reflect-metadata": "^0.1.13"
},
"devDependencies": {
"@types/jest": "^29.1.1",
"@types/jsonwebtoken": "^8.5.8",
"@types/jsonwebtoken": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.30.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.1.2",

View File

@@ -3,6 +3,27 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.9](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.28.8...@standardnotes/syncing-server@1.28.9) (2023-01-17)
### Bug Fixes
* allow to run typeorm in non-replica mode ([f73129c](https://github.com/standardnotes/syncing-server-js/commit/f73129cd7e7d6a9b8a63e5c80284467597557982))
## [1.28.8](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.28.7...@standardnotes/syncing-server@1.28.8) (2023-01-17)
### Bug Fixes
* **syncing-server-js:** add debug logs for dumping items for revisions creation ([8db19c3](https://github.com/standardnotes/syncing-server-js/commit/8db19c3e2b55af9230b92621f01ae0d7c514913a))
* **syncing-server-js:** creating directory for revision dumps ([9b926fb](https://github.com/standardnotes/syncing-server-js/commit/9b926fbad6c40a2e3792cc0d7c54987febd6dced))
## [1.28.7](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.28.6...@standardnotes/syncing-server@1.28.7) (2023-01-16)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.28.6](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.28.5...@standardnotes/syncing-server@1.28.6) (2023-01-13)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.28.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.28.4...@standardnotes/syncing-server@1.28.5) (2023-01-04)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.28.5",
"version": "1.28.9",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -47,7 +47,7 @@
"inversify": "^6.0.1",
"inversify-express-utils": "^6.4.3",
"ioredis": "^5.2.4",
"jsonwebtoken": "8.5.1",
"jsonwebtoken": "^9.0.0",
"mysql2": "^2.3.3",
"newrelic": "^9.6.0",
"nodemon": "^2.0.19",
@@ -65,7 +65,7 @@
"@types/inversify-express-utils": "^2.0.0",
"@types/ioredis": "^5.0.0",
"@types/jest": "^29.1.1",
"@types/jsonwebtoken": "^8.5.0",
"@types/jsonwebtoken": "^9.0.1",
"@types/newrelic": "^7.0.4",
"@types/prettyjson": "^0.0.30",
"@types/ua-parser-js": "^0.7.36",

View File

@@ -10,32 +10,41 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false
const replicationConfig = {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST', true),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
restoreNodeTimeout: 5,
}
export const AppDataSource = new DataSource({
type: 'mysql',
charset: 'utf8mb4',
supportBigNumbers: true,
bigNumberStrings: false,
maxQueryExecutionTime,
replication: {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
restoreNodeTimeout: 5,
},
replication: inReplicaMode ? replicationConfig : undefined,
host: inReplicaMode ? undefined : env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')),
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
entities: [Item, Revision],
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
migrationsRun: true,

View File

@@ -2,6 +2,8 @@ import { KeyParamsData } from '@standardnotes/responses'
import { promises } from 'fs'
import * as uuid from 'uuid'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { dirname } from 'path'
import TYPES from '../../Bootstrap/Types'
import { Item } from '../../Domain/Item/Item'
@@ -14,6 +16,7 @@ export class FSItemBackupService implements ItemBackupServiceInterface {
constructor(
@inject(TYPES.FILE_UPLOAD_PATH) private fileUploadPath: string,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
@inject(TYPES.Logger) private logger: Logger,
) {}
async backup(_items: Item[], _authParams: KeyParamsData): Promise<string> {
@@ -27,6 +30,10 @@ export class FSItemBackupService implements ItemBackupServiceInterface {
const path = `${this.fileUploadPath}/dumps/${uuid.v4()}`
this.logger.debug(`Dumping item ${item.uuid} to ${path}`)
await promises.mkdir(dirname(path), { recursive: true })
await promises.writeFile(path, contents)
return path

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.5.4](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.5.3...@standardnotes/websockets-server@1.5.4) (2023-01-13)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.5.3](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.5.2...@standardnotes/websockets-server@1.5.3) (2022-12-28)
**Note:** Version bump only for package @standardnotes/websockets-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.5.3",
"version": "1.5.4",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.19.7](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.19.6...@standardnotes/workspace-server@1.19.7) (2023-01-17)
### Bug Fixes
* allow to run typeorm in non-replica mode ([f73129c](https://github.com/standardnotes/server/commit/f73129cd7e7d6a9b8a63e5c80284467597557982))
## [1.19.6](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.19.5...@standardnotes/workspace-server@1.19.6) (2023-01-16)
**Note:** Version bump only for package @standardnotes/workspace-server
## [1.19.5](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.19.4...@standardnotes/workspace-server@1.19.5) (2023-01-13)
**Note:** Version bump only for package @standardnotes/workspace-server
## [1.19.4](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.19.3...@standardnotes/workspace-server@1.19.4) (2022-12-28)
**Note:** Version bump only for package @standardnotes/workspace-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/workspace-server",
"version": "1.19.4",
"version": "1.19.7",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -11,31 +11,41 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false
const replicationConfig = {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST', true),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
restoreNodeTimeout: 5,
}
export const AppDataSource = new DataSource({
type: 'mysql',
charset: 'utf8mb4',
supportBigNumbers: true,
bigNumberStrings: false,
maxQueryExecutionTime,
replication: {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
},
replication: inReplicaMode ? replicationConfig : undefined,
host: inReplicaMode ? undefined : env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')),
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
entities: [Workspace, WorkspaceUser, WorkspaceInvite],
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
migrationsRun: true,

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