mirror of
https://github.com/standardnotes/server
synced 2026-02-03 11:01:13 -05:00
Compare commits
13 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ace63cfcc1 | ||
|
|
d28c268e86 | ||
|
|
6f43726a3b | ||
|
|
4f6a2a83d3 | ||
|
|
937ce5a157 | ||
|
|
0c1a779ef0 | ||
|
|
e01d1f44d0 | ||
|
|
cea9021c16 | ||
|
|
3039f58b5a | ||
|
|
e2326190d4 | ||
|
|
095d13f8bb | ||
|
|
1292d1d898 | ||
|
|
8bc92616d2 |
182
.github/workflows/websockets.release.yml
vendored
Normal file
182
.github/workflows/websockets.release.yml
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
name: Websockets Server
|
||||
|
||||
concurrency:
|
||||
group: websockets
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*standardnotes/websockets-server*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
|
||||
- name: Lint
|
||||
run: yarn lint:websockets
|
||||
|
||||
- name: Test
|
||||
run: yarn test:websockets
|
||||
|
||||
publish-aws-ecr:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Build locally
|
||||
run: yarn build
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
- name: Build, tag, and push image to Amazon ECR
|
||||
id: build-image
|
||||
env:
|
||||
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
||||
ECR_REPOSITORY: websockets
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
yarn docker build @standardnotes/websockets-server -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
||||
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
|
||||
|
||||
publish-docker-hub:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Build locally
|
||||
run: yarn build
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Publish Docker image as stable
|
||||
run: |
|
||||
yarn docker build @standardnotes/websockets-server -t standardnotes/websockets:latest
|
||||
docker push standardnotes/websockets:latest
|
||||
|
||||
deploy-web:
|
||||
needs: publish-aws-ecr
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
- name: Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition websockets-prod --query taskDefinition > task-definition.json
|
||||
- name: Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="websockets-prod") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def-prod
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: websockets-prod
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/websockets:${{ github.sha }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def-prod.outputs.task-definition }}
|
||||
service: websockets-prod
|
||||
cluster: prod
|
||||
wait-for-service-stability: true
|
||||
|
||||
deploy-worker:
|
||||
needs: publish-aws-ecr
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
- name: Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition websockets-worker-prod --query taskDefinition > task-definition.json
|
||||
- name: Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="websockets-worker-prod") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def-prod
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: websockets-worker-prod
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/websockets:${{ github.sha }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def-prod.outputs.task-definition }}
|
||||
service: websockets-worker-prod
|
||||
cluster: prod
|
||||
wait-for-service-stability: true
|
||||
|
||||
newrelic:
|
||||
needs: [ deploy-web, deploy-worker ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create New Relic deployment marker for Web
|
||||
uses: newrelic/deployment-marker-action@v1
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_WEBSOCKETS_WEB_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
- name: Create New Relic deployment marker for Worker
|
||||
uses: newrelic/deployment-marker-action@v1
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_WEBSOCKETS_WORKER_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
157
.pnp.cjs
generated
157
.pnp.cjs
generated
@@ -80,6 +80,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"name": "@standardnotes/time",\
|
||||
"reference": "workspace:packages/time"\
|
||||
},\
|
||||
{\
|
||||
"name": "@standardnotes/websockets-server",\
|
||||
"reference": "workspace:packages/websockets"\
|
||||
},\
|
||||
{\
|
||||
"name": "@standardnotes/workspace-server",\
|
||||
"reference": "workspace:packages/workspace"\
|
||||
@@ -104,6 +108,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["@standardnotes/sncrypto-node", ["workspace:packages/sncrypto-node"]],\
|
||||
["@standardnotes/syncing-server", ["workspace:packages/syncing-server"]],\
|
||||
["@standardnotes/time", ["workspace:packages/time"]],\
|
||||
["@standardnotes/websockets-server", ["workspace:packages/websockets"]],\
|
||||
["@standardnotes/workspace-server", ["workspace:packages/workspace"]]\
|
||||
],\
|
||||
"fallbackPool": [\
|
||||
@@ -2521,16 +2526,30 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/api", [\
|
||||
["npm:1.12.1", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.12.1-6b8bfe4ccf-8623fc82de.zip/node_modules/@standardnotes/api/",\
|
||||
["npm:1.16.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.16.0-efccf518ba-465f76dd29.zip/node_modules/@standardnotes/api/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/api", "npm:1.12.1"],\
|
||||
["@standardnotes/api", "npm:1.16.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/encryption", "npm:1.16.2"],\
|
||||
["@standardnotes/models", "npm:1.24.2"],\
|
||||
["@standardnotes/responses", "npm:1.10.6"],\
|
||||
["@standardnotes/encryption", "npm:1.17.1"],\
|
||||
["@standardnotes/models", "npm:1.27.0"],\
|
||||
["@standardnotes/responses", "npm:1.11.0"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/utils", "npm:1.9.1"],\
|
||||
["@standardnotes/utils", "npm:1.10.0"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.16.1", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.16.1-7af309f020-1f939c1825.zip/node_modules/@standardnotes/api/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/api", "npm:1.16.1"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/encryption", "npm:1.18.0"],\
|
||||
["@standardnotes/models", "npm:1.27.0"],\
|
||||
["@standardnotes/responses", "npm:1.11.0"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/utils", "npm:1.10.0"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
@@ -2600,7 +2619,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.5.0"],\
|
||||
["@standardnotes/analytics", "workspace:packages/analytics"],\
|
||||
["@standardnotes/api", "npm:1.12.1"],\
|
||||
["@standardnotes/api", "npm:1.16.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
@@ -2725,15 +2744,28 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/encryption", [\
|
||||
["npm:1.16.2", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.16.2-9e53125abe-50efc1b201.zip/node_modules/@standardnotes/encryption/",\
|
||||
["npm:1.17.1", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.17.1-f4d1330273-2b2408ffbd.zip/node_modules/@standardnotes/encryption/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/encryption", "npm:1.16.2"],\
|
||||
["@standardnotes/encryption", "npm:1.17.1"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/models", "npm:1.24.2"],\
|
||||
["@standardnotes/responses", "npm:1.10.6"],\
|
||||
["@standardnotes/models", "npm:1.27.0"],\
|
||||
["@standardnotes/responses", "npm:1.11.0"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.13.0"],\
|
||||
["@standardnotes/utils", "npm:1.9.1"],\
|
||||
["@standardnotes/utils", "npm:1.10.0"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.18.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.18.0-d83f58c21a-f9c39d0986.zip/node_modules/@standardnotes/encryption/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/encryption", "npm:1.18.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/models", "npm:1.27.0"],\
|
||||
["@standardnotes/responses", "npm:1.11.0"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.13.0"],\
|
||||
["@standardnotes/utils", "npm:1.10.0"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
@@ -2791,10 +2823,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.52.4", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.52.4-05c59084e4-aea7b48627.zip/node_modules/@standardnotes/features/",\
|
||||
["npm:1.53.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.53.0-8ea4a2d559-a856e815a3.zip/node_modules/@standardnotes/features/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/features", "npm:1.52.4"],\
|
||||
["@standardnotes/features", "npm:1.53.0"],\
|
||||
["@standardnotes/auth", "npm:3.19.4"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
@@ -2856,14 +2888,27 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/models", [\
|
||||
["npm:1.24.2", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.24.2-8c2c157efa-17b3cfba39.zip/node_modules/@standardnotes/models/",\
|
||||
["npm:1.26.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.26.0-dade8919ab-f595a3de88.zip/node_modules/@standardnotes/models/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/models", "npm:1.24.2"],\
|
||||
["@standardnotes/models", "npm:1.26.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/features", "npm:1.52.4"],\
|
||||
["@standardnotes/responses", "npm:1.10.6"],\
|
||||
["@standardnotes/utils", "npm:1.9.1"],\
|
||||
["@standardnotes/features", "npm:1.53.0"],\
|
||||
["@standardnotes/responses", "npm:1.11.0"],\
|
||||
["@standardnotes/utils", "npm:1.10.0"],\
|
||||
["lodash", "npm:4.17.21"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.27.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.27.0-831bd645c6-263fd9e923.zip/node_modules/@standardnotes/models/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/models", "npm:1.27.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/features", "npm:1.53.0"],\
|
||||
["@standardnotes/responses", "npm:1.11.0"],\
|
||||
["@standardnotes/utils", "npm:1.10.0"],\
|
||||
["lodash", "npm:4.17.21"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
@@ -2899,12 +2944,12 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/responses", [\
|
||||
["npm:1.10.6", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.10.6-f636794f47-0583e2cb77.zip/node_modules/@standardnotes/responses/",\
|
||||
["npm:1.11.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.11.0-d066ddbbb6-46d6a47980.zip/node_modules/@standardnotes/responses/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/responses", "npm:1.10.6"],\
|
||||
["@standardnotes/responses", "npm:1.11.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/features", "npm:1.52.4"],\
|
||||
["@standardnotes/features", "npm:1.53.0"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
@@ -3125,6 +3170,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/utils", [\
|
||||
["npm:1.10.0", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.10.0-0dc2ade40b-c02d54ca8a.zip/node_modules/@standardnotes/utils/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/utils", "npm:1.10.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["dompurify", "npm:2.4.0"],\
|
||||
["lodash", "npm:4.17.21"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.6.12", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.6.12-8fa8d7d09b-e177b1fa51.zip/node_modules/@standardnotes/utils/",\
|
||||
"packageDependencies": [\
|
||||
@@ -3134,17 +3190,45 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["lodash", "npm:4.17.21"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.9.1", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.9.1-e48d87ffc7-f775bb3744.zip/node_modules/@standardnotes/utils/",\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/websockets-server", [\
|
||||
["workspace:packages/websockets", {\
|
||||
"packageLocation": "./packages/websockets/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/utils", "npm:1.9.1"],\
|
||||
["@standardnotes/websockets-server", "workspace:packages/websockets"],\
|
||||
["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.5.0"],\
|
||||
["@standardnotes/api", "npm:1.16.1"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["dompurify", "npm:2.4.0"],\
|
||||
["lodash", "npm:4.17.21"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@types/cors", "npm:2.8.12"],\
|
||||
["@types/express", "npm:4.17.13"],\
|
||||
["@types/ioredis", "npm:4.28.10"],\
|
||||
["@types/jest", "npm:29.1.1"],\
|
||||
["@types/newrelic", "npm:7.0.3"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.30.5"],\
|
||||
["aws-sdk", "npm:2.1168.0"],\
|
||||
["axios", "npm:0.27.2"],\
|
||||
["cors", "npm:2.8.5"],\
|
||||
["dotenv", "npm:16.0.1"],\
|
||||
["eslint", "npm:8.19.0"],\
|
||||
["eslint-plugin-prettier", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.2.1"],\
|
||||
["express", "npm:4.18.1"],\
|
||||
["inversify", "npm:6.0.1"],\
|
||||
["inversify-express-utils", "npm:6.4.3"],\
|
||||
["ioredis", "npm:5.2.0"],\
|
||||
["jest", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:29.1.2"],\
|
||||
["mysql2", "npm:2.3.3"],\
|
||||
["newrelic", "npm:9.0.0"],\
|
||||
["reflect-metadata", "npm:0.1.13"],\
|
||||
["ts-jest", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:29.0.3"],\
|
||||
["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.7"],\
|
||||
["winston", "npm:3.8.1"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
"linkType": "SOFT"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/workspace-server", [\
|
||||
@@ -3154,10 +3238,11 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["@standardnotes/workspace-server", "workspace:packages/workspace"],\
|
||||
["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.5.0"],\
|
||||
["@standardnotes/api", "npm:1.12.1"],\
|
||||
["@standardnotes/api", "npm:1.16.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/models", "npm:1.26.0"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/time", "workspace:packages/time"],\
|
||||
["@types/cors", "npm:2.8.12"],\
|
||||
|
||||
Binary file not shown.
BIN
.yarn/cache/@standardnotes-api-npm-1.16.1-7af309f020-1f939c1825.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-api-npm-1.16.1-7af309f020-1f939c1825.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@standardnotes-encryption-npm-1.18.0-d83f58c21a-f9c39d0986.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-encryption-npm-1.18.0-d83f58c21a-f9c39d0986.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@standardnotes-models-npm-1.27.0-831bd645c6-263fd9e923.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-models-npm-1.27.0-831bd645c6-263fd9e923.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
16
package.json
16
package.json
@@ -18,6 +18,7 @@
|
||||
"lint:files": "yarn workspace @standardnotes/files-server lint",
|
||||
"lint:api-gateway": "yarn workspace @standardnotes/api-gateway lint",
|
||||
"lint:event-store": "yarn workspace @standardnotes/event-store lint",
|
||||
"lint:websockets": "yarn workspace @standardnotes/websockets-server lint",
|
||||
"lint:workspace": "yarn workspace @standardnotes/workspace-server lint",
|
||||
"test": "yarn workspaces foreach -p -j 10 --verbose run test",
|
||||
"test:auth": "yarn workspace @standardnotes/auth-server test",
|
||||
@@ -25,16 +26,18 @@
|
||||
"test:syncing-server": "yarn workspace @standardnotes/syncing-server test",
|
||||
"test:files": "yarn workspace @standardnotes/files-server test",
|
||||
"test:event-store": "yarn workspace @standardnotes/event-store test",
|
||||
"test:websockets": "yarn workspace @standardnotes/websockets-server test",
|
||||
"test:workspace": "yarn workspace @standardnotes/workspace-server test",
|
||||
"clean": "yarn workspaces foreach -p --verbose run clean",
|
||||
"setup:env": "cp .env.sample .env && yarn workspaces foreach -p --verbose run setup:env",
|
||||
"build": "yarn workspaces foreach -pt -j 10 --verbose run build",
|
||||
"build:auth": "yarn workspace @standardnotes/auth-server build",
|
||||
"build:scheduler": "yarn workspace @standardnotes/scheduler-server build",
|
||||
"build:syncing-server": "yarn workspace @standardnotes/syncing-server build",
|
||||
"build:files": "yarn workspace @standardnotes/files-server build",
|
||||
"build:api-gateway": "yarn workspace @standardnotes/api-gateway build",
|
||||
"build:workspace": "yarn workspace @standardnotes/workspace-server build",
|
||||
"build:auth": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/auth-server run build",
|
||||
"build:scheduler": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/scheduler-server run build",
|
||||
"build:syncing-server": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/syncing-server run build",
|
||||
"build:files": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/files-server run build",
|
||||
"build:api-gateway": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/api-gateway run build",
|
||||
"build:websockets": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/websockets-server run build",
|
||||
"build:workspace": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/workspace-server run build",
|
||||
"start:auth": "yarn workspace @standardnotes/auth-server start",
|
||||
"start:auth-worker": "yarn workspace @standardnotes/auth-server worker",
|
||||
"start:scheduler": "yarn workspace @standardnotes/scheduler-server worker",
|
||||
@@ -43,6 +46,7 @@
|
||||
"start:files": "yarn workspace @standardnotes/files-server start",
|
||||
"start:files-worker": "yarn workspace @standardnotes/files-server worker",
|
||||
"start:api-gateway": "yarn workspace @standardnotes/api-gateway start",
|
||||
"start:websockets": "yarn workspace @standardnotes/websockets-server start",
|
||||
"start:workspace": "yarn workspace @standardnotes/workspace-server start",
|
||||
"release": "lerna version --conventional-graduate --conventional-commits --yes -m \"chore(release): publish new version\"",
|
||||
"publish": "lerna publish from-git --yes --no-verify-access --loglevel verbose",
|
||||
|
||||
@@ -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.30.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.30.0...@standardnotes/api-gateway@1.30.1) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.30.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.29.0...@standardnotes/api-gateway@1.30.0) (2022-10-12)
|
||||
|
||||
### Features
|
||||
|
||||
* **workspace:** add endpoints for initiating keyshare in a workspace ([0c1a779](https://github.com/standardnotes/api-gateway/commit/0c1a779ef03819928e7e791a6843d90eb9fed964))
|
||||
|
||||
# [1.29.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.28.2...@standardnotes/api-gateway@1.29.0) (2022-10-11)
|
||||
|
||||
### Features
|
||||
|
||||
* add listin worspaces and workspace users ([095d13f](https://github.com/standardnotes/api-gateway/commit/095d13f8bbfe543fcf086840e1a985447a6c51ef))
|
||||
|
||||
## [1.28.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.28.1...@standardnotes/api-gateway@1.28.2) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.28.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.28.0...@standardnotes/api-gateway@1.28.1) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -20,6 +20,7 @@ import '../src/Controller/v1/OfflineController'
|
||||
import '../src/Controller/v1/FilesController'
|
||||
import '../src/Controller/v1/SubscriptionInvitesController'
|
||||
import '../src/Controller/v1/WorkspacesController'
|
||||
import '../src/Controller/v1/InvitesController'
|
||||
|
||||
import '../src/Controller/v2/PaymentsControllerV2'
|
||||
import '../src/Controller/v2/ActionsControllerV2'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.28.1",
|
||||
"version": "1.30.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
23
packages/api-gateway/src/Controller/v1/InvitesController.ts
Normal file
23
packages/api-gateway/src/Controller/v1/InvitesController.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { inject } from 'inversify'
|
||||
import { Request, Response } from 'express'
|
||||
import { controller, BaseHttpController, httpPost } from 'inversify-express-utils'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/invites', TYPES.AuthMiddleware)
|
||||
export class InvitesController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/:inviteUuid/accept')
|
||||
async accept(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callWorkspaceServer(
|
||||
request,
|
||||
response,
|
||||
`invites/${request.params.inviteUuid}/accept`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { inject } from 'inversify'
|
||||
import { Request, Response } from 'express'
|
||||
import { controller, BaseHttpController, httpPost } from 'inversify-express-utils'
|
||||
import { controller, BaseHttpController, httpPost, httpGet } from 'inversify-express-utils'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
@@ -16,6 +16,31 @@ export class WorkspacesController extends BaseHttpController {
|
||||
await this.httpService.callWorkspaceServer(request, response, 'workspaces', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/:workspaceUuid/users')
|
||||
async listWorkspaceUsers(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callWorkspaceServer(
|
||||
request,
|
||||
response,
|
||||
`workspaces/${request.params.workspaceUuid}/users`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPost('/:workspaceUuid/users/:userUuid/keyshare')
|
||||
async initiateKeyshare(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callWorkspaceServer(
|
||||
request,
|
||||
response,
|
||||
`workspaces/${request.params.workspaceUuid}/users/${request.params.userUuid}/keyshare`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/')
|
||||
async listWorkspaces(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callWorkspaceServer(request, response, 'workspaces', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/:workspaceUuid/invites')
|
||||
async invite(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callWorkspaceServer(
|
||||
|
||||
@@ -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.43.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.43.0...@standardnotes/auth-server@1.43.1) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.43.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.42.0...@standardnotes/auth-server@1.43.0) (2022-10-12)
|
||||
|
||||
### Features
|
||||
|
||||
* **workspace:** add endpoints for initiating keyshare in a workspace ([0c1a779](https://github.com/standardnotes/server/commit/0c1a779ef03819928e7e791a6843d90eb9fed964))
|
||||
|
||||
# [1.42.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.41.2...@standardnotes/auth-server@1.42.0) (2022-10-11)
|
||||
|
||||
### Features
|
||||
|
||||
* add listin worspaces and workspace users ([095d13f](https://github.com/standardnotes/server/commit/095d13f8bbfe543fcf086840e1a985447a6c51ef))
|
||||
|
||||
## [1.41.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.41.1...@standardnotes/auth-server@1.41.2) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.41.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.41.0...@standardnotes/auth-server@1.41.1) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.41.1",
|
||||
"version": "1.43.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -34,7 +34,7 @@
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.3.0",
|
||||
"@standardnotes/analytics": "workspace:*",
|
||||
"@standardnotes/api": "^1.12.1",
|
||||
"@standardnotes/api": "^1.16.0",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.39.0...@standardnotes/common@1.40.0) (2022-10-13)
|
||||
|
||||
### Features
|
||||
|
||||
* **websockets:** add websockets service ([d28c268](https://github.com/standardnotes/server/commit/d28c268e86644f34f5550eeded5bb4a7407d4a99))
|
||||
|
||||
# [1.39.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.38.0...@standardnotes/common@1.39.0) (2022-10-11)
|
||||
|
||||
### Features
|
||||
|
||||
* **workspace:** extract workspace user status to common ([8bc9261](https://github.com/standardnotes/server/commit/8bc92616d2fbeb833c3fcbef6b87538745fc7f3e))
|
||||
|
||||
# [1.38.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.37.0...@standardnotes/common@1.38.0) (2022-10-11)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/common",
|
||||
"version": "1.38.0",
|
||||
"version": "1.40.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
1
packages/common/src/Domain/DataType/JSONString.ts
Normal file
1
packages/common/src/Domain/DataType/JSONString.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type JSONString = string
|
||||
@@ -2,6 +2,7 @@ export * from './Content/ContentType'
|
||||
export * from './Content/ContentDecoder'
|
||||
export * from './Content/ContentDecoderInterface'
|
||||
export * from './DataType/AnyRecord'
|
||||
export * from './DataType/JSONString'
|
||||
export * from './DataType/MicrosecondsTimestamp'
|
||||
export * from './DataType/Uuid'
|
||||
export * from './DataType/ApplicationIdentifier'
|
||||
@@ -26,3 +27,4 @@ export * from './Validator/UuidValidator'
|
||||
export * from './Validator/ValidatorInterface'
|
||||
export * from './Workspace/WorkspaceAccessLevel'
|
||||
export * from './Workspace/WorkspaceType'
|
||||
export * from './Workspace/WorkspaceUserStatus'
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.8.26](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.25...@standardnotes/domain-events-infra@1.8.26) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.8.25](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.24...@standardnotes/domain-events-infra@1.8.25) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.8.24](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.23...@standardnotes/domain-events-infra@1.8.24) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.8.24",
|
||||
"version": "1.8.26",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.67.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.66.3...@standardnotes/domain-events@2.67.0) (2022-10-13)
|
||||
|
||||
### Features
|
||||
|
||||
* **websockets:** add websockets service ([d28c268](https://github.com/standardnotes/server/commit/d28c268e86644f34f5550eeded5bb4a7407d4a99))
|
||||
|
||||
## [2.66.3](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.66.2...@standardnotes/domain-events@2.66.3) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
## [2.66.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.66.1...@standardnotes/domain-events@2.66.2) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.66.2",
|
||||
"version": "2.67.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { WebSocketMessageRequestedEventPayload } from './WebSocketMessageRequestedEventPayload'
|
||||
|
||||
export interface WebSocketMessageRequestedEvent extends DomainEventInterface {
|
||||
type: 'WEB_SOCKET_MESSAGE_REQUESTED'
|
||||
payload: WebSocketMessageRequestedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { JSONString, Uuid } from '@standardnotes/common'
|
||||
|
||||
export interface WebSocketMessageRequestedEventPayload {
|
||||
userUuid: Uuid
|
||||
message: JSONString
|
||||
}
|
||||
@@ -100,6 +100,8 @@ export * from './Event/UserRolesChangedEvent'
|
||||
export * from './Event/UserRolesChangedEventPayload'
|
||||
export * from './Event/UserSignedInEvent'
|
||||
export * from './Event/UserSignedInEventPayload'
|
||||
export * from './Event/WebSocketMessageRequestedEvent'
|
||||
export * from './Event/WebSocketMessageRequestedEventPayload'
|
||||
export * from './Event/WorkspaceInviteCreatedEvent'
|
||||
export * from './Event/WorkspaceInviteCreatedEventPayload'
|
||||
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.5](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.4.4...@standardnotes/event-store@1.4.5) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.4.4](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.4.3...@standardnotes/event-store@1.4.4) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.4.3](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.4.2...@standardnotes/event-store@1.4.3) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.5",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.6.17](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.16...@standardnotes/files-server@1.6.17) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.6.16](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.15...@standardnotes/files-server@1.6.16) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.6.15](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.14...@standardnotes/files-server@1.6.15) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.6.15",
|
||||
"version": "1.6.17",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.11](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.4.10...@standardnotes/predicates@1.4.11) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/predicates
|
||||
|
||||
## [1.4.10](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.4.9...@standardnotes/predicates@1.4.10) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/predicates
|
||||
|
||||
## [1.4.9](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.4.8...@standardnotes/predicates@1.4.9) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/predicates
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/predicates",
|
||||
"version": "1.4.9",
|
||||
"version": "1.4.11",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.10.45](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.44...@standardnotes/scheduler-server@1.10.45) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.10.44](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.43...@standardnotes/scheduler-server@1.10.44) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.10.43](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.42...@standardnotes/scheduler-server@1.10.43) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.10.43",
|
||||
"version": "1.10.45",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.9](https://github.com/standardnotes/server/compare/@standardnotes/security@1.4.8...@standardnotes/security@1.4.9) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/security
|
||||
|
||||
## [1.4.8](https://github.com/standardnotes/server/compare/@standardnotes/security@1.4.7...@standardnotes/security@1.4.8) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/security
|
||||
|
||||
## [1.4.7](https://github.com/standardnotes/server/compare/@standardnotes/security@1.4.6...@standardnotes/security@1.4.7) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/security
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/security",
|
||||
"version": "1.4.7",
|
||||
"version": "1.4.9",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.9.7](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.9.6...@standardnotes/syncing-server@1.9.7) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.9.6](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.9.5...@standardnotes/syncing-server@1.9.6) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.9.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.9.4...@standardnotes/syncing-server@1.9.5) (2022-10-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.9.5",
|
||||
"version": "1.9.7",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
28
packages/websockets/.env.sample
Normal file
28
packages/websockets/.env.sample
Normal file
@@ -0,0 +1,28 @@
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
|
||||
PORT=3000
|
||||
|
||||
AUTH_JWT_SECRET=auth_jwt_secret
|
||||
|
||||
REDIS_URL=redis://cache
|
||||
|
||||
SNS_TOPIC_ARN=
|
||||
SNS_AWS_REGION=
|
||||
SQS_QUEUE_URL=
|
||||
SQS_AWS_REGION=
|
||||
|
||||
REDIS_EVENTS_CHANNEL=events
|
||||
|
||||
WEB_SOCKET_CONNECTION_TOKEN_SECRET=
|
||||
WEB_SOCKET_CONNECTION_TOKEN_TTL=
|
||||
|
||||
# (Optional) New Relic Setup
|
||||
NEW_RELIC_ENABLED=false
|
||||
NEW_RELIC_APP_NAME=Websockets
|
||||
NEW_RELIC_LICENSE_KEY=
|
||||
NEW_RELIC_NO_CONFIG_FILE=true
|
||||
NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
|
||||
NEW_RELIC_LOG_ENABLED=false
|
||||
NEW_RELIC_LOG_LEVEL=info
|
||||
3
packages/websockets/.eslintignore
Normal file
3
packages/websockets/.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
dist
|
||||
test-setup.ts
|
||||
data
|
||||
6
packages/websockets/.eslintrc
Normal file
6
packages/websockets/.eslintrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../.eslintrc",
|
||||
"parserOptions": {
|
||||
"project": "./linter.tsconfig.json"
|
||||
}
|
||||
}
|
||||
10
packages/websockets/CHANGELOG.md
Normal file
10
packages/websockets/CHANGELOG.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# 1.1.0 (2022-10-13)
|
||||
|
||||
### Features
|
||||
|
||||
* **websockets:** add websockets service ([d28c268](https://github.com/standardnotes/server/commit/d28c268e86644f34f5550eeded5bb4a7407d4a99))
|
||||
27
packages/websockets/Dockerfile
Normal file
27
packages/websockets/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM node:16.15.1-alpine AS builder
|
||||
|
||||
# Install dependencies for building native libraries
|
||||
RUN apk add --update git openssh-client python3 alpine-sdk
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
# docker-build plugin copies everything needed for `yarn install` to `manifests` folder.
|
||||
COPY manifests ./
|
||||
|
||||
RUN yarn install --immutable
|
||||
|
||||
FROM node:16.15.1-alpine
|
||||
|
||||
RUN apk add --update curl
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
# Copy the installed dependencies from the previous stage.
|
||||
COPY --from=builder /workspace ./
|
||||
|
||||
# docker-build plugin runs `yarn pack` in all workspace dependencies and copies them to `packs` folder.
|
||||
COPY packs ./
|
||||
|
||||
ENTRYPOINT [ "/workspace/packages/websockets/docker/entrypoint.sh" ]
|
||||
|
||||
CMD [ "start-web" ]
|
||||
70
packages/websockets/bin/server.ts
Normal file
70
packages/websockets/bin/server.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import 'newrelic'
|
||||
|
||||
import * as Sentry from '@sentry/node'
|
||||
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressHealthCheckController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController'
|
||||
|
||||
import * as cors from 'cors'
|
||||
import { urlencoded, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
|
||||
import * as winston from 'winston'
|
||||
|
||||
import { InversifyExpressServer } from 'inversify-express-utils'
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const server = new InversifyExpressServer(container)
|
||||
|
||||
server.setConfig((app) => {
|
||||
app.use((_request: Request, response: Response, next: NextFunction) => {
|
||||
response.setHeader('X-Websockets-Version', container.get(TYPES.VERSION))
|
||||
next()
|
||||
})
|
||||
app.use(json())
|
||||
app.use(urlencoded({ extended: true }))
|
||||
app.use(cors())
|
||||
|
||||
if (env.get('SENTRY_DSN', true)) {
|
||||
Sentry.init({
|
||||
dsn: env.get('SENTRY_DSN'),
|
||||
integrations: [new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true })],
|
||||
tracesSampleRate: 0,
|
||||
})
|
||||
|
||||
app.use(Sentry.Handlers.requestHandler() as RequestHandler)
|
||||
}
|
||||
})
|
||||
|
||||
const logger: winston.Logger = container.get(TYPES.Logger)
|
||||
|
||||
server.setErrorConfig((app) => {
|
||||
if (env.get('SENTRY_DSN', true)) {
|
||||
app.use(Sentry.Handlers.errorHandler() as ErrorRequestHandler)
|
||||
}
|
||||
|
||||
app.use((error: Record<string, unknown>, _request: Request, response: Response, _next: NextFunction) => {
|
||||
logger.error(error.stack)
|
||||
|
||||
response.status(500).send({
|
||||
error: {
|
||||
message:
|
||||
"Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const serverInstance = server.build()
|
||||
|
||||
serverInstance.listen(env.get('PORT'))
|
||||
|
||||
logger.info(`Server started on port ${process.env.PORT}`)
|
||||
})
|
||||
25
packages/websockets/bin/worker.ts
Normal file
25
packages/websockets/bin/worker.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import 'newrelic'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
import { DomainEventSubscriberFactoryInterface } from '@standardnotes/domain-events'
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const logger: Logger = container.get(TYPES.Logger)
|
||||
|
||||
logger.info('Starting worker...')
|
||||
|
||||
const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory)
|
||||
subscriberFactory.create().start()
|
||||
|
||||
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
|
||||
})
|
||||
22
packages/websockets/docker/entrypoint.sh
Executable file
22
packages/websockets/docker/entrypoint.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
COMMAND=$1 && shift 1
|
||||
|
||||
case "$COMMAND" in
|
||||
'start-web' )
|
||||
echo "Starting Web..."
|
||||
yarn workspace @standardnotes/websockets-server start
|
||||
;;
|
||||
|
||||
'start-worker' )
|
||||
echo "Starting Worker..."
|
||||
yarn workspace @standardnotes/websockets-server worker
|
||||
;;
|
||||
|
||||
* )
|
||||
echo "Unknown command"
|
||||
;;
|
||||
esac
|
||||
|
||||
exec "$@"
|
||||
12
packages/websockets/jest.config.js
Normal file
12
packages/websockets/jest.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const base = require('../../jest.config')
|
||||
const { defaults: tsjPreset } = require('ts-jest/presets')
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
transform: {
|
||||
...tsjPreset.transform,
|
||||
},
|
||||
coveragePathIgnorePatterns: ['/Bootstrap/', '/InversifyExpressUtils/'],
|
||||
setupFilesAfterEnv: ['./test-setup.ts'],
|
||||
}
|
||||
4
packages/websockets/linter.tsconfig.json
Normal file
4
packages/websockets/linter.tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist", "test-setup.ts"]
|
||||
}
|
||||
59
packages/websockets/package.json
Normal file
59
packages/websockets/package.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.1.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
"private": true,
|
||||
"description": "Websockets Server",
|
||||
"main": "dist/src/index.js",
|
||||
"typings": "dist/src/index.d.ts",
|
||||
"author": "Karol Sójko <karol@standardnotes.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"clean": "rm -fr dist",
|
||||
"setup:env": "cp .env.sample .env",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "tsc --rootDir ./",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"pretest": "yarn lint && yarn build",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"start": "yarn node dist/bin/server.js",
|
||||
"worker": "yarn node dist/bin/worker.js",
|
||||
"typeorm": "typeorm-ts-node-commonjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.3.0",
|
||||
"@standardnotes/api": "^1.16.1",
|
||||
"@standardnotes/common": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:^",
|
||||
"@standardnotes/domain-events-infra": "workspace:^",
|
||||
"@standardnotes/security": "workspace:^",
|
||||
"aws-sdk": "^2.1159.0",
|
||||
"axios": "^0.27.2",
|
||||
"cors": "2.8.5",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"inversify": "^6.0.1",
|
||||
"inversify-express-utils": "^6.4.3",
|
||||
"ioredis": "^5.2.0",
|
||||
"mysql2": "^2.3.3",
|
||||
"newrelic": "^9.0.0",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"typeorm": "^0.3.6",
|
||||
"winston": "^3.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.9",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/jest": "^29.1.1",
|
||||
"@types/newrelic": "^7.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^29.1.2",
|
||||
"ts-jest": "^29.0.3"
|
||||
}
|
||||
}
|
||||
186
packages/websockets/src/Bootstrap/Container.ts
Normal file
186
packages/websockets/src/Bootstrap/Container.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import * as winston from 'winston'
|
||||
import Redis from 'ioredis'
|
||||
import * as AWS from 'aws-sdk'
|
||||
import { Container } from 'inversify'
|
||||
import {
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventMessageHandlerInterface,
|
||||
DomainEventSubscriberFactoryInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Env } from './Env'
|
||||
import TYPES from './Types'
|
||||
import { WebSocketsConnectionRepositoryInterface } from '../Domain/WebSockets/WebSocketsConnectionRepositoryInterface'
|
||||
import { RedisWebSocketsConnectionRepository } from '../Infra/Redis/RedisWebSocketsConnectionRepository'
|
||||
import { AddWebSocketsConnection } from '../Domain/UseCase/AddWebSocketsConnection/AddWebSocketsConnection'
|
||||
import { RemoveWebSocketsConnection } from '../Domain/UseCase/RemoveWebSocketsConnection/RemoveWebSocketsConnection'
|
||||
import { WebSocketsClientMessenger } from '../Infra/WebSockets/WebSocketsClientMessenger'
|
||||
import {
|
||||
RedisDomainEventSubscriberFactory,
|
||||
RedisEventMessageHandler,
|
||||
SQSDomainEventSubscriberFactory,
|
||||
SQSEventMessageHandler,
|
||||
SQSNewRelicEventMessageHandler,
|
||||
} from '@standardnotes/domain-events-infra'
|
||||
import { ApiGatewayAuthMiddleware } from '../Controller/ApiGatewayAuthMiddleware'
|
||||
|
||||
import {
|
||||
CrossServiceTokenData,
|
||||
TokenDecoder,
|
||||
TokenDecoderInterface,
|
||||
TokenEncoder,
|
||||
TokenEncoderInterface,
|
||||
WebSocketConnectionTokenData,
|
||||
} from '@standardnotes/security'
|
||||
import { CreateWebSocketConnectionToken } from '../Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken'
|
||||
import { WebSocketsController } from '../Controller/WebSocketsController'
|
||||
import { WebSocketServerInterface } from '@standardnotes/api'
|
||||
import { ClientMessengerInterface } from '../Client/ClientMessengerInterface'
|
||||
import { WebSocketMessageRequestedEventHandler } from '../Domain/Handler/WebSocketMessageRequestedEventHandler'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(): Promise<Container> {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const container = new Container()
|
||||
|
||||
const redisUrl = env.get('REDIS_URL')
|
||||
const isRedisInClusterMode = redisUrl.indexOf(',') > 0
|
||||
let redis
|
||||
if (isRedisInClusterMode) {
|
||||
redis = new Redis.Cluster(redisUrl.split(','))
|
||||
} else {
|
||||
redis = new Redis(redisUrl)
|
||||
}
|
||||
|
||||
container.bind(TYPES.Redis).toConstantValue(redis)
|
||||
|
||||
const newrelicWinstonFormatter = newrelicFormatter(winston)
|
||||
const winstonFormatters = [winston.format.splat(), winston.format.json()]
|
||||
if (env.get('NEW_RELIC_ENABLED', true) === 'true') {
|
||||
winstonFormatters.push(newrelicWinstonFormatter())
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: env.get('LOG_LEVEL') || 'info',
|
||||
format: winston.format.combine(...winstonFormatters),
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
|
||||
})
|
||||
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
|
||||
|
||||
if (env.get('SNS_AWS_REGION', true)) {
|
||||
container.bind<AWS.SNS>(TYPES.SNS).toConstantValue(
|
||||
new AWS.SNS({
|
||||
apiVersion: 'latest',
|
||||
region: env.get('SNS_AWS_REGION', true),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
const sqsConfig: AWS.SQS.Types.ClientConfiguration = {
|
||||
apiVersion: 'latest',
|
||||
region: env.get('SQS_AWS_REGION', true),
|
||||
}
|
||||
if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
|
||||
sqsConfig.credentials = {
|
||||
accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
|
||||
secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
|
||||
}
|
||||
}
|
||||
container.bind<AWS.SQS>(TYPES.SQS).toConstantValue(new AWS.SQS(sqsConfig))
|
||||
}
|
||||
|
||||
// Controller
|
||||
container.bind<WebSocketServerInterface>(TYPES.WebSocketsController).to(WebSocketsController)
|
||||
|
||||
// Repositories
|
||||
container
|
||||
.bind<WebSocketsConnectionRepositoryInterface>(TYPES.WebSocketsConnectionRepository)
|
||||
.to(RedisWebSocketsConnectionRepository)
|
||||
|
||||
// Middleware
|
||||
container.bind<ApiGatewayAuthMiddleware>(TYPES.ApiGatewayAuthMiddleware).to(ApiGatewayAuthMiddleware)
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
|
||||
container
|
||||
.bind(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)
|
||||
.toConstantValue(env.get('WEB_SOCKET_CONNECTION_TOKEN_SECRET', true))
|
||||
container
|
||||
.bind(TYPES.WEB_SOCKET_CONNECTION_TOKEN_TTL)
|
||||
.toConstantValue(+env.get('WEB_SOCKET_CONNECTION_TOKEN_TTL', true))
|
||||
container.bind(TYPES.REDIS_URL).toConstantValue(env.get('REDIS_URL'))
|
||||
container.bind(TYPES.SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN', true))
|
||||
container.bind(TYPES.SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
|
||||
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
|
||||
container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
|
||||
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
|
||||
container.bind(TYPES.WEBSOCKETS_API_URL).toConstantValue(env.get('WEBSOCKETS_API_URL', true))
|
||||
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
|
||||
|
||||
// use cases
|
||||
container.bind<AddWebSocketsConnection>(TYPES.AddWebSocketsConnection).to(AddWebSocketsConnection)
|
||||
container.bind<RemoveWebSocketsConnection>(TYPES.RemoveWebSocketsConnection).to(RemoveWebSocketsConnection)
|
||||
container
|
||||
.bind<CreateWebSocketConnectionToken>(TYPES.CreateWebSocketConnectionToken)
|
||||
.to(CreateWebSocketConnectionToken)
|
||||
|
||||
// Handlers
|
||||
container
|
||||
.bind<WebSocketMessageRequestedEventHandler>(TYPES.WebSocketMessageRequestedEventHandler)
|
||||
.to(WebSocketMessageRequestedEventHandler)
|
||||
|
||||
// Services
|
||||
container
|
||||
.bind<TokenDecoderInterface<CrossServiceTokenData>>(TYPES.CrossServiceTokenDecoder)
|
||||
.toConstantValue(new TokenDecoder<CrossServiceTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
|
||||
container
|
||||
.bind<TokenEncoderInterface<WebSocketConnectionTokenData>>(TYPES.WebSocketConnectionTokenEncoder)
|
||||
.toConstantValue(
|
||||
new TokenEncoder<WebSocketConnectionTokenData>(container.get(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)),
|
||||
)
|
||||
container.bind<ClientMessengerInterface>(TYPES.WebSocketsClientMessenger).to(WebSocketsClientMessenger)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['WEB_SOCKET_MESSAGE_REQUESTED', container.get(TYPES.WebSocketMessageRequestedEventHandler)],
|
||||
])
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
container
|
||||
.bind<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler)
|
||||
.toConstantValue(
|
||||
env.get('NEW_RELIC_ENABLED', true) === 'true'
|
||||
? new SQSNewRelicEventMessageHandler(eventHandlers, container.get(TYPES.Logger))
|
||||
: new SQSEventMessageHandler(eventHandlers, container.get(TYPES.Logger)),
|
||||
)
|
||||
container
|
||||
.bind<DomainEventSubscriberFactoryInterface>(TYPES.DomainEventSubscriberFactory)
|
||||
.toConstantValue(
|
||||
new SQSDomainEventSubscriberFactory(
|
||||
container.get(TYPES.SQS),
|
||||
container.get(TYPES.SQS_QUEUE_URL),
|
||||
container.get(TYPES.DomainEventMessageHandler),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
container
|
||||
.bind<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler)
|
||||
.toConstantValue(new RedisEventMessageHandler(eventHandlers, container.get(TYPES.Logger)))
|
||||
container
|
||||
.bind<DomainEventSubscriberFactoryInterface>(TYPES.DomainEventSubscriberFactory)
|
||||
.toConstantValue(
|
||||
new RedisDomainEventSubscriberFactory(
|
||||
container.get(TYPES.Redis),
|
||||
container.get(TYPES.DomainEventMessageHandler),
|
||||
container.get(TYPES.REDIS_EVENTS_CHANNEL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
}
|
||||
24
packages/websockets/src/Bootstrap/Env.ts
Normal file
24
packages/websockets/src/Bootstrap/Env.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { config, DotenvParseOutput } from 'dotenv'
|
||||
import { injectable } from 'inversify'
|
||||
|
||||
@injectable()
|
||||
export class Env {
|
||||
private env?: DotenvParseOutput
|
||||
|
||||
public load(): void {
|
||||
const output = config()
|
||||
this.env = <DotenvParseOutput>output.parsed
|
||||
}
|
||||
|
||||
public get(key: string, optional = false): string {
|
||||
if (!this.env) {
|
||||
this.load()
|
||||
}
|
||||
|
||||
if (!process.env[key] && !optional) {
|
||||
throw new Error(`Environment variable ${key} not set`)
|
||||
}
|
||||
|
||||
return <string>process.env[key]
|
||||
}
|
||||
}
|
||||
40
packages/websockets/src/Bootstrap/Types.ts
Normal file
40
packages/websockets/src/Bootstrap/Types.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
const TYPES = {
|
||||
Logger: Symbol.for('Logger'),
|
||||
Redis: Symbol.for('Redis'),
|
||||
SNS: Symbol.for('SNS'),
|
||||
SQS: Symbol.for('SQS'),
|
||||
// Controller
|
||||
WebSocketsController: Symbol.for('WebSocketsController'),
|
||||
// Repositories
|
||||
WebSocketsConnectionRepository: Symbol.for('WebSocketsConnectionRepository'),
|
||||
// Middleware
|
||||
ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'),
|
||||
// env vars
|
||||
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
|
||||
WEB_SOCKET_CONNECTION_TOKEN_SECRET: Symbol.for('WEB_SOCKET_CONNECTION_TOKEN_SECRET'),
|
||||
WEB_SOCKET_CONNECTION_TOKEN_TTL: Symbol.for('WEB_SOCKET_CONNECTION_TOKEN_TTL'),
|
||||
REDIS_URL: Symbol.for('REDIS_URL'),
|
||||
SNS_TOPIC_ARN: Symbol.for('SNS_TOPIC_ARN'),
|
||||
SNS_AWS_REGION: Symbol.for('SNS_AWS_REGION'),
|
||||
SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'),
|
||||
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
|
||||
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
||||
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
|
||||
WEBSOCKETS_API_URL: Symbol.for('WEBSOCKETS_API_URL'),
|
||||
VERSION: Symbol.for('VERSION'),
|
||||
// use cases
|
||||
AddWebSocketsConnection: Symbol.for('AddWebSocketsConnection'),
|
||||
RemoveWebSocketsConnection: Symbol.for('RemoveWebSocketsConnection'),
|
||||
CreateWebSocketConnectionToken: Symbol.for('CreateWebSocketConnectionToken'),
|
||||
// Handlers
|
||||
WebSocketMessageRequestedEventHandler: Symbol.for('WebSocketMessageRequestedEventHandler'),
|
||||
// Services
|
||||
CrossServiceTokenDecoder: Symbol.for('CrossServiceTokenDecoder'),
|
||||
WebSocketConnectionTokenEncoder: Symbol.for('WebSocketConnectionTokenEncoder'),
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
DomainEventMessageHandler: Symbol.for('DomainEventMessageHandler'),
|
||||
HTTPClient: Symbol.for('HTTPClient'),
|
||||
WebSocketsClientMessenger: Symbol.for('WebSocketsClientMessenger'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
@@ -0,0 +1,5 @@
|
||||
import { JSONString, Uuid } from '@standardnotes/common'
|
||||
|
||||
export interface ClientMessengerInterface {
|
||||
send(userUuid: Uuid, message: JSONString): Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { ApiGatewayAuthMiddleware } from './ApiGatewayAuthMiddleware'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { Logger } from 'winston'
|
||||
import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/security'
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
|
||||
describe('ApiGatewayAuthMiddleware', () => {
|
||||
let tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>
|
||||
let request: Request
|
||||
let response: Response
|
||||
let next: NextFunction
|
||||
|
||||
const logger = {
|
||||
debug: jest.fn(),
|
||||
} as unknown as jest.Mocked<Logger>
|
||||
|
||||
const createMiddleware = () => new ApiGatewayAuthMiddleware(tokenDecoder, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
tokenDecoder = {} as jest.Mocked<TokenDecoderInterface<CrossServiceTokenData>>
|
||||
tokenDecoder.decodeToken = jest.fn().mockReturnValue({
|
||||
user: {
|
||||
uuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
uuid: 'a-b-c',
|
||||
name: RoleName.CoreUser,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
request = {
|
||||
headers: {},
|
||||
} as jest.Mocked<Request>
|
||||
response = {
|
||||
locals: {},
|
||||
} as jest.Mocked<Response>
|
||||
response.status = jest.fn().mockReturnThis()
|
||||
response.send = jest.fn()
|
||||
next = jest.fn()
|
||||
})
|
||||
|
||||
it('should authorize user', async () => {
|
||||
request.headers['x-auth-token'] = 'auth-jwt-token'
|
||||
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.locals.user).toEqual({
|
||||
uuid: '1-2-3',
|
||||
email: 'test@test.te',
|
||||
})
|
||||
expect(response.locals.roles).toEqual([
|
||||
{
|
||||
uuid: 'a-b-c',
|
||||
name: RoleName.CoreUser,
|
||||
},
|
||||
])
|
||||
|
||||
expect(next).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not authorize if request is missing auth jwt token in headers', async () => {
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.status).toHaveBeenCalledWith(401)
|
||||
expect(next).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not authorize if auth jwt token is malformed', async () => {
|
||||
request.headers['x-auth-token'] = 'auth-jwt-token'
|
||||
|
||||
tokenDecoder.decodeToken = jest.fn().mockReturnValue(undefined)
|
||||
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.status).toHaveBeenCalledWith(401)
|
||||
expect(next).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should pass the error to next middleware if one occurres', async () => {
|
||||
request.headers['x-auth-token'] = 'auth-jwt-token'
|
||||
|
||||
const error = new Error('Ooops')
|
||||
|
||||
tokenDecoder.decodeToken = jest.fn().mockImplementation(() => {
|
||||
throw error
|
||||
})
|
||||
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.status).not.toHaveBeenCalled()
|
||||
|
||||
expect(next).toHaveBeenCalledWith(error)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/security'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { BaseMiddleware } from 'inversify-express-utils'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
|
||||
@injectable()
|
||||
export class ApiGatewayAuthMiddleware extends BaseMiddleware {
|
||||
constructor(
|
||||
@inject(TYPES.CrossServiceTokenDecoder) private tokenDecoder: TokenDecoderInterface<CrossServiceTokenData>,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
if (!request.headers['x-auth-token']) {
|
||||
this.logger.debug('ApiGatewayAuthMiddleware missing x-auth-token header.')
|
||||
|
||||
response.status(401).send({
|
||||
error: {
|
||||
tag: 'invalid-auth',
|
||||
message: 'Invalid login credentials.',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const token: CrossServiceTokenData | undefined = this.tokenDecoder.decodeToken(
|
||||
request.headers['x-auth-token'] as string,
|
||||
)
|
||||
|
||||
if (token === undefined) {
|
||||
this.logger.debug('ApiGatewayAuthMiddleware authentication failure.')
|
||||
|
||||
response.status(401).send({
|
||||
error: {
|
||||
tag: 'invalid-auth',
|
||||
message: 'Invalid login credentials.',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response.locals.user = token.user
|
||||
response.locals.roles = token.roles
|
||||
response.locals.session = token.session
|
||||
response.locals.readOnlyAccess = token.session?.readonly_access ?? false
|
||||
|
||||
return next()
|
||||
} catch (error) {
|
||||
return next(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { WebSocketsController } from './WebSocketsController'
|
||||
import { CreateWebSocketConnectionToken } from '../Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken'
|
||||
|
||||
describe('WebSocketsController', () => {
|
||||
let createWebSocketConnectionToken: CreateWebSocketConnectionToken
|
||||
|
||||
const createController = () => new WebSocketsController(createWebSocketConnectionToken)
|
||||
|
||||
beforeEach(() => {
|
||||
createWebSocketConnectionToken = {} as jest.Mocked<CreateWebSocketConnectionToken>
|
||||
createWebSocketConnectionToken.execute = jest.fn().mockReturnValue({ token: 'foobar' })
|
||||
})
|
||||
|
||||
it('should create a web sockets connection token', async () => {
|
||||
const response = await createController().createConnectionToken({ userUuid: '1-2-3' })
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: { token: 'foobar' },
|
||||
})
|
||||
|
||||
expect(createWebSocketConnectionToken.execute).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
})
|
||||
29
packages/websockets/src/Controller/WebSocketsController.ts
Normal file
29
packages/websockets/src/Controller/WebSocketsController.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
HttpStatusCode,
|
||||
WebSocketConnectionTokenRequestParams,
|
||||
WebSocketConnectionTokenResponse,
|
||||
WebSocketServerInterface,
|
||||
} from '@standardnotes/api'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { CreateWebSocketConnectionToken } from '../Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken'
|
||||
|
||||
@injectable()
|
||||
export class WebSocketsController implements WebSocketServerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.CreateWebSocketConnectionToken)
|
||||
private createWebSocketConnectionToken: CreateWebSocketConnectionToken,
|
||||
) {}
|
||||
|
||||
async createConnectionToken(
|
||||
params: WebSocketConnectionTokenRequestParams,
|
||||
): Promise<WebSocketConnectionTokenResponse> {
|
||||
const result = await this.createWebSocketConnectionToken.execute({ userUuid: params.userUuid as string })
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DomainEventHandlerInterface, WebSocketMessageRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { ClientMessengerInterface } from '../../Client/ClientMessengerInterface'
|
||||
|
||||
@injectable()
|
||||
export class WebSocketMessageRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(@inject(TYPES.WebSocketsClientMessenger) private webSocketsClientMessenger: ClientMessengerInterface) {}
|
||||
|
||||
async handle(event: WebSocketMessageRequestedEvent): Promise<void> {
|
||||
await this.webSocketsClientMessenger.send(event.payload.userUuid, event.payload.message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'reflect-metadata'
|
||||
import { Logger } from 'winston'
|
||||
import { WebSocketsConnectionRepositoryInterface } from '../../WebSockets/WebSocketsConnectionRepositoryInterface'
|
||||
|
||||
import { AddWebSocketsConnection } from './AddWebSocketsConnection'
|
||||
|
||||
describe('AddWebSocketsConnection', () => {
|
||||
let webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new AddWebSocketsConnection(webSocketsConnectionRepository, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
webSocketsConnectionRepository = {} as jest.Mocked<WebSocketsConnectionRepositoryInterface>
|
||||
webSocketsConnectionRepository.saveConnection = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
})
|
||||
|
||||
it('should save a web sockets connection for a user for further communication', async () => {
|
||||
await createUseCase().execute({ userUuid: '1-2-3', connectionId: '2-3-4' })
|
||||
|
||||
expect(webSocketsConnectionRepository.saveConnection).toHaveBeenCalledWith('1-2-3', '2-3-4')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { WebSocketsConnectionRepositoryInterface } from '../../WebSockets/WebSocketsConnectionRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { AddWebSocketsConnectionDTO } from './AddWebSocketsConnectionDTO'
|
||||
import { AddWebSocketsConnectionResponse } from './AddWebSocketsConnectionResponse'
|
||||
|
||||
@injectable()
|
||||
export class AddWebSocketsConnection implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.WebSocketsConnectionRepository)
|
||||
private webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: AddWebSocketsConnectionDTO): Promise<AddWebSocketsConnectionResponse> {
|
||||
this.logger.debug(`Persisting connection ${dto.connectionId} for user ${dto.userUuid}`)
|
||||
|
||||
await this.webSocketsConnectionRepository.saveConnection(dto.userUuid, dto.connectionId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type AddWebSocketsConnectionDTO = {
|
||||
userUuid: string
|
||||
connectionId: string
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type AddWebSocketsConnectionResponse = {
|
||||
success: boolean
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TokenEncoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
|
||||
|
||||
import { CreateWebSocketConnectionToken } from './CreateWebSocketConnectionToken'
|
||||
|
||||
describe('CreateWebSocketConnection', () => {
|
||||
let tokenEncoder: TokenEncoderInterface<WebSocketConnectionTokenData>
|
||||
const tokenTTL = 30
|
||||
|
||||
const createUseCase = () => new CreateWebSocketConnectionToken(tokenEncoder, tokenTTL)
|
||||
|
||||
beforeEach(() => {
|
||||
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<WebSocketConnectionTokenData>>
|
||||
tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar')
|
||||
})
|
||||
|
||||
it('should create a web socket connection token', async () => {
|
||||
const result = await createUseCase().execute({ userUuid: '1-2-3' })
|
||||
|
||||
expect(result.token).toEqual('foobar')
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith({ userUuid: '1-2-3' }, 30)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export type CreateWebSocketConnectionDTO = {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type CreateWebSocketConnectionResponse = {
|
||||
token: string
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { TokenEncoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { CreateWebSocketConnectionDTO } from './CreateWebSocketConnectionDTO'
|
||||
import { CreateWebSocketConnectionResponse } from './CreateWebSocketConnectionResponse'
|
||||
|
||||
@injectable()
|
||||
export class CreateWebSocketConnectionToken implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.WebSocketConnectionTokenEncoder)
|
||||
private tokenEncoder: TokenEncoderInterface<WebSocketConnectionTokenData>,
|
||||
@inject(TYPES.WEB_SOCKET_CONNECTION_TOKEN_TTL) private tokenTTL: number,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateWebSocketConnectionDTO): Promise<CreateWebSocketConnectionResponse> {
|
||||
const data: WebSocketConnectionTokenData = {
|
||||
userUuid: dto.userUuid,
|
||||
}
|
||||
|
||||
return {
|
||||
token: this.tokenEncoder.encodeExpirableToken(data, this.tokenTTL),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'reflect-metadata'
|
||||
import { Logger } from 'winston'
|
||||
import { WebSocketsConnectionRepositoryInterface } from '../../WebSockets/WebSocketsConnectionRepositoryInterface'
|
||||
|
||||
import { RemoveWebSocketsConnection } from './RemoveWebSocketsConnection'
|
||||
|
||||
describe('RemoveWebSocketsConnection', () => {
|
||||
let webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new RemoveWebSocketsConnection(webSocketsConnectionRepository, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
webSocketsConnectionRepository = {} as jest.Mocked<WebSocketsConnectionRepositoryInterface>
|
||||
webSocketsConnectionRepository.removeConnection = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
})
|
||||
|
||||
it('should remove a web sockets connection', async () => {
|
||||
await createUseCase().execute({ connectionId: '2-3-4' })
|
||||
|
||||
expect(webSocketsConnectionRepository.removeConnection).toHaveBeenCalledWith('2-3-4')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { WebSocketsConnectionRepositoryInterface } from '../../WebSockets/WebSocketsConnectionRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { RemoveWebSocketsConnectionDTO } from './RemoveWebSocketsConnectionDTO'
|
||||
import { RemoveWebSocketsConnectionResponse } from './RemoveWebSocketsConnectionResponse'
|
||||
|
||||
@injectable()
|
||||
export class RemoveWebSocketsConnection implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.WebSocketsConnectionRepository)
|
||||
private webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: RemoveWebSocketsConnectionDTO): Promise<RemoveWebSocketsConnectionResponse> {
|
||||
this.logger.debug(`Removing connection ${dto.connectionId}`)
|
||||
|
||||
await this.webSocketsConnectionRepository.removeConnection(dto.connectionId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type RemoveWebSocketsConnectionDTO = {
|
||||
connectionId: string
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type RemoveWebSocketsConnectionResponse = {
|
||||
success: boolean
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface UseCaseInterface {
|
||||
execute(...args: any[]): Promise<Record<string, unknown>>
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface WebSocketsConnectionRepositoryInterface {
|
||||
findAllByUserUuid(userUuid: string): Promise<string[]>
|
||||
saveConnection(userUuid: string, connectionId: string): Promise<void>
|
||||
removeConnection(connectionId: string): Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { controller, httpGet } from 'inversify-express-utils'
|
||||
|
||||
@controller('/healthcheck')
|
||||
export class InversifyExpressHealthCheckController {
|
||||
@httpGet('/')
|
||||
public async get(): Promise<string> {
|
||||
return 'OK'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { WebSocketServerInterface } from '@standardnotes/api'
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
BaseHttpController,
|
||||
controller,
|
||||
httpDelete,
|
||||
httpPost,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
results,
|
||||
} from 'inversify-express-utils'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AddWebSocketsConnection } from '../../Domain/UseCase/AddWebSocketsConnection/AddWebSocketsConnection'
|
||||
import { RemoveWebSocketsConnection } from '../../Domain/UseCase/RemoveWebSocketsConnection/RemoveWebSocketsConnection'
|
||||
|
||||
@controller('/sockets')
|
||||
export class InversifyExpressWebSocketsController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.AddWebSocketsConnection) private addWebSocketsConnection: AddWebSocketsConnection,
|
||||
@inject(TYPES.RemoveWebSocketsConnection) private removeWebSocketsConnection: RemoveWebSocketsConnection,
|
||||
@inject(TYPES.WebSocketsController) private webSocketsController: WebSocketServerInterface,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/tokens', TYPES.ApiGatewayAuthMiddleware)
|
||||
async createConnectionToken(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.webSocketsController.createConnectionToken({
|
||||
userUuid: response.locals.user.uuid,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
@httpPost('/connections/:connectionId', TYPES.ApiGatewayAuthMiddleware)
|
||||
async storeWebSocketsConnection(
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
|
||||
await this.addWebSocketsConnection.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
connectionId: request.params.connectionId,
|
||||
})
|
||||
|
||||
return this.json({ success: true })
|
||||
}
|
||||
|
||||
@httpDelete('/connections/:connectionId')
|
||||
async deleteWebSocketsConnection(
|
||||
request: Request,
|
||||
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
|
||||
await this.removeWebSocketsConnection.execute({ connectionId: request.params.connectionId })
|
||||
|
||||
return this.json({ success: true })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import * as IORedis from 'ioredis'
|
||||
|
||||
import { RedisWebSocketsConnectionRepository } from './RedisWebSocketsConnectionRepository'
|
||||
|
||||
describe('RedisWebSocketsConnectionRepository', () => {
|
||||
let redisClient: IORedis.Redis
|
||||
|
||||
const createRepository = () => new RedisWebSocketsConnectionRepository(redisClient)
|
||||
|
||||
beforeEach(() => {
|
||||
redisClient = {} as jest.Mocked<IORedis.Redis>
|
||||
redisClient.sadd = jest.fn()
|
||||
redisClient.set = jest.fn()
|
||||
redisClient.get = jest.fn()
|
||||
redisClient.srem = jest.fn()
|
||||
redisClient.del = jest.fn()
|
||||
redisClient.smembers = jest.fn()
|
||||
})
|
||||
|
||||
it('should save a connection to set of user connections', async () => {
|
||||
await createRepository().saveConnection('1-2-3', '2-3-4')
|
||||
|
||||
expect(redisClient.sadd).toHaveBeenCalledWith('ws_user_connections:1-2-3', '2-3-4')
|
||||
expect(redisClient.set).toHaveBeenCalledWith('ws_connection:2-3-4', '1-2-3')
|
||||
})
|
||||
|
||||
it('should remove a connection from the set of user connections', async () => {
|
||||
redisClient.get = jest.fn().mockReturnValue('1-2-3')
|
||||
|
||||
await createRepository().removeConnection('2-3-4')
|
||||
|
||||
expect(redisClient.srem).toHaveBeenCalledWith('ws_user_connections:1-2-3', '2-3-4')
|
||||
expect(redisClient.del).toHaveBeenCalledWith('ws_connection:2-3-4')
|
||||
})
|
||||
|
||||
it('should return all connections for a user uuid', async () => {
|
||||
const userUuid = '1-2-3'
|
||||
|
||||
await createRepository().findAllByUserUuid(userUuid)
|
||||
expect(redisClient.smembers).toHaveBeenCalledWith(`ws_user_connections:${userUuid}`)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as IORedis from 'ioredis'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { WebSocketsConnectionRepositoryInterface } from '../../Domain/WebSockets/WebSocketsConnectionRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class RedisWebSocketsConnectionRepository implements WebSocketsConnectionRepositoryInterface {
|
||||
private readonly WEB_SOCKETS_USER_CONNECTIONS_PREFIX = 'ws_user_connections'
|
||||
private readonly WEB_SOCKETS_CONNETION_PREFIX = 'ws_connection'
|
||||
|
||||
constructor(@inject(TYPES.Redis) private redisClient: IORedis.Redis) {}
|
||||
|
||||
async findAllByUserUuid(userUuid: string): Promise<string[]> {
|
||||
return await this.redisClient.smembers(`${this.WEB_SOCKETS_USER_CONNECTIONS_PREFIX}:${userUuid}`)
|
||||
}
|
||||
|
||||
async removeConnection(connectionId: string): Promise<void> {
|
||||
const userUuid = await this.redisClient.get(`${this.WEB_SOCKETS_CONNETION_PREFIX}:${connectionId}`)
|
||||
|
||||
await this.redisClient.srem(`${this.WEB_SOCKETS_USER_CONNECTIONS_PREFIX}:${userUuid}`, connectionId)
|
||||
await this.redisClient.del(`${this.WEB_SOCKETS_CONNETION_PREFIX}:${connectionId}`)
|
||||
}
|
||||
|
||||
async saveConnection(userUuid: string, connectionId: string): Promise<void> {
|
||||
await this.redisClient.set(`${this.WEB_SOCKETS_CONNETION_PREFIX}:${connectionId}`, userUuid)
|
||||
await this.redisClient.sadd(`${this.WEB_SOCKETS_USER_CONNECTIONS_PREFIX}:${userUuid}`, connectionId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { AxiosInstance } from 'axios'
|
||||
import { JSONString, Uuid } from '@standardnotes/common'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { WebSocketsConnectionRepositoryInterface } from '../../Domain/WebSockets/WebSocketsConnectionRepositoryInterface'
|
||||
import { ClientMessengerInterface } from '../../Client/ClientMessengerInterface'
|
||||
|
||||
@injectable()
|
||||
export class WebSocketsClientMessenger implements ClientMessengerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.WebSocketsConnectionRepository)
|
||||
private webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface,
|
||||
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
|
||||
@inject(TYPES.WEBSOCKETS_API_URL) private webSocketsApiUrl: string,
|
||||
) {}
|
||||
|
||||
async send(userUuid: Uuid, message: JSONString): Promise<void> {
|
||||
const userConnections = await this.webSocketsConnectionRepository.findAllByUserUuid(userUuid)
|
||||
|
||||
for (const connectionUuid of userConnections) {
|
||||
await this.httpClient.request({
|
||||
method: 'POST',
|
||||
url: `${this.webSocketsApiUrl}/${connectionUuid}`,
|
||||
headers: {
|
||||
Accept: 'text/plain',
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
data: message,
|
||||
validateStatus:
|
||||
/* istanbul ignore next */
|
||||
(status: number) => status >= 200 && status < 500,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { WebSocketsConnectionRepositoryInterface } from '../../Domain/WebSockets/WebSocketsConnectionRepositoryInterface'
|
||||
import { AxiosInstance } from 'axios'
|
||||
|
||||
import { WebSocketsClientMessenger } from './WebSocketsClientMessenger'
|
||||
|
||||
describe('WebSocketsClientMessenger', () => {
|
||||
let connectionIds: string[]
|
||||
let webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface
|
||||
let httpClient: AxiosInstance
|
||||
|
||||
const webSocketsApiUrl = 'http://test-websockets'
|
||||
|
||||
const createService = () =>
|
||||
new WebSocketsClientMessenger(webSocketsConnectionRepository, httpClient, webSocketsApiUrl)
|
||||
|
||||
beforeEach(() => {
|
||||
connectionIds = ['1', '2']
|
||||
|
||||
webSocketsConnectionRepository = {} as jest.Mocked<WebSocketsConnectionRepositoryInterface>
|
||||
webSocketsConnectionRepository.findAllByUserUuid = jest.fn().mockReturnValue(connectionIds)
|
||||
|
||||
httpClient = {} as jest.Mocked<AxiosInstance>
|
||||
httpClient.request = jest.fn()
|
||||
})
|
||||
|
||||
it('should send a message to all user connections', async () => {
|
||||
await createService().send('1-2-3', 'message')
|
||||
|
||||
expect(httpClient.request).toHaveBeenCalledTimes(connectionIds.length)
|
||||
connectionIds.map((id, index) => {
|
||||
expect(httpClient.request).toHaveBeenNthCalledWith(
|
||||
index + 1,
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
url: `${webSocketsApiUrl}/${id}`,
|
||||
data: 'message',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
0
packages/websockets/test-setup.ts
Normal file
0
packages/websockets/test-setup.ts
Normal file
13
packages/websockets/tsconfig.json
Normal file
13
packages/websockets/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"bin/**/*",
|
||||
"migrations/**/*",
|
||||
],
|
||||
"references": []
|
||||
}
|
||||
17
packages/websockets/wait-for.sh
Executable file
17
packages/websockets/wait-for.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
host="$1"
|
||||
shift
|
||||
port="$1"
|
||||
shift
|
||||
cmd="$@"
|
||||
|
||||
while ! nc -vz $host $port; do
|
||||
>&2 echo "$host:$port is unavailable yet - waiting for it to start"
|
||||
sleep 10
|
||||
done
|
||||
|
||||
>&2 echo "$host:$port is up - executing command"
|
||||
exec $cmd
|
||||
@@ -3,6 +3,40 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.2](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.13.1...@standardnotes/workspace-server@1.13.2) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
## [1.13.1](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.13.0...@standardnotes/workspace-server@1.13.1) (2022-10-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **workspace:** add workspace to workspace user foreign keys ([4f6a2a8](https://github.com/standardnotes/server/commit/4f6a2a83d3d7b57d176e169f33780730eeae6919))
|
||||
|
||||
# [1.13.0](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.12.0...@standardnotes/workspace-server@1.13.0) (2022-10-12)
|
||||
|
||||
### Features
|
||||
|
||||
* **workspace:** add endpoints for initiating keyshare in a workspace ([0c1a779](https://github.com/standardnotes/server/commit/0c1a779ef03819928e7e791a6843d90eb9fed964))
|
||||
|
||||
# [1.12.0](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.11.0...@standardnotes/workspace-server@1.12.0) (2022-10-12)
|
||||
|
||||
### Features
|
||||
|
||||
* **workspace:** add initiating key share ([cea9021](https://github.com/standardnotes/server/commit/cea9021c164588969890370a2332f11749ac820e))
|
||||
|
||||
# [1.11.0](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.10.0...@standardnotes/workspace-server@1.11.0) (2022-10-11)
|
||||
|
||||
### Features
|
||||
|
||||
* add listin worspaces and workspace users ([095d13f](https://github.com/standardnotes/server/commit/095d13f8bbfe543fcf086840e1a985447a6c51ef))
|
||||
|
||||
# [1.10.0](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.9.0...@standardnotes/workspace-server@1.10.0) (2022-10-11)
|
||||
|
||||
### Features
|
||||
|
||||
* **workspace:** extract workspace user status to common ([8bc9261](https://github.com/standardnotes/server/commit/8bc92616d2fbeb833c3fcbef6b87538745fc7f3e))
|
||||
|
||||
# [1.9.0](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.8.0...@standardnotes/workspace-server@1.9.0) (2022-10-11)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'newrelic'
|
||||
import * as Sentry from '@sentry/node'
|
||||
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressHealthCheckController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressInvitesController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressWorkspacesController'
|
||||
|
||||
import * as cors from 'cors'
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class addWorkspaceUserForeignKey1665580464598 implements MigrationInterface {
|
||||
name = 'addWorkspaceUserForeignKey1665580464598'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE `workspace_users` ADD CONSTRAINT `FK_cd407e5f2c4f3156ad2015aed41` FOREIGN KEY (`workspace_uuid`) REFERENCES `workspaces`(`uuid`) ON DELETE CASCADE ON UPDATE NO ACTION',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `workspace_users` DROP FOREIGN KEY `FK_cd407e5f2c4f3156ad2015aed41`')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/workspace-server",
|
||||
"version": "1.9.0",
|
||||
"version": "1.13.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -25,10 +25,11 @@
|
||||
"dependencies": {
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.3.0",
|
||||
"@standardnotes/api": "^1.12.1",
|
||||
"@standardnotes/api": "^1.16.0",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-events": "workspace:^",
|
||||
"@standardnotes/domain-events-infra": "workspace:^",
|
||||
"@standardnotes/models": "^1.26.0",
|
||||
"@standardnotes/security": "workspace:*",
|
||||
"@standardnotes/time": "workspace:^",
|
||||
"aws-sdk": "^2.1159.0",
|
||||
|
||||
@@ -38,6 +38,15 @@ import { WorkspaceInvite } from '../Domain/Invite/WorkspaceInvite'
|
||||
import { InviteToWorkspace } from '../Domain/UseCase/InviteToWorkspace/InviteToWorkspace'
|
||||
import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
|
||||
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
|
||||
import { WorkspaceProjection } from '../Domain/Projection/WorkspaceProjection'
|
||||
import { WorkspaceProjector } from '../Domain/Projection/WorkspaceProjector'
|
||||
import { ProjectorInterface } from '../Domain/Projection/ProjectorInterface'
|
||||
import { WorkspaceUserProjection } from '../Domain/Projection/WorkspaceUserProjection'
|
||||
import { WorkspaceUserProjector } from '../Domain/Projection/WorkspaceUserProjector'
|
||||
import { AcceptInvitation } from '../Domain/UseCase/AcceptInvitation/AcceptInvitation'
|
||||
import { ListWorkspaces } from '../Domain/UseCase/ListWorkspaces/ListWorkspaces'
|
||||
import { ListWorkspaceUsers } from '../Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsers'
|
||||
import { InitiateKeyShare } from '../Domain/UseCase/InitiateKeyShare/InitiateKeyShare'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -131,8 +140,17 @@ export class ContainerConfigLoader {
|
||||
// use cases
|
||||
container.bind<CreateWorkspace>(TYPES.CreateWorkspace).to(CreateWorkspace)
|
||||
container.bind<InviteToWorkspace>(TYPES.InviteToWorkspace).to(InviteToWorkspace)
|
||||
container.bind<AcceptInvitation>(TYPES.AcceptInvitation).to(AcceptInvitation)
|
||||
container.bind<ListWorkspaces>(TYPES.ListWorkspaces).to(ListWorkspaces)
|
||||
container.bind<ListWorkspaceUsers>(TYPES.ListWorkspaceUsers).to(ListWorkspaceUsers)
|
||||
container.bind<InitiateKeyShare>(TYPES.InitiateKeyShare).to(InitiateKeyShare)
|
||||
// Handlers
|
||||
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
||||
// Projection
|
||||
container.bind<ProjectorInterface<Workspace, WorkspaceProjection>>(TYPES.WorkspaceProjector).to(WorkspaceProjector)
|
||||
container
|
||||
.bind<ProjectorInterface<WorkspaceUser, WorkspaceUserProjection>>(TYPES.WorkspaceUserProjector)
|
||||
.to(WorkspaceUserProjector)
|
||||
// Services
|
||||
container.bind<DomainEventFactoryInterface>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
||||
|
||||
@@ -28,8 +28,15 @@ const TYPES = {
|
||||
// use cases
|
||||
CreateWorkspace: Symbol.for('CreateWorkspace'),
|
||||
InviteToWorkspace: Symbol.for('InviteToWorkspace'),
|
||||
AcceptInvitation: Symbol.for('AcceptInvitation'),
|
||||
ListWorkspaces: Symbol.for('ListWorkspaces'),
|
||||
ListWorkspaceUsers: Symbol.for('ListWorkspaceUsers'),
|
||||
InitiateKeyShare: Symbol.for('InitiateKeyShare'),
|
||||
// Handlers
|
||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||
// Projection
|
||||
WorkspaceProjector: Symbol.for('WorkspaceProjector'),
|
||||
WorkspaceUserProjector: Symbol.for('WorkspaceUserProjector'),
|
||||
// Services
|
||||
Timer: Symbol.for('Timer'),
|
||||
CrossServiceTokenDecoder: Symbol.for('CrossServiceTokenDecoder'),
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
import { WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common'
|
||||
import 'reflect-metadata'
|
||||
import { ProjectorInterface } from '../Domain/Projection/ProjectorInterface'
|
||||
import { WorkspaceProjection } from '../Domain/Projection/WorkspaceProjection'
|
||||
import { WorkspaceUserProjection } from '../Domain/Projection/WorkspaceUserProjection'
|
||||
import { AcceptInvitation } from '../Domain/UseCase/AcceptInvitation/AcceptInvitation'
|
||||
|
||||
import { CreateWorkspace } from '../Domain/UseCase/CreateWorkspace/CreateWorkspace'
|
||||
import { InitiateKeyShare } from '../Domain/UseCase/InitiateKeyShare/InitiateKeyShare'
|
||||
import { InviteToWorkspace } from '../Domain/UseCase/InviteToWorkspace/InviteToWorkspace'
|
||||
import { ListWorkspaces } from '../Domain/UseCase/ListWorkspaces/ListWorkspaces'
|
||||
import { ListWorkspaceUsers } from '../Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsers'
|
||||
import { Workspace } from '../Domain/Workspace/Workspace'
|
||||
import { WorkspaceUser } from '../Domain/Workspace/WorkspaceUser'
|
||||
|
||||
import { WorkspacesController } from './WorkspacesController'
|
||||
|
||||
describe('WorkspacesController', () => {
|
||||
let doCreateWorkspace: CreateWorkspace
|
||||
let doInviteToWorkspace: InviteToWorkspace
|
||||
let doAcceptInvitation: AcceptInvitation
|
||||
let doListWorkspaces: ListWorkspaces
|
||||
let doListWorkspaceUsers: ListWorkspaceUsers
|
||||
let doInitiateKeyshare: InitiateKeyShare
|
||||
let workspacesProject: ProjectorInterface<Workspace, WorkspaceProjection>
|
||||
let workspaceUsersProjector: ProjectorInterface<WorkspaceUser, WorkspaceUserProjection>
|
||||
let workspace1: Workspace
|
||||
let workspace2: Workspace
|
||||
let workspaceUser1: WorkspaceUser
|
||||
let workspaceUser2: WorkspaceUser
|
||||
|
||||
const createController = () => new WorkspacesController(doCreateWorkspace, doInviteToWorkspace)
|
||||
const createController = () =>
|
||||
new WorkspacesController(
|
||||
doCreateWorkspace,
|
||||
doInviteToWorkspace,
|
||||
doListWorkspaces,
|
||||
doListWorkspaceUsers,
|
||||
doAcceptInvitation,
|
||||
doInitiateKeyshare,
|
||||
workspacesProject,
|
||||
workspaceUsersProjector,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
doCreateWorkspace = {} as jest.Mocked<CreateWorkspace>
|
||||
@@ -18,6 +47,26 @@ describe('WorkspacesController', () => {
|
||||
|
||||
doInviteToWorkspace = {} as jest.Mocked<InviteToWorkspace>
|
||||
doInviteToWorkspace.execute = jest.fn().mockReturnValue({ invite: { uuid: 'i-1-2-3' } })
|
||||
|
||||
doListWorkspaces = {} as jest.Mocked<ListWorkspaces>
|
||||
doListWorkspaces.execute = jest
|
||||
.fn()
|
||||
.mockReturnValue({ ownedWorkspaces: [workspace1], joinedWorkspaces: [workspace2] })
|
||||
|
||||
doListWorkspaceUsers = {} as jest.Mocked<ListWorkspaceUsers>
|
||||
doListWorkspaceUsers.execute = jest.fn().mockReturnValue({ workspaceUsers: [workspaceUser1, workspaceUser2] })
|
||||
|
||||
doAcceptInvitation = {} as jest.Mocked<AcceptInvitation>
|
||||
doAcceptInvitation.execute = jest.fn().mockReturnValue({ success: true })
|
||||
|
||||
doInitiateKeyshare = {} as jest.Mocked<InitiateKeyShare>
|
||||
doInitiateKeyshare.execute = jest.fn().mockReturnValue({ success: true })
|
||||
|
||||
workspacesProject = {} as jest.Mocked<ProjectorInterface<Workspace, WorkspaceProjection>>
|
||||
workspacesProject.project = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
|
||||
workspaceUsersProjector = {} as jest.Mocked<ProjectorInterface<WorkspaceUser, WorkspaceUserProjection>>
|
||||
workspaceUsersProjector.project = jest.fn().mockReturnValue({ bar: 'buzz' })
|
||||
})
|
||||
|
||||
it('should create a workspace', async () => {
|
||||
@@ -52,4 +101,103 @@ describe('WorkspacesController', () => {
|
||||
status: 200,
|
||||
})
|
||||
})
|
||||
|
||||
it('should accept an invite', async () => {
|
||||
const result = await createController().acceptInvite({
|
||||
userUuid: '1-2-3',
|
||||
encryptedPrivateKey: 'foo',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'bar',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
status: 200,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not accept an invite if it fails', async () => {
|
||||
doAcceptInvitation.execute = jest.fn().mockReturnValue({ success: false })
|
||||
const result = await createController().acceptInvite({
|
||||
userUuid: '1-2-3',
|
||||
encryptedPrivateKey: 'foo',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'bar',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
error: {
|
||||
message: 'Could not accept invite',
|
||||
},
|
||||
},
|
||||
status: 400,
|
||||
})
|
||||
})
|
||||
|
||||
it('should list workspaces', async () => {
|
||||
const result = await createController().listWorkspaces({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
ownedWorkspaces: [{ foo: 'bar' }],
|
||||
joinedWorkspaces: [{ foo: 'bar' }],
|
||||
},
|
||||
status: 200,
|
||||
})
|
||||
})
|
||||
|
||||
it('should list workspace users', async () => {
|
||||
const result = await createController().listWorkspaceUsers({
|
||||
userUuid: '1-2-3',
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
users: [{ bar: 'buzz' }, { bar: 'buzz' }],
|
||||
},
|
||||
status: 200,
|
||||
})
|
||||
})
|
||||
|
||||
it('should initiate keyshare', async () => {
|
||||
const result = await createController().initiateKeyshare({
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foo',
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
performingUserUuid: 'p-1-2-3',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
status: 200,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not initiate keyshare if it fails', async () => {
|
||||
doInitiateKeyshare.execute = jest.fn().mockReturnValue({ success: false })
|
||||
|
||||
const result = await createController().initiateKeyshare({
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foo',
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
performingUserUuid: 'p-1-2-3',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
data: {
|
||||
error: {
|
||||
message: 'Could not initiate keyshare.',
|
||||
},
|
||||
},
|
||||
status: 400,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,20 +6,73 @@ import {
|
||||
WorkspaceInvitationRequestParams,
|
||||
WorkspaceInvitationResponse,
|
||||
WorkspaceServerInterface,
|
||||
WorkspaceListRequestParams,
|
||||
WorkspaceListResponse,
|
||||
WorkspaceInvitationAcceptingRequestParams,
|
||||
WorkspaceInvitationAcceptingResponse,
|
||||
WorkspaceUserListRequestParams,
|
||||
WorkspaceKeyshareInitiatingRequestParams,
|
||||
WorkspaceKeyshareInitiatingResponse,
|
||||
} from '@standardnotes/api'
|
||||
import { Uuid, WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { CreateWorkspace } from '../Domain/UseCase/CreateWorkspace/CreateWorkspace'
|
||||
import { InviteToWorkspace } from '../Domain/UseCase/InviteToWorkspace/InviteToWorkspace'
|
||||
import { ProjectorInterface } from '../Domain/Projection/ProjectorInterface'
|
||||
import { WorkspaceProjection } from '../Domain/Projection/WorkspaceProjection'
|
||||
import { Workspace } from '../Domain/Workspace/Workspace'
|
||||
import { ListWorkspaces } from '../Domain/UseCase/ListWorkspaces/ListWorkspaces'
|
||||
import { WorkspaceUserListResponse } from '@standardnotes/api/dist/Domain/Response/Workspace/WorkspaceUserListResponse'
|
||||
import { AcceptInvitation } from '../Domain/UseCase/AcceptInvitation/AcceptInvitation'
|
||||
import { WorkspaceUser } from '../Domain/Workspace/WorkspaceUser'
|
||||
import { WorkspaceUserProjection } from '../Domain/Projection/WorkspaceUserProjection'
|
||||
import { ListWorkspaceUsers } from '../Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsers'
|
||||
import { InitiateKeyShare } from '../Domain/UseCase/InitiateKeyShare/InitiateKeyShare'
|
||||
|
||||
@injectable()
|
||||
export class WorkspacesController implements WorkspaceServerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.CreateWorkspace) private doCreateWorkspace: CreateWorkspace,
|
||||
@inject(TYPES.InviteToWorkspace) private doInviteToWorkspace: InviteToWorkspace,
|
||||
@inject(TYPES.ListWorkspaces) private doListWorkspaces: ListWorkspaces,
|
||||
@inject(TYPES.ListWorkspaceUsers) private doListWorkspaceUsers: ListWorkspaceUsers,
|
||||
@inject(TYPES.AcceptInvitation) private doAcceptInvite: AcceptInvitation,
|
||||
@inject(TYPES.InitiateKeyShare) private doInitiateKeyshare: InitiateKeyShare,
|
||||
@inject(TYPES.WorkspaceProjector) private workspaceProjector: ProjectorInterface<Workspace, WorkspaceProjection>,
|
||||
@inject(TYPES.WorkspaceUserProjector)
|
||||
private workspaceUserProjector: ProjectorInterface<WorkspaceUser, WorkspaceUserProjection>,
|
||||
) {}
|
||||
|
||||
async initiateKeyshare(
|
||||
params: WorkspaceKeyshareInitiatingRequestParams,
|
||||
): Promise<WorkspaceKeyshareInitiatingResponse> {
|
||||
const result = await this.doInitiateKeyshare.execute({
|
||||
userUuid: params.userUuid,
|
||||
workspaceUuid: params.workspaceUuid,
|
||||
encryptedWorkspaceKey: params.encryptedWorkspaceKey,
|
||||
performingUserUuid: params.performingUserUuid as Uuid,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: {
|
||||
error: {
|
||||
message: 'Could not initiate keyshare.',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async inviteToWorkspace(params: WorkspaceInvitationRequestParams): Promise<WorkspaceInvitationResponse> {
|
||||
const { invite } = await this.doInviteToWorkspace.execute({
|
||||
inviteeEmail: params.inviteeEmail,
|
||||
@@ -49,4 +102,69 @@ export class WorkspacesController implements WorkspaceServerInterface {
|
||||
data: { uuid: workspace.uuid },
|
||||
}
|
||||
}
|
||||
|
||||
async listWorkspaces(params: WorkspaceListRequestParams): Promise<WorkspaceListResponse> {
|
||||
const { ownedWorkspaces, joinedWorkspaces } = await this.doListWorkspaces.execute({
|
||||
userUuid: params.userUuid as Uuid,
|
||||
})
|
||||
|
||||
const ownedWorkspacesProjections = []
|
||||
for (const ownedWorkspace of ownedWorkspaces) {
|
||||
ownedWorkspacesProjections.push(await this.workspaceProjector.project(ownedWorkspace))
|
||||
}
|
||||
|
||||
const joinedWorkspacesProjections = []
|
||||
for (const joinedWorkspace of joinedWorkspaces) {
|
||||
joinedWorkspacesProjections.push(await this.workspaceProjector.project(joinedWorkspace))
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: { ownedWorkspaces: ownedWorkspacesProjections, joinedWorkspaces: joinedWorkspacesProjections },
|
||||
}
|
||||
}
|
||||
|
||||
async listWorkspaceUsers(params: WorkspaceUserListRequestParams): Promise<WorkspaceUserListResponse> {
|
||||
const { workspaceUsers } = await this.doListWorkspaceUsers.execute({
|
||||
userUuid: params.userUuid as Uuid,
|
||||
workspaceUuid: params.workspaceUuid,
|
||||
})
|
||||
|
||||
const workspaceUserProjections = []
|
||||
for (const workspaceUser of workspaceUsers) {
|
||||
workspaceUserProjections.push(await this.workspaceUserProjector.project(workspaceUser))
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: { users: workspaceUserProjections },
|
||||
}
|
||||
}
|
||||
|
||||
async acceptInvite(params: WorkspaceInvitationAcceptingRequestParams): Promise<WorkspaceInvitationAcceptingResponse> {
|
||||
const result = await this.doAcceptInvite.execute({
|
||||
acceptingUserUuid: params.userUuid,
|
||||
encryptedPrivateKey: params.encryptedPrivateKey,
|
||||
invitationUuid: params.inviteUuid,
|
||||
publicKey: params.publicKey,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
status: HttpStatusCode.BadRequest,
|
||||
data: {
|
||||
error: {
|
||||
message: 'Could not accept invite',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface ProjectorInterface<T, E> {
|
||||
project(object: T): Promise<E>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Workspace } from '@standardnotes/models'
|
||||
|
||||
export type WorkspaceProjection = Workspace
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { WorkspaceType } from '@standardnotes/common'
|
||||
import { Workspace } from '../Workspace/Workspace'
|
||||
|
||||
import { WorkspaceProjector } from './WorkspaceProjector'
|
||||
|
||||
describe('WorkspaceProjector', () => {
|
||||
const createProjector = () => new WorkspaceProjector()
|
||||
|
||||
it('should project a workspace', async () => {
|
||||
expect(
|
||||
await createProjector().project({
|
||||
uuid: 'w-1-2-3',
|
||||
type: WorkspaceType.Private,
|
||||
name: 'test',
|
||||
keyRotationIndex: 0,
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
} as jest.Mocked<Workspace>),
|
||||
).toEqual({
|
||||
uuid: 'w-1-2-3',
|
||||
type: 'private',
|
||||
name: 'test',
|
||||
keyRotationIndex: 0,
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import { injectable } from 'inversify'
|
||||
import { ProjectorInterface } from './ProjectorInterface'
|
||||
|
||||
import { WorkspaceProjection } from './WorkspaceProjection'
|
||||
import { Workspace } from '../Workspace/Workspace'
|
||||
|
||||
@injectable()
|
||||
export class WorkspaceProjector implements ProjectorInterface<Workspace, WorkspaceProjection> {
|
||||
async project(workspace: Workspace): Promise<WorkspaceProjection> {
|
||||
return {
|
||||
uuid: workspace.uuid,
|
||||
type: workspace.type,
|
||||
name: workspace.name,
|
||||
keyRotationIndex: workspace.keyRotationIndex,
|
||||
createdAt: workspace.createdAt,
|
||||
updatedAt: workspace.updatedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { WorkspaceUser } from '@standardnotes/models'
|
||||
|
||||
export type WorkspaceUserProjection = WorkspaceUser
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { WorkspaceAccessLevel, WorkspaceUserStatus } from '@standardnotes/common'
|
||||
import { WorkspaceUser } from '../Workspace/WorkspaceUser'
|
||||
|
||||
import { WorkspaceUserProjector } from './WorkspaceUserProjector'
|
||||
|
||||
describe('WorkspaceUserProjector', () => {
|
||||
const createProjector = () => new WorkspaceUserProjector()
|
||||
|
||||
it('should project a workspace user', async () => {
|
||||
expect(
|
||||
await createProjector().project({
|
||||
uuid: '1-2-3',
|
||||
accessLevel: WorkspaceAccessLevel.Owner,
|
||||
userUuid: 'u-1-2-3',
|
||||
userDisplayName: 'foobar',
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
encryptedWorkspaceKey: 'foo',
|
||||
publicKey: 'bar',
|
||||
encryptedPrivateKey: 'buzz',
|
||||
status: WorkspaceUserStatus.PendingKeyshare,
|
||||
keyRotationIndex: 0,
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
} as jest.Mocked<WorkspaceUser>),
|
||||
).toEqual({
|
||||
uuid: '1-2-3',
|
||||
accessLevel: 'owner',
|
||||
userUuid: 'u-1-2-3',
|
||||
userDisplayName: 'foobar',
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
encryptedWorkspaceKey: 'foo',
|
||||
publicKey: 'bar',
|
||||
encryptedPrivateKey: 'buzz',
|
||||
status: 'pending-keyshare',
|
||||
keyRotationIndex: 0,
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
})
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user