mirror of
https://github.com/standardnotes/server
synced 2026-02-09 02:01:13 -05:00
Compare commits
43 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9ec846a3c | ||
|
|
786829f317 | ||
|
|
10891af33b | ||
|
|
3f091175e2 | ||
|
|
0e5d7c918b | ||
|
|
fd2358a4b8 | ||
|
|
dd36b1859c | ||
|
|
8c9a8a484f | ||
|
|
451ed1ae3a | ||
|
|
4ec30df2dc | ||
|
|
163b7ff2d8 | ||
|
|
6e136e98b3 | ||
|
|
100eef2cb8 | ||
|
|
1d8cf4b675 | ||
|
|
5a01517097 | ||
|
|
ca54d4e0a0 | ||
|
|
2bcc4a2254 | ||
|
|
afe5ff3e70 | ||
|
|
4d8b021284 | ||
|
|
281dd3d378 | ||
|
|
7efb48dd2a | ||
|
|
d7b68bcafb | ||
|
|
04d1dffe53 | ||
|
|
db492c3787 | ||
|
|
d04b04507a | ||
|
|
6c87d3614d | ||
|
|
dd6d409ebb | ||
|
|
9f75c2b601 | ||
|
|
9df87a0e3d | ||
|
|
628dcf1539 | ||
|
|
38b42dad62 | ||
|
|
90359d61d9 | ||
|
|
57c3b9c29e | ||
|
|
b25f2e8c54 | ||
|
|
5be40fa99c | ||
|
|
bc909dd3aa | ||
|
|
3110c20596 | ||
|
|
69a9c5555b | ||
|
|
cca6c7f65e | ||
|
|
7a8a5fcfdf | ||
|
|
845d310af9 | ||
|
|
d61e6f338e | ||
|
|
f2ddbc82d0 |
141
.github/workflows/api-gateway.release.yml
vendored
Normal file
141
.github/workflows/api-gateway.release.yml
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
name: Api Gateway
|
||||
|
||||
concurrency:
|
||||
group: api_gateway
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*standardnotes/api-gateway*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16.x'
|
||||
- run: yarn lint:api-gateway
|
||||
|
||||
publish-aws-ecr:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build locally
|
||||
run: yarn build:api-gateway
|
||||
- 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: api-gateway
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
yarn docker build @standardnotes/api-gateway -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: Build locally
|
||||
run: yarn build:api-gateway
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build, tag, and push image to Docker Hub
|
||||
run: |
|
||||
yarn docker build @standardnotes/api-gateway -t standardnotes/api-gateway:${{ github.sha }}
|
||||
docker push standardnotes/api-gateway:${{ github.sha }}
|
||||
docker tag standardnotes/api-gateway:${{ github.sha }} standardnotes/api-gateway:latest
|
||||
docker push standardnotes/api-gateway: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: DEV - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition api-gateway-dev --query taskDefinition > task-definition.json
|
||||
- name: DEV - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="api-gateway-dev") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: DEV - Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def-dev
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: api-gateway-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/api-gateway:${{ github.sha }}
|
||||
- name: DEV - Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def-dev.outputs.task-definition }}
|
||||
service: api-gateway-dev
|
||||
cluster: dev
|
||||
wait-for-service-stability: true
|
||||
- name: PROD - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition api-gateway-prod --query taskDefinition > task-definition.json
|
||||
- name: PROD - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="api-gateway-prod") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: PROD - 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: api-gateway-prod
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/api-gateway:${{ github.sha }}
|
||||
- name: PROD - 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: api-gateway-prod
|
||||
cluster: prod
|
||||
wait-for-service-stability: true
|
||||
|
||||
newrelic:
|
||||
needs: deploy-web
|
||||
|
||||
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_API_GATEWAY_WEB_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
@@ -1,14 +1,13 @@
|
||||
name: Auth Server Dev
|
||||
name: Auth Server
|
||||
|
||||
concurrency:
|
||||
group: auth_dev_environment
|
||||
group: auth
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '@standardnotes/auth-server@[0-9]*.[0-9]*.[0-9]*-alpha.[0-9]*'
|
||||
- '@standardnotes/auth-server@[0-9]*.[0-9]*.[0-9]*-beta.[0-9]*'
|
||||
- '*standardnotes/auth-server*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -50,8 +49,8 @@ jobs:
|
||||
run: |
|
||||
yarn docker build @standardnotes/auth-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:dev
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:dev
|
||||
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
|
||||
@@ -71,8 +70,8 @@ jobs:
|
||||
run: |
|
||||
yarn docker build @standardnotes/auth-server -t standardnotes/auth:${{ github.sha }}
|
||||
docker push standardnotes/auth:${{ github.sha }}
|
||||
docker tag standardnotes/auth:${{ github.sha }} standardnotes/auth:dev
|
||||
docker push standardnotes/auth:dev
|
||||
docker tag standardnotes/auth:${{ github.sha }} standardnotes/auth:latest
|
||||
docker push standardnotes/auth:latest
|
||||
|
||||
deploy-web:
|
||||
needs: publish-aws-ecr
|
||||
@@ -86,26 +85,46 @@ jobs:
|
||||
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
|
||||
- name: DEV - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition auth-dev --query taskDefinition > task-definition.json
|
||||
- name: Fill in the new version in the Amazon ECS task definition
|
||||
- name: DEV - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="auth-dev") | .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
|
||||
- name: DEV - Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def-dev
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: auth-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/auth:${{ github.sha }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
- name: DEV - Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
task-definition: ${{ steps.task-def-dev.outputs.task-definition }}
|
||||
service: auth-dev
|
||||
cluster: dev
|
||||
wait-for-service-stability: true
|
||||
- name: PROD - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition auth-prod --query taskDefinition > task-definition.json
|
||||
- name: PROD - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="auth-prod") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: PROD - 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: auth-prod
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/auth:${{ github.sha }}
|
||||
- name: PROD - 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: auth-prod
|
||||
cluster: prod
|
||||
wait-for-service-stability: true
|
||||
|
||||
deploy-worker:
|
||||
needs: publish-aws-ecr
|
||||
@@ -119,26 +138,46 @@ jobs:
|
||||
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
|
||||
- name: DEV - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition auth-worker-dev --query taskDefinition > task-definition.json
|
||||
- name: Fill in the new version in the Amazon ECS task definition
|
||||
- name: DEV - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="auth-worker-dev") | .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
|
||||
- name: DEV - Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def-dev
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: auth-worker-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/auth:${{ github.sha }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
- name: DEV - Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
task-definition: ${{ steps.task-def-dev.outputs.task-definition }}
|
||||
service: auth-worker-dev
|
||||
cluster: dev
|
||||
wait-for-service-stability: true
|
||||
- name: PROD - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition auth-worker-prod --query taskDefinition > task-definition.json
|
||||
- name: PROD - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="auth-worker-prod") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: PROD - 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: auth-worker-prod
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/auth:${{ github.sha }}
|
||||
- name: PROD - 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: auth-worker-prod
|
||||
cluster: prod
|
||||
wait-for-service-stability: true
|
||||
|
||||
newrelic:
|
||||
needs: [ deploy-web, deploy-worker ]
|
||||
@@ -150,7 +189,7 @@ jobs:
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_AUTH_WEB_DEV }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_AUTH_WEB_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
@@ -159,19 +198,7 @@ jobs:
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_AUTH_WORKER_DEV }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_AUTH_WORKER_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
|
||||
notify_discord:
|
||||
needs: [ deploy-web, deploy-worker ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Run Discord Webhook
|
||||
uses: johnnyhuy/actions-discord-git-webhook@main
|
||||
with:
|
||||
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
|
||||
204
.github/workflows/files.release.yml
vendored
Normal file
204
.github/workflows/files.release.yml
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
name: Files Server
|
||||
|
||||
concurrency:
|
||||
group: files
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*standardnotes/files-server*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16.x'
|
||||
- run: yarn lint:files
|
||||
- run: yarn test:files
|
||||
|
||||
publish-aws-ecr:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build locally
|
||||
run: yarn build:files
|
||||
- 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: files
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
yarn docker build @standardnotes/files-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: Build locally
|
||||
run: yarn build:files
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build, tag, and push image to Docker Hub
|
||||
run: |
|
||||
yarn docker build @standardnotes/files-server -t standardnotes/files:${{ github.sha }}
|
||||
docker push standardnotes/files:${{ github.sha }}
|
||||
docker tag standardnotes/files:${{ github.sha }} standardnotes/files:latest
|
||||
docker push standardnotes/files: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: DEV - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition files-dev --query taskDefinition > task-definition.json
|
||||
- name: DEV - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="files-dev") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: DEV - Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def-dev
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: files-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/files:${{ github.sha }}
|
||||
- name: DEV - Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def-dev.outputs.task-definition }}
|
||||
service: files-dev
|
||||
cluster: dev
|
||||
wait-for-service-stability: true
|
||||
- name: PROD - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition files-prod --query taskDefinition > task-definition.json
|
||||
- name: PROD - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="files-prod") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: PROD - 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: files-prod
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/files:${{ github.sha }}
|
||||
- name: PROD - 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: files-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: DEV - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition files-worker-dev --query taskDefinition > task-definition.json
|
||||
- name: DEV - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="files-worker-dev") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: DEV - Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def-dev
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: files-worker-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/files:${{ github.sha }}
|
||||
- name: DEV - Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def-dev.outputs.task-definition }}
|
||||
service: files-worker-dev
|
||||
cluster: dev
|
||||
wait-for-service-stability: true
|
||||
- name: PROD - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition files-worker-prod --query taskDefinition > task-definition.json
|
||||
- name: PROD - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="files-worker-prod") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: PROD - 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: files-worker-prod
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/files:${{ github.sha }}
|
||||
- name: PROD - 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: files-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_FILES_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_FILES_WORKER_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
@@ -1,14 +1,13 @@
|
||||
name: Scheduler Server Dev
|
||||
name: Scheduler Server
|
||||
|
||||
concurrency:
|
||||
group: scheduler_dev_environment
|
||||
group: scheduler
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '@standardnotes/scheduler-server@[0-9]*.[0-9]*.[0-9]*-alpha.[0-9]*'
|
||||
- '@standardnotes/scheduler-server@[0-9]*.[0-9]*.[0-9]*-beta.[0-9]*'
|
||||
- '*standardnotes/scheduler-server*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -50,8 +49,8 @@ jobs:
|
||||
run: |
|
||||
yarn docker build @standardnotes/scheduler-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:dev
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:dev
|
||||
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
|
||||
@@ -71,8 +70,8 @@ jobs:
|
||||
run: |
|
||||
yarn docker build @standardnotes/scheduler-server -t standardnotes/scheduler:${{ github.sha }}
|
||||
docker push standardnotes/scheduler:${{ github.sha }}
|
||||
docker tag standardnotes/scheduler:${{ github.sha }} standardnotes/scheduler:dev
|
||||
docker push standardnotes/scheduler:dev
|
||||
docker tag standardnotes/scheduler:${{ github.sha }} standardnotes/scheduler:latest
|
||||
docker push standardnotes/scheduler:latest
|
||||
|
||||
deploy-worker:
|
||||
needs: publish-aws-ecr
|
||||
@@ -86,26 +85,46 @@ jobs:
|
||||
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
|
||||
- name: DEV - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition scheduler-worker-dev --query taskDefinition > task-definition.json
|
||||
- name: Fill in the new version in the Amazon ECS task definition
|
||||
- name: DEV - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="scheduler-worker-dev") | .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
|
||||
- name: DEV - Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def-dev
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: scheduler-worker-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/scheduler-worker:${{ github.sha }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
- name: DEV - Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
task-definition: ${{ steps.task-def-dev.outputs.task-definition }}
|
||||
service: scheduler-worker-dev
|
||||
cluster: dev
|
||||
wait-for-service-stability: true
|
||||
- name: PROD - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition scheduler-worker-prod --query taskDefinition > task-definition.json
|
||||
- name: PROD - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="scheduler-worker-prod") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: PROD - 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: scheduler-worker-prod
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/scheduler-worker:${{ github.sha }}
|
||||
- name: PROD - 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: scheduler-worker-prod
|
||||
cluster: prod
|
||||
wait-for-service-stability: true
|
||||
|
||||
newrelic:
|
||||
needs: [ deploy-worker ]
|
||||
@@ -118,19 +137,7 @@ jobs:
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_SCHEDULER_WORKER_DEV }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_SCHEDULER_WORKER_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
|
||||
notify_discord:
|
||||
needs: [ deploy-worker ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Run Discord Webhook
|
||||
uses: johnnyhuy/actions-discord-git-webhook@main
|
||||
with:
|
||||
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
|
||||
170
.github/workflows/syncing-server.release.dev.yml
vendored
170
.github/workflows/syncing-server.release.dev.yml
vendored
@@ -1,170 +0,0 @@
|
||||
name: Syncing Server Dev
|
||||
|
||||
concurrency:
|
||||
group: syncing_server_dev_environment
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '@standardnotes/syncing-server@[0-9]*.[0-9]*.[0-9]*-alpha.[0-9]*'
|
||||
- '@standardnotes/syncing-server@[0-9]*.[0-9]*.[0-9]*-beta.[0-9]*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16.x'
|
||||
- run: yarn lint:syncing-server
|
||||
- run: yarn test:syncing-server
|
||||
|
||||
publish-aws-ecr:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: cp .env.sample .env
|
||||
- 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: syncing-server-js
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
yarn docker build @standardnotes/syncing-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:dev
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:dev
|
||||
|
||||
publish-docker-hub:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: cp .env.sample .env
|
||||
- name: Publish to Registry
|
||||
uses: elgohr/Publish-Docker-Github-Action@master
|
||||
with:
|
||||
name: standardnotes/syncing-server-js
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
tags: "dev,${{ github.sha }}"
|
||||
|
||||
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 syncing-server-js-dev --query taskDefinition > task-definition.json
|
||||
- name: Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="syncing-server-js-dev") | .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
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: syncing-server-js-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/syncing-server-js:${{ github.sha }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
service: syncing-server-js-dev
|
||||
cluster: dev
|
||||
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 syncing-server-js-worker-dev --query taskDefinition > task-definition.json
|
||||
- name: Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="syncing-server-js-worker-dev") | .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
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: syncing-server-js-worker-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/syncing-server-js:${{ github.sha }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
service: syncing-server-js-worker-dev
|
||||
cluster: dev
|
||||
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_SYNCING_SERVER_WEB_DEV }}
|
||||
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_SYNCING_SERVER_WORKER_DEV }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
|
||||
notify_discord:
|
||||
needs: [ deploy-web, deploy-worker ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Run Discord Webhook
|
||||
uses: johnnyhuy/actions-discord-git-webhook@main
|
||||
with:
|
||||
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
223
.github/workflows/syncing-server.release.yml
vendored
Normal file
223
.github/workflows/syncing-server.release.yml
vendored
Normal file
@@ -0,0 +1,223 @@
|
||||
name: Syncing Server
|
||||
|
||||
concurrency:
|
||||
group: syncing_server
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*standardnotes/syncing-server*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16.x'
|
||||
- run: yarn install --immutable
|
||||
- run: yarn lint:syncing-server
|
||||
- run: yarn test:syncing-server
|
||||
|
||||
publish-aws-ecr:
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build locally
|
||||
run: yarn build:syncing-server
|
||||
- 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: syncing-server-js
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
yarn docker build @standardnotes/syncing-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: Build locally
|
||||
run: yarn build:syncing-server
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build, tag, and push image to Docker Hub
|
||||
run: |
|
||||
yarn docker build @standardnotes/syncing-server -t standardnotes/syncing-server-js:${{ github.sha }}
|
||||
docker push standardnotes/syncing-server-js:${{ github.sha }}
|
||||
docker tag standardnotes/syncing-server-js:${{ github.sha }} standardnotes/syncing-server-js:latest
|
||||
docker push standardnotes/syncing-server-js: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: DEV - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition syncing-server-js-dev --query taskDefinition > task-definition.json
|
||||
- name: DEV - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="syncing-server-js-dev") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: DEV - Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def-dev
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: syncing-server-js-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/syncing-server-js:${{ github.sha }}
|
||||
- name: DEV - Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def-dev.outputs.task-definition }}
|
||||
service: syncing-server-js-dev
|
||||
cluster: dev
|
||||
wait-for-service-stability: true
|
||||
- name: PROD - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition syncing-server-js-prod --query taskDefinition > task-definition.json
|
||||
- name: PROD - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="syncing-server-js-prod") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: PROD - 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: syncing-server-js-prod
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/syncing-server-js:${{ github.sha }}
|
||||
- name: PROD - 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: syncing-server-js-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: DEV - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition syncing-server-js-worker-dev --query taskDefinition > task-definition.json
|
||||
- name: DEV - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="syncing-server-js-worker-dev") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: DEV - Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def-dev
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: syncing-server-js-worker-dev
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/syncing-server-js:${{ github.sha }}
|
||||
- name: DEV - Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.task-def-dev.outputs.task-definition }}
|
||||
service: syncing-server-js-worker-dev
|
||||
cluster: dev
|
||||
wait-for-service-stability: true
|
||||
- name: PROD - Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition syncing-server-js-worker-prod --query taskDefinition > task-definition.json
|
||||
- name: PROD - Fill in the new version in the Amazon ECS task definition
|
||||
run: |
|
||||
jq '(.containerDefinitions[] | select(.name=="syncing-server-js-worker-prod") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
|
||||
- name: PROD - 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: syncing-server-js-worker-prod
|
||||
image: ${{ secrets.AWS_ECR_REGISTRY }}/syncing-server-js:${{ github.sha }}
|
||||
- name: PROD - 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: syncing-server-js-worker-prod
|
||||
cluster: prod
|
||||
wait-for-service-stability: true
|
||||
|
||||
newrelic:
|
||||
needs: [ deploy-web, deploy-worker ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: DEV - 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_SYNCING_SERVER_WEB_DEV }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
- name: DEV - 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_SYNCING_SERVER_WORKER_DEV }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
- name: PROD - 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_SYNCING_SERVER_WEB_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
- name: PROD - 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_SYNCING_SERVER_WORKER_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
9
.github/workflows/version-bump.yml
vendored
9
.github/workflows/version-bump.yml
vendored
@@ -2,7 +2,7 @@ name: Version Bump
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop, main ]
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
@@ -35,10 +35,5 @@ jobs:
|
||||
- name: Install locally
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Bump Prod Version
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Bump Version
|
||||
run: yarn release:prod
|
||||
|
||||
- name: Bump Beta Version
|
||||
if: ${{ github.ref == 'refs/heads/develop' }}
|
||||
run: yarn release:beta
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,3 +15,6 @@ newrelic_agent.log
|
||||
!.yarn/unplugged
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
packages/files/uploads/*
|
||||
!packages/files/uploads/.gitkeep
|
||||
|
||||
BIN
.yarn/cache/@newrelic-aws-sdk-npm-3.1.0-7c3485a153-5601d90c78.zip
vendored
Normal file
BIN
.yarn/cache/@newrelic-aws-sdk-npm-3.1.0-7c3485a153-5601d90c78.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@newrelic-koa-npm-5.0.0-c9c6a0e1dc-e98d921b96.zip
vendored
Normal file
BIN
.yarn/cache/@newrelic-koa-npm-5.0.0-c9c6a0e1dc-e98d921b96.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@newrelic-native-metrics-npm-6.0.2-805c5534f5-78f92bbe7f.zip
vendored
Normal file
BIN
.yarn/cache/@newrelic-native-metrics-npm-6.0.2-805c5534f5-78f92bbe7f.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@newrelic-superagent-npm-4.0.0-6cc7e8ec57-5fb257ac05.zip
vendored
Normal file
BIN
.yarn/cache/@newrelic-superagent-npm-4.0.0-6cc7e8ec57-5fb257ac05.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@standardnotes-auth-npm-3.19.2-5289525e60-2e4b37b303.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-auth-npm-3.19.2-5289525e60-2e4b37b303.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@standardnotes-config-npm-2.0.1-5f34962133-5284e034f2.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-config-npm-2.0.1-5f34962133-5284e034f2.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@standardnotes-domain-events-infra-npm-1.4.127-18a82f2f72-54e37c296f.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-domain-events-infra-npm-1.4.127-18a82f2f72-54e37c296f.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@standardnotes-domain-events-npm-2.29.0-13bec3d9a7-1b68999e2a.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-domain-events-npm-2.29.0-13bec3d9a7-1b68999e2a.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@standardnotes-time-npm-1.7.1-e6859705d5-ccb9c4af73.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-time-npm-1.7.1-e6859705d5-ccb9c4af73.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@types-busboy-npm-1.5.0-0e24e7f08d-ffa7bf25c0.zip
vendored
Normal file
BIN
.yarn/cache/@types-busboy-npm-1.5.0-0e24e7f08d-ffa7bf25c0.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@types-connect-busboy-npm-1.0.0-fca702448d-ccbf7bc42d.zip
vendored
Normal file
BIN
.yarn/cache/@types-connect-busboy-npm-1.0.0-fca702448d-ccbf7bc42d.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-eslint-plugin-npm-4.33.0-b5d1be4879-d74855d0a5.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-eslint-plugin-npm-4.33.0-b5d1be4879-d74855d0a5.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-experimental-utils-npm-4.33.0-f10f287886-f859800ada.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-experimental-utils-npm-4.33.0-f10f287886-f859800ada.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-parser-npm-4.33.0-799c6ce8d5-102457eae1.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-parser-npm-4.33.0-799c6ce8d5-102457eae1.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-scope-manager-npm-4.33.0-28014c179d-9a25fb7ba7.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-scope-manager-npm-4.33.0-28014c179d-9a25fb7ba7.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-types-npm-4.33.0-9e9b956afa-3baae1ca35.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-types-npm-4.33.0-9e9b956afa-3baae1ca35.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-typescript-estree-npm-4.33.0-b6b79c10d0-2566984390.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-typescript-estree-npm-4.33.0-b6b79c10d0-2566984390.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-visitor-keys-npm-4.33.0-8b7e72a3c9-59953e474a.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-visitor-keys-npm-4.33.0-8b7e72a3c9-59953e474a.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/agent-base-npm-5.1.1-d451a4ad62-61ae789f30.zip
vendored
Normal file
BIN
.yarn/cache/agent-base-npm-5.1.1-d451a4ad62-61ae789f30.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/aws-sdk-npm-2.1160.0-1a3db600b7-b95647d4de.zip
vendored
Normal file
BIN
.yarn/cache/aws-sdk-npm-2.1160.0-1a3db600b7-b95647d4de.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/busboy-npm-1.6.0-ebb5cbb04b-32801e2c01.zip
vendored
Normal file
BIN
.yarn/cache/busboy-npm-1.6.0-ebb5cbb04b-32801e2c01.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/connect-busboy-npm-1.0.0-9908d1785d-e4a8cece06.zip
vendored
Normal file
BIN
.yarn/cache/connect-busboy-npm-1.0.0-9908d1785d-e4a8cece06.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/dotenv-npm-8.6.0-2ce3e9f7bb-38e902c80b.zip
vendored
Normal file
BIN
.yarn/cache/dotenv-npm-8.6.0-2ce3e9f7bb-38e902c80b.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/express-winston-npm-4.2.0-e4cfb26486-029529107f.zip
vendored
Normal file
BIN
.yarn/cache/express-winston-npm-4.2.0-e4cfb26486-029529107f.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/helmet-npm-4.4.1-286ac392ee-cfe385e185.zip
vendored
Normal file
BIN
.yarn/cache/helmet-npm-4.4.1-286ac392ee-cfe385e185.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/helmet-npm-4.6.0-f244fd965c-139ad678d1.zip
vendored
Normal file
BIN
.yarn/cache/helmet-npm-4.6.0-f244fd965c-139ad678d1.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/https-proxy-agent-npm-4.0.0-9021ec873f-19471d5aae.zip
vendored
Normal file
BIN
.yarn/cache/https-proxy-agent-npm-4.0.0-9021ec873f-19471d5aae.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/newrelic-npm-7.5.2-b949bcba7c-f6c67dbb7d.zip
vendored
Normal file
BIN
.yarn/cache/newrelic-npm-7.5.2-b949bcba7c-f6c67dbb7d.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/prettyjson-npm-1.2.5-a72b7bf823-e36e8ae4f7.zip
vendored
Normal file
BIN
.yarn/cache/prettyjson-npm-1.2.5-a72b7bf823-e36e8ae4f7.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/streamsearch-npm-1.1.0-fc3ad6536d-1cce16cea8.zip
vendored
Normal file
BIN
.yarn/cache/streamsearch-npm-1.1.0-fc3ad6536d-1cce16cea8.zip
vendored
Normal file
Binary file not shown.
11
package.json
11
package.json
@@ -15,22 +15,30 @@
|
||||
"lint:auth": "yarn workspace @standardnotes/auth-server lint",
|
||||
"lint:scheduler": "yarn workspace @standardnotes/scheduler-server lint",
|
||||
"lint:syncing-server": "yarn workspace @standardnotes/syncing-server lint",
|
||||
"lint:files": "yarn workspace @standardnotes/files-server lint",
|
||||
"lint:api-gateway": "yarn workspace @standardnotes/api-gateway lint",
|
||||
"test": "yarn workspaces foreach -p -j 10 --verbose run test",
|
||||
"test:auth": "yarn workspace @standardnotes/auth-server test",
|
||||
"test:scheduler": "yarn workspace @standardnotes/scheduler-server test",
|
||||
"test:syncing-server": "yarn workspace @standardnotes/syncing-server test",
|
||||
"test:files": "yarn workspace @standardnotes/files-server test",
|
||||
"clean": "yarn workspaces foreach -p --verbose run clean",
|
||||
"setup: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",
|
||||
"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",
|
||||
"start:syncing-server": "yarn workspace @standardnotes/syncing-server start",
|
||||
"start:syncing-server-worker": "yarn workspace @standardnotes/syncing-server worker",
|
||||
"release:beta": "lerna version --conventional-prerelease --conventional-commits --yes -m \"chore(release): publish\""
|
||||
"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",
|
||||
"release:prod": "lerna version --conventional-graduate --conventional-commits --yes -m \"chore(release): publish new version\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.2",
|
||||
@@ -39,6 +47,7 @@
|
||||
"@lerna-lite/list": "^1.5.1",
|
||||
"@lerna-lite/run": "^1.5.1",
|
||||
"@types/jest": "^28.1.3",
|
||||
"@types/node": "^18.0.0",
|
||||
"@typescript-eslint/parser": "^5.29.0",
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
|
||||
33
packages/api-gateway/.env.sample
Normal file
33
packages/api-gateway/.env.sample
Normal file
@@ -0,0 +1,33 @@
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
|
||||
PORT=3000
|
||||
|
||||
SYNCING_SERVER_JS_URL=http://syncing_server_js:3000
|
||||
AUTH_SERVER_URL=http://auth:3000
|
||||
PAYMENTS_SERVER_URL=http://payments:3000
|
||||
FILES_SERVER_URL=http://files:3000
|
||||
|
||||
HTTP_CALL_TIMEOUT=60000
|
||||
|
||||
AUTH_JWT_SECRET=auth_jwt_secret
|
||||
|
||||
# (Optional) New Relic Setup
|
||||
NEW_RELIC_ENABLED=false
|
||||
NEW_RELIC_APP_NAME=API Gateway
|
||||
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
|
||||
|
||||
REDIS_URL=redis://cache
|
||||
REDIS_EVENTS_CHANNEL=events
|
||||
|
||||
# (Optional) SNS Setup
|
||||
SNS_TOPIC_ARN=
|
||||
SNS_AWS_REGION=
|
||||
|
||||
# (Optional) Caching Cross Service Tokens
|
||||
CROSS_SERVICE_TOKEN_CACHE_TTL=
|
||||
2
packages/api-gateway/.eslintignore
Normal file
2
packages/api-gateway/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
test-setup.ts
|
||||
6
packages/api-gateway/.eslintrc
Normal file
6
packages/api-gateway/.eslintrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../.eslintrc",
|
||||
"parserOptions": {
|
||||
"project": "./linter.tsconfig.json"
|
||||
}
|
||||
}
|
||||
40
packages/api-gateway/CHANGELOG.md
Normal file
40
packages/api-gateway/CHANGELOG.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 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.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.1.1...@standardnotes/api-gateway@1.1.2) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.1.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.1.0...@standardnotes/api-gateway@1.1.1) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.1.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.1.0-alpha.4...@standardnotes/api-gateway@1.1.0) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.1.0-alpha.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.1.0-alpha.3...@standardnotes/api-gateway@1.1.0-alpha.4) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.1.0-alpha.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.1.0-alpha.2...@standardnotes/api-gateway@1.1.0-alpha.3) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.1.0-alpha.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.1.0-alpha.1...@standardnotes/api-gateway@1.1.0-alpha.2) (2022-06-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing curl to docker image for healthcheck purposes ([7efb48d](https://github.com/standardnotes/api-gateway/commit/7efb48dd2a6066c29601d34bfcbfe6231f644c50))
|
||||
|
||||
# [1.1.0-alpha.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.1.0-alpha.0...@standardnotes/api-gateway@1.1.0-alpha.1) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# 1.1.0-alpha.0 (2022-06-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add api-gateway package ([57c3b9c](https://github.com/standardnotes/api-gateway/commit/57c3b9c29e5b16449c864e59dbc1fd11689125f9))
|
||||
25
packages/api-gateway/Dockerfile
Normal file
25
packages/api-gateway/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM node:16.15.1-alpine AS builder
|
||||
|
||||
# Install dependencies for building native libraries
|
||||
RUN apk add --update git openssh-client curl 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
|
||||
|
||||
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/api-gateway/docker/entrypoint.sh" ]
|
||||
|
||||
CMD [ "start-web" ]
|
||||
75
packages/api-gateway/bin/report.ts
Normal file
75
packages/api-gateway/bin/report.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
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 { DomainEventPublisherInterface, DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events'
|
||||
import { AnalyticsActivity, AnalyticsStoreInterface, Period, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
|
||||
const requestReport = async (
|
||||
analyticsStore: AnalyticsStoreInterface,
|
||||
statisticsStore: StatisticsStoreInterface,
|
||||
domainEventPublisher: DomainEventPublisherInterface,
|
||||
): Promise<void> => {
|
||||
const event: DailyAnalyticsReportGeneratedEvent = {
|
||||
type: 'DAILY_ANALYTICS_REPORT_GENERATED',
|
||||
createdAt: new Date(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
applicationStatistics: await statisticsStore.getYesterdayApplicationUsage(),
|
||||
snjsStatistics: await statisticsStore.getYesterdaySNJSUsage(),
|
||||
outOfSyncIncidents: await statisticsStore.getYesterdayOutOfSyncIncidents(),
|
||||
activityStatistics: [
|
||||
{
|
||||
name: AnalyticsActivity.EditingItems,
|
||||
retention: await analyticsStore.calculateActivityRetention(
|
||||
AnalyticsActivity.EditingItems,
|
||||
Period.DayBeforeYesterday,
|
||||
Period.Yesterday,
|
||||
),
|
||||
totalCount: await analyticsStore.calculateActivityTotalCount(
|
||||
AnalyticsActivity.EditingItems,
|
||||
Period.Yesterday,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
await domainEventPublisher.publish(event)
|
||||
}
|
||||
|
||||
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 usage report generation...')
|
||||
|
||||
const analyticsStore: AnalyticsStoreInterface = container.get(TYPES.AnalyticsStore)
|
||||
const statisticsStore: StatisticsStoreInterface = container.get(TYPES.StatisticsStore)
|
||||
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
|
||||
|
||||
Promise.resolve(requestReport(analyticsStore, statisticsStore, domainEventPublisher))
|
||||
.then(() => {
|
||||
logger.info('Usage report generation complete')
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`Could not finish usage report generation: ${error.message}`)
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
113
packages/api-gateway/bin/server.ts
Normal file
113
packages/api-gateway/bin/server.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import 'newrelic'
|
||||
|
||||
import * as Sentry from '@sentry/node'
|
||||
|
||||
import '../src/Controller/LegacyController'
|
||||
import '../src/Controller/HealthCheckController'
|
||||
|
||||
import '../src/Controller/v1/SessionsController'
|
||||
import '../src/Controller/v1/UsersController'
|
||||
import '../src/Controller/v1/ActionsController'
|
||||
import '../src/Controller/v1/InvoicesController'
|
||||
import '../src/Controller/v1/RevisionsController'
|
||||
import '../src/Controller/v1/ItemsController'
|
||||
import '../src/Controller/v1/PaymentsController'
|
||||
import '../src/Controller/v1/WebSocketsController'
|
||||
import '../src/Controller/v1/TokensController'
|
||||
import '../src/Controller/v1/OfflineController'
|
||||
import '../src/Controller/v1/FilesController'
|
||||
import '../src/Controller/v1/SubscriptionInvitesController'
|
||||
|
||||
import '../src/Controller/v2/PaymentsControllerV2'
|
||||
import '../src/Controller/v2/ActionsControllerV2'
|
||||
|
||||
import * as helmet from 'helmet'
|
||||
import * as cors from 'cors'
|
||||
import { text, 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-API-Gateway-Version', container.get(TYPES.VERSION))
|
||||
next()
|
||||
})
|
||||
/* eslint-disable */
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["https: 'self'"],
|
||||
baseUri: ["'self'"],
|
||||
childSrc: ["*", "blob:"],
|
||||
connectSrc: ["*"],
|
||||
fontSrc: ["*", "'self'"],
|
||||
formAction: ["'self'"],
|
||||
frameAncestors: ["*", "*.standardnotes.org", "*.standardnotes.com"],
|
||||
frameSrc: ["*", "blob:"],
|
||||
imgSrc: ["'self'", "*", "data:"],
|
||||
manifestSrc: ["'self'"],
|
||||
mediaSrc: ["'self'"],
|
||||
objectSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'"]
|
||||
}
|
||||
}
|
||||
}))
|
||||
/* eslint-enable */
|
||||
app.use(json({ limit: '50mb' }))
|
||||
app.use(
|
||||
text({
|
||||
type: ['text/plain', 'application/x-www-form-urlencoded', 'application/x-www-form-urlencoded; charset=utf-8'],
|
||||
}),
|
||||
)
|
||||
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}`)
|
||||
})
|
||||
29
packages/api-gateway/docker/entrypoint.sh
Executable file
29
packages/api-gateway/docker/entrypoint.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
COMMAND=$1 && shift 1
|
||||
|
||||
case "$COMMAND" in
|
||||
'start-local' )
|
||||
echo "Building the project..."
|
||||
yarn workspace @standardnotes/api-gateway build
|
||||
echo "Starting Web..."
|
||||
yarn workspace @standardnotes/api-gateway start
|
||||
;;
|
||||
|
||||
'start-web' )
|
||||
echo "Starting Web..."
|
||||
yarn workspace @standardnotes/api-gateway start
|
||||
;;
|
||||
|
||||
'report' )
|
||||
echo "Starting Usage Report Generation..."
|
||||
yarn workspace @standardnotes/api-gateway report
|
||||
;;
|
||||
|
||||
* )
|
||||
echo "Unknown command"
|
||||
;;
|
||||
esac
|
||||
|
||||
exec "$@"
|
||||
18
packages/api-gateway/jest.config.js
Normal file
18
packages/api-gateway/jest.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const base = require('../../jest.config');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: 'tsconfig.json',
|
||||
},
|
||||
},
|
||||
coveragePathIgnorePatterns: [
|
||||
'/Bootstrap/',
|
||||
'HealthCheckController'
|
||||
],
|
||||
setupFilesAfterEnv: [
|
||||
'./test-setup.ts'
|
||||
]
|
||||
};
|
||||
4
packages/api-gateway/linter.tsconfig.json
Normal file
4
packages/api-gateway/linter.tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist", "test-setup.ts"]
|
||||
}
|
||||
60
packages/api-gateway/package.json
Normal file
60
packages/api-gateway/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.1.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
"description": "API Gateway For Standard Notes Services",
|
||||
"main": "dist/src/index.js",
|
||||
"typings": "dist/src/index.d.ts",
|
||||
"repository": "git@github.com:standardnotes/api-gateway.git",
|
||||
"author": "Karol Sójko <karolsojko@standardnotes.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"clean": "rm -fr dist",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "tsc --rootDir ./",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"start": "yarn node dist/bin/server.js",
|
||||
"report": "yarn node dist/bin/report.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@newrelic/native-metrics": "7.0.2",
|
||||
"@newrelic/winston-enricher": "^2.1.0",
|
||||
"@sentry/node": "^6.16.1",
|
||||
"@standardnotes/analytics": "^1.4.0",
|
||||
"@standardnotes/auth": "3.19.2",
|
||||
"@standardnotes/domain-events": "2.29.0",
|
||||
"@standardnotes/domain-events-infra": "1.4.127",
|
||||
"@standardnotes/time": "^1.7.0",
|
||||
"aws-sdk": "^2.1160.0",
|
||||
"axios": "0.24.0",
|
||||
"cors": "2.8.5",
|
||||
"dotenv": "8.2.0",
|
||||
"express": "4.17.1",
|
||||
"helmet": "4.4.1",
|
||||
"inversify": "^6.0.1",
|
||||
"inversify-express-utils": "^6.4.3",
|
||||
"ioredis": "^5.0.6",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"newrelic": "8.6.0",
|
||||
"prettyjson": "1.2.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"winston": "3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.9",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/jest": "^28.1.3",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/newrelic": "^7.0.1",
|
||||
"@types/prettyjson": "^0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^28.1.1",
|
||||
"nodemon": "^2.0.16",
|
||||
"ts-jest": "^28.0.1"
|
||||
}
|
||||
}
|
||||
117
packages/api-gateway/src/Bootstrap/Container.ts
Normal file
117
packages/api-gateway/src/Bootstrap/Container.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import * as winston from 'winston'
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import Redis from 'ioredis'
|
||||
import { Container } from 'inversify'
|
||||
import * as AWS from 'aws-sdk'
|
||||
import {
|
||||
AnalyticsStoreInterface,
|
||||
PeriodKeyGenerator,
|
||||
RedisAnalyticsStore,
|
||||
RedisStatisticsStore,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
import { RedisDomainEventPublisher, SNSDomainEventPublisher } from '@standardnotes/domain-events-infra'
|
||||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { Env } from './Env'
|
||||
import TYPES from './Types'
|
||||
import { AuthMiddleware } from '../Controller/AuthMiddleware'
|
||||
import { HttpServiceInterface } from '../Service/Http/HttpServiceInterface'
|
||||
import { HttpService } from '../Service/Http/HttpService'
|
||||
import { SubscriptionTokenAuthMiddleware } from '../Controller/SubscriptionTokenAuthMiddleware'
|
||||
import { StatisticsMiddleware } from '../Controller/StatisticsMiddleware'
|
||||
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
|
||||
import { RedisCrossServiceTokenCache } from '../Infra/Redis/RedisCrossServiceTokenCache'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicWinstonEnricher = require('@newrelic/winston-enricher')
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(): Promise<Container> {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const container = new Container()
|
||||
|
||||
const winstonFormatters = [winston.format.splat(), winston.format.json()]
|
||||
if (env.get('NEW_RELIC_ENABLED', true) === 'true') {
|
||||
winstonFormatters.push(newrelicWinstonEnricher())
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
container.bind<AxiosInstance>(TYPES.HTTPClient).toConstantValue(axios.create())
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.SYNCING_SERVER_JS_URL).toConstantValue(env.get('SYNCING_SERVER_JS_URL'))
|
||||
container.bind(TYPES.AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
|
||||
container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
|
||||
container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true))
|
||||
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
|
||||
container
|
||||
.bind(TYPES.HTTP_CALL_TIMEOUT)
|
||||
.toConstantValue(env.get('HTTP_CALL_TIMEOUT', true) ? +env.get('HTTP_CALL_TIMEOUT', true) : 60_000)
|
||||
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
|
||||
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.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
|
||||
container.bind(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL).toConstantValue(+env.get('CROSS_SERVICE_TOKEN_CACHE_TTL', true))
|
||||
|
||||
// Middleware
|
||||
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
|
||||
container
|
||||
.bind<SubscriptionTokenAuthMiddleware>(TYPES.SubscriptionTokenAuthMiddleware)
|
||||
.to(SubscriptionTokenAuthMiddleware)
|
||||
container.bind<StatisticsMiddleware>(TYPES.StatisticsMiddleware).to(StatisticsMiddleware)
|
||||
|
||||
// Services
|
||||
container.bind<HttpServiceInterface>(TYPES.HTTPService).to(HttpService)
|
||||
const periodKeyGenerator = new PeriodKeyGenerator()
|
||||
container
|
||||
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
|
||||
.toConstantValue(new RedisAnalyticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
|
||||
container
|
||||
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
|
||||
.toConstantValue(new RedisStatisticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
|
||||
container.bind<CrossServiceTokenCacheInterface>(TYPES.CrossServiceTokenCache).to(RedisCrossServiceTokenCache)
|
||||
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
||||
|
||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||
container
|
||||
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
|
||||
} else {
|
||||
container
|
||||
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||
.toConstantValue(
|
||||
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
|
||||
)
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
}
|
||||
24
packages/api-gateway/src/Bootstrap/Env.ts
Normal file
24
packages/api-gateway/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]
|
||||
}
|
||||
}
|
||||
31
packages/api-gateway/src/Bootstrap/Types.ts
Normal file
31
packages/api-gateway/src/Bootstrap/Types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
const TYPES = {
|
||||
Logger: Symbol.for('Logger'),
|
||||
Redis: Symbol.for('Redis'),
|
||||
HTTPClient: Symbol.for('HTTPClient'),
|
||||
SNS: Symbol.for('SNS'),
|
||||
// env vars
|
||||
SYNCING_SERVER_JS_URL: Symbol.for('SYNCING_SERVER_JS_URL'),
|
||||
AUTH_SERVER_URL: Symbol.for('AUTH_SERVER_URL'),
|
||||
PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
|
||||
FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'),
|
||||
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
|
||||
HTTP_CALL_TIMEOUT: Symbol.for('HTTP_CALL_TIMEOUT'),
|
||||
VERSION: Symbol.for('VERSION'),
|
||||
SNS_TOPIC_ARN: Symbol.for('SNS_TOPIC_ARN'),
|
||||
SNS_AWS_REGION: Symbol.for('SNS_AWS_REGION'),
|
||||
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
||||
CROSS_SERVICE_TOKEN_CACHE_TTL: Symbol.for('CROSS_SERVICE_TOKEN_CACHE_TTL'),
|
||||
// Middleware
|
||||
StatisticsMiddleware: Symbol.for('StatisticsMiddleware'),
|
||||
AuthMiddleware: Symbol.for('AuthMiddleware'),
|
||||
SubscriptionTokenAuthMiddleware: Symbol.for('SubscriptionTokenAuthMiddleware'),
|
||||
// Services
|
||||
HTTPService: Symbol.for('HTTPService'),
|
||||
CrossServiceTokenCache: Symbol.for('CrossServiceTokenCache'),
|
||||
AnalyticsStore: Symbol.for('AnalyticsStore'),
|
||||
StatisticsStore: Symbol.for('StatisticsStore'),
|
||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||
Timer: Symbol.for('Timer'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
124
packages/api-gateway/src/Controller/AuthMiddleware.ts
Normal file
124
packages/api-gateway/src/Controller/AuthMiddleware.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { CrossServiceTokenData } from '@standardnotes/auth'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { BaseMiddleware } from 'inversify-express-utils'
|
||||
import { verify } from 'jsonwebtoken'
|
||||
import { AxiosError, AxiosInstance } from 'axios'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
|
||||
|
||||
@injectable()
|
||||
export class AuthMiddleware extends BaseMiddleware {
|
||||
constructor(
|
||||
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
|
||||
@inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
|
||||
@inject(TYPES.AUTH_JWT_SECRET) private jwtSecret: string,
|
||||
@inject(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL) private crossServiceTokenCacheTTL: number,
|
||||
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
|
||||
const authHeaderValue = request.headers.authorization as string
|
||||
|
||||
if (!authHeaderValue) {
|
||||
response.status(401).send({
|
||||
error: {
|
||||
tag: 'invalid-auth',
|
||||
message: 'Invalid login credentials.',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
let crossServiceTokenFetchedFromCache = true
|
||||
let crossServiceToken = null
|
||||
if (this.crossServiceTokenCacheTTL) {
|
||||
crossServiceToken = await this.crossServiceTokenCache.get(authHeaderValue)
|
||||
}
|
||||
|
||||
if (crossServiceToken === null) {
|
||||
const authResponse = await this.httpClient.request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: authHeaderValue,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status >= 200 && status < 500
|
||||
},
|
||||
url: `${this.authServerUrl}/sessions/validate`,
|
||||
})
|
||||
|
||||
if (authResponse.status > 200) {
|
||||
response.setHeader('content-type', authResponse.headers['content-type'])
|
||||
response.status(authResponse.status).send(authResponse.data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
crossServiceToken = authResponse.data.authToken
|
||||
crossServiceTokenFetchedFromCache = false
|
||||
}
|
||||
|
||||
response.locals.authToken = crossServiceToken
|
||||
|
||||
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
|
||||
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
|
||||
await this.crossServiceTokenCache.set({
|
||||
authorizationHeaderValue: authHeaderValue,
|
||||
encodedCrossServiceToken: crossServiceToken,
|
||||
expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
|
||||
userUuid: decodedToken.user.uuid,
|
||||
})
|
||||
}
|
||||
|
||||
response.locals.userUuid = decodedToken.user.uuid
|
||||
response.locals.roles = decodedToken.roles
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error).message
|
||||
|
||||
this.logger.error(
|
||||
`Could not pass the request to ${this.authServerUrl}/sessions/validate on underlying service: ${errorMessage}`,
|
||||
)
|
||||
|
||||
this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
|
||||
|
||||
if ((error as AxiosError).response?.headers['content-type']) {
|
||||
response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
|
||||
}
|
||||
|
||||
const errorCode = (error as AxiosError).isAxiosError ? +((error as AxiosError).code as string) : 500
|
||||
|
||||
response.status(errorCode).send(errorMessage)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
private getCrossServiceTokenCacheExpireTimestamp(token: CrossServiceTokenData): number {
|
||||
const crossServiceTokenDefaultCacheExpiration = this.timer.getTimestampInSeconds() + this.crossServiceTokenCacheTTL
|
||||
|
||||
if (token.session === undefined) {
|
||||
return crossServiceTokenDefaultCacheExpiration
|
||||
}
|
||||
|
||||
const sessionAccessExpiration = this.timer.convertStringDateToSeconds(token.session.access_expiration)
|
||||
const sessionRefreshExpiration = this.timer.convertStringDateToSeconds(token.session.refresh_expiration)
|
||||
|
||||
return Math.min(crossServiceTokenDefaultCacheExpiration, sessionAccessExpiration, sessionRefreshExpiration)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { controller, httpGet } from 'inversify-express-utils'
|
||||
|
||||
@controller('/healthcheck')
|
||||
export class HealthCheckController {
|
||||
@httpGet('/')
|
||||
public async get(): Promise<string> {
|
||||
return 'OK'
|
||||
}
|
||||
}
|
||||
124
packages/api-gateway/src/Controller/LegacyController.ts
Normal file
124
packages/api-gateway/src/Controller/LegacyController.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { controller, all, BaseHttpController, httpPost, httpGet, results, httpDelete } from 'inversify-express-utils'
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('', TYPES.StatisticsMiddleware)
|
||||
export class LegacyController extends BaseHttpController {
|
||||
private AUTH_ROUTES: Map<string, string>
|
||||
private PARAMETRIZED_AUTH_ROUTES: Map<string, string>
|
||||
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
this.AUTH_ROUTES = new Map([
|
||||
['POST:/auth', 'POST:auth'],
|
||||
['POST:/auth/sign_out', 'POST:auth/sign_out'],
|
||||
['POST:/auth/change_pw', 'PUT:/users/legacy-endpoint-user/attributes/credentials'],
|
||||
['GET:/sessions', 'GET:sessions'],
|
||||
['DELETE:/session', 'DELETE:session'],
|
||||
['DELETE:/session/all', 'DELETE:session/all'],
|
||||
['POST:/session/refresh', 'POST:session/refresh'],
|
||||
['POST:/auth/sign_in', 'POST:auth/sign_in'],
|
||||
['GET:/auth/params', 'GET:auth/params'],
|
||||
])
|
||||
|
||||
this.PARAMETRIZED_AUTH_ROUTES = new Map([
|
||||
['PATCH:/users/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})', 'users/{uuid}'],
|
||||
])
|
||||
}
|
||||
|
||||
@httpPost('/items/sync', TYPES.AuthMiddleware)
|
||||
async legacyItemsSync(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
|
||||
}
|
||||
|
||||
@httpGet('/items/:item_id/revisions', TYPES.AuthMiddleware)
|
||||
async legacyGetRevisions(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
|
||||
}
|
||||
|
||||
@httpGet('/items/:item_id/revisions/:id', TYPES.AuthMiddleware)
|
||||
async legacyGetRevision(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
|
||||
}
|
||||
|
||||
@httpGet('/items/mfa/:userUuid')
|
||||
async blockedMFARequest(): Promise<results.StatusCodeResult> {
|
||||
return this.statusCode(401)
|
||||
}
|
||||
|
||||
@httpDelete('/items/mfa/:userUuid')
|
||||
async blockedMFARemoveRequest(): Promise<results.StatusCodeResult> {
|
||||
return this.statusCode(401)
|
||||
}
|
||||
|
||||
@all('*')
|
||||
async legacyProxyToSyncingServer(request: Request, response: Response): Promise<void> {
|
||||
if (request.path === '/') {
|
||||
response.send('Welcome to the Standard Notes server infrastructure. Learn more at https://docs.standardnotes.com')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.shouldBeRedirectedToAuthService(request)) {
|
||||
const methodAndPath = this.getMethodAndPath(request)
|
||||
|
||||
request.method = methodAndPath.method
|
||||
await this.httpService.callAuthServerWithLegacyFormat(request, response, methodAndPath.path, request.body)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
|
||||
}
|
||||
|
||||
private getMethodAndPath(request: Request): { method: string; path: string } {
|
||||
const requestKey = `${request.method}:${request.path}`
|
||||
|
||||
if (this.AUTH_ROUTES.has(requestKey)) {
|
||||
const legacyRoute = this.AUTH_ROUTES.get(requestKey) as string
|
||||
const legacyRouteMethodAndPath = legacyRoute.split(':')
|
||||
|
||||
return {
|
||||
method: legacyRouteMethodAndPath[0],
|
||||
path: legacyRouteMethodAndPath[1],
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of this.AUTH_ROUTES.keys()) {
|
||||
const regExp = new RegExp(key)
|
||||
const matches = regExp.exec(requestKey)
|
||||
if (matches !== null) {
|
||||
const legacyRoute = (this.AUTH_ROUTES.get(key) as string).replace('{uuid}', matches[1])
|
||||
const legacyRouteMethodAndPath = legacyRoute.split(':')
|
||||
|
||||
return {
|
||||
method: legacyRouteMethodAndPath[0],
|
||||
path: legacyRouteMethodAndPath[1],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw Error('could not find path for key')
|
||||
}
|
||||
|
||||
private shouldBeRedirectedToAuthService(request: Request): boolean {
|
||||
const requestKey = `${request.method}:${request.path}`
|
||||
|
||||
if (this.AUTH_ROUTES.has(requestKey)) {
|
||||
return true
|
||||
}
|
||||
|
||||
for (const key of this.PARAMETRIZED_AUTH_ROUTES.keys()) {
|
||||
const regExp = new RegExp(key)
|
||||
const matches = regExp.test(requestKey)
|
||||
if (matches) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
31
packages/api-gateway/src/Controller/StatisticsMiddleware.ts
Normal file
31
packages/api-gateway/src/Controller/StatisticsMiddleware.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { BaseMiddleware } from 'inversify-express-utils'
|
||||
import { Logger } from 'winston'
|
||||
import { StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
|
||||
@injectable()
|
||||
export class StatisticsMiddleware extends BaseMiddleware {
|
||||
constructor(
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handler(request: Request, _response: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const snjsVersion = request.headers['x-snjs-version'] ?? 'unknown'
|
||||
await this.statisticsStore.incrementSNJSVersionUsage(snjsVersion as string)
|
||||
|
||||
const applicationVersion = request.headers['x-application-version'] ?? 'unknown'
|
||||
await this.statisticsStore.incrementApplicationVersionUsage(applicationVersion as string)
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not store analytics data: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { OfflineUserTokenData, CrossServiceTokenData } from '@standardnotes/auth'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { BaseMiddleware } from 'inversify-express-utils'
|
||||
import { verify } from 'jsonwebtoken'
|
||||
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { TokenAuthenticationMethod } from './TokenAuthenticationMethod'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
|
||||
constructor(
|
||||
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
|
||||
@inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
|
||||
@inject(TYPES.AUTH_JWT_SECRET) private jwtSecret: string,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
|
||||
const subscriptionToken = request.query.subscription_token
|
||||
const email = request.headers['x-offline-email']
|
||||
if (!subscriptionToken) {
|
||||
response.status(401).send({
|
||||
error: {
|
||||
tag: 'invalid-auth',
|
||||
message: 'Invalid login credentials.',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response.locals.tokenAuthenticationMethod = email
|
||||
? TokenAuthenticationMethod.OfflineSubscriptionToken
|
||||
: TokenAuthenticationMethod.SubscriptionToken
|
||||
|
||||
try {
|
||||
const url =
|
||||
response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken
|
||||
? `${this.authServerUrl}/offline/subscription-tokens/${subscriptionToken}/validate`
|
||||
: `${this.authServerUrl}/subscription-tokens/${subscriptionToken}/validate`
|
||||
|
||||
const authResponse = await this.httpClient.request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
data: {
|
||||
email,
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status >= 200 && status < 500
|
||||
},
|
||||
url,
|
||||
})
|
||||
|
||||
if (authResponse.status > 200) {
|
||||
response.setHeader('content-type', authResponse.headers['content-type'])
|
||||
response.status(authResponse.status).send(authResponse.data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken) {
|
||||
this.handleOfflineAuthTokenValidationResponse(response, authResponse)
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
this.handleAuthTokenValidationResponse(response, authResponse)
|
||||
|
||||
return next()
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error).message
|
||||
|
||||
this.logger.error(
|
||||
`Could not pass the request to ${this.authServerUrl}/subscription-tokens/${subscriptionToken}/validate on underlying service: ${errorMessage}`,
|
||||
)
|
||||
|
||||
this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
|
||||
|
||||
if ((error as AxiosError).response?.headers['content-type']) {
|
||||
response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
|
||||
}
|
||||
|
||||
const errorCode = (error as AxiosError).isAxiosError ? +((error as AxiosError).code as string) : 500
|
||||
|
||||
response.status(errorCode).send(errorMessage)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private handleOfflineAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
|
||||
response.locals.offlineAuthToken = authResponse.data.authToken
|
||||
|
||||
const decodedToken = <OfflineUserTokenData>(
|
||||
verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
)
|
||||
|
||||
response.locals.offlineUserEmail = decodedToken.userEmail
|
||||
response.locals.offlineFeaturesToken = decodedToken.featuresToken
|
||||
}
|
||||
|
||||
private handleAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
|
||||
response.locals.authToken = authResponse.data.authToken
|
||||
|
||||
const decodedToken = <CrossServiceTokenData>(
|
||||
verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
)
|
||||
|
||||
response.locals.userUuid = decodedToken.user.uuid
|
||||
response.locals.roles = decodedToken.roles
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum TokenAuthenticationMethod {
|
||||
OfflineSubscriptionToken = 'OfflineSubscriptionToken',
|
||||
SubscriptionToken = 'SubscriptionToken',
|
||||
}
|
||||
52
packages/api-gateway/src/Controller/v1/ActionsController.ts
Normal file
52
packages/api-gateway/src/Controller/v1/ActionsController.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-express-utils'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1', TYPES.StatisticsMiddleware)
|
||||
export class ActionsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/login')
|
||||
async login(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'auth/sign_in', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/login-params')
|
||||
async loginParams(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'auth/params', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/logout')
|
||||
async logout(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'auth/sign_out', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/auth/methods')
|
||||
async methods(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'auth/methods', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/failed-backups-emails/mute/:settingUuid')
|
||||
async muteFailedBackupsEmails(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`internal/settings/email_backup/${request.params.settingUuid}/mute`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/sign-in-emails/mute/:settingUuid')
|
||||
async muteSignInEmails(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`internal/settings/sign_in/${request.params.settingUuid}/mute`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
18
packages/api-gateway/src/Controller/v1/FilesController.ts
Normal file
18
packages/api-gateway/src/Controller/v1/FilesController.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, httpPost } from 'inversify-express-utils'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/files', TYPES.StatisticsMiddleware)
|
||||
export class FilesController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/valet-tokens', TYPES.AuthMiddleware)
|
||||
async createToken(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'valet-tokens', request.body)
|
||||
}
|
||||
}
|
||||
17
packages/api-gateway/src/Controller/v1/InvoicesController.ts
Normal file
17
packages/api-gateway/src/Controller/v1/InvoicesController.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { BaseHttpController, controller, httpPost } from 'inversify-express-utils'
|
||||
import { inject } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1', TYPES.StatisticsMiddleware)
|
||||
export class InvoicesController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/invoices/send-latest', TYPES.SubscriptionTokenAuthMiddleware)
|
||||
async sendLatestInvoice(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/send-invoice', request.body)
|
||||
}
|
||||
}
|
||||
27
packages/api-gateway/src/Controller/v1/ItemsController.ts
Normal file
27
packages/api-gateway/src/Controller/v1/ItemsController.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-express-utils'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/items', TYPES.StatisticsMiddleware, TYPES.AuthMiddleware)
|
||||
export class ItemsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/')
|
||||
async sync(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callSyncingServer(request, response, 'items/sync', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/check-integrity')
|
||||
async checkIntegrity(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callSyncingServer(request, response, 'items/check-integrity', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/:uuid')
|
||||
async getItem(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callSyncingServer(request, response, `items/${request.params.uuid}`, request.body)
|
||||
}
|
||||
}
|
||||
33
packages/api-gateway/src/Controller/v1/OfflineController.ts
Normal file
33
packages/api-gateway/src/Controller/v1/OfflineController.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-express-utils'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/offline', TYPES.StatisticsMiddleware)
|
||||
export class OfflineController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpGet('/features')
|
||||
async getOfflineFeatures(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'offline/features', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/subscription-tokens')
|
||||
async createOfflineSubscriptionToken(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'offline/subscription-tokens', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/payments/stripe-setup-intent')
|
||||
async createStripeSetupIntent(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(
|
||||
request,
|
||||
response,
|
||||
'api/pro_users/stripe-setup-intent/offline',
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
162
packages/api-gateway/src/Controller/v1/PaymentsController.ts
Normal file
162
packages/api-gateway/src/Controller/v1/PaymentsController.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { all, BaseHttpController, controller, httpDelete, httpGet, httpPost } from 'inversify-express-utils'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1', TYPES.StatisticsMiddleware)
|
||||
export class PaymentsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpGet('/downloads')
|
||||
async downloads(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/downloads', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/downloads/download-info')
|
||||
async downloadInfo(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/downloads/download-info', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/downloads/platforms')
|
||||
async platformDownloads(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/downloads/platforms', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/help/categories')
|
||||
async categoriesHelp(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/help/categories', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/knowledge/categories')
|
||||
async categoriesKnowledge(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/knowledge/categories', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/extensions')
|
||||
async extensions(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/extensions', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/subscriptions/tiered', TYPES.SubscriptionTokenAuthMiddleware)
|
||||
async createTieredSubscription(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/tiered', request.body)
|
||||
}
|
||||
|
||||
@all('/subscriptions(/*)?')
|
||||
async subscriptions(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, request.path.replace('v1', 'api'), request.body)
|
||||
}
|
||||
|
||||
@httpGet('/reset/validate')
|
||||
async validateReset(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/reset/validate', request.body)
|
||||
}
|
||||
|
||||
@httpDelete('/reset')
|
||||
async reset(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/reset', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/reset')
|
||||
async resetRequest(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/reset', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/user-registration')
|
||||
async userRegistration(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'admin/events/registration', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/admin/graphql')
|
||||
async adminGraphql(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'admin/graphql', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/admin/auth/login')
|
||||
async adminLogin(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'admin/auth/login', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/admin/auth/logout')
|
||||
async adminLogout(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'admin/auth/logout', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/students')
|
||||
async students(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/students', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/students/:token/approve')
|
||||
async studentsApprove(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(
|
||||
request,
|
||||
response,
|
||||
`api/students/${request.params.token}/approve`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPost('/email_subscriptions/:token/less')
|
||||
async subscriptionsLess(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(
|
||||
request,
|
||||
response,
|
||||
`api/email_subscriptions/${request.params.token}/less`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPost('/email_subscriptions/:token/more')
|
||||
async subscriptionsMore(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(
|
||||
request,
|
||||
response,
|
||||
`api/email_subscriptions/${request.params.token}/more`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPost('/email_subscriptions/:token/mute/:campaignId')
|
||||
async subscriptionsMute(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(
|
||||
request,
|
||||
response,
|
||||
`api/email_subscriptions/${request.params.token}/mute/${request.params.campaignId}`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPost('/email_subscriptions/:token/unsubscribe')
|
||||
async subscriptionsUnsubscribe(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(
|
||||
request,
|
||||
response,
|
||||
`api/email_subscriptions/${request.params.token}/unsubscribe`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPost('/payments/stripe-setup-intent', TYPES.SubscriptionTokenAuthMiddleware)
|
||||
async createStripeSetupIntent(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/stripe-setup-intent', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/pro_users/cp-prepayment-info', TYPES.SubscriptionTokenAuthMiddleware)
|
||||
async coinpaymentsPrepaymentInfo(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/cp-prepayment-info', request.body)
|
||||
}
|
||||
|
||||
@all('/pro_users(/*)?')
|
||||
async proUsers(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, request.path.replace('v1', 'api'), request.body)
|
||||
}
|
||||
|
||||
@all('/refunds')
|
||||
async refunds(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/refunds', request.body)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, httpDelete, httpGet } from 'inversify-express-utils'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/items/:item_id/revisions', TYPES.StatisticsMiddleware, TYPES.AuthMiddleware)
|
||||
export class RevisionsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpGet('/')
|
||||
async getRevisions(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callSyncingServer(request, response, `items/${request.params.item_id}/revisions`)
|
||||
}
|
||||
|
||||
@httpGet('/:id')
|
||||
async getRevision(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callSyncingServer(
|
||||
request,
|
||||
response,
|
||||
`items/${request.params.item_id}/revisions/${request.params.id}`,
|
||||
)
|
||||
}
|
||||
|
||||
@httpDelete('/:id')
|
||||
async deleteRevision(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callSyncingServer(
|
||||
request,
|
||||
response,
|
||||
`items/${request.params.item_id}/revisions/${request.params.id}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
34
packages/api-gateway/src/Controller/v1/SessionsController.ts
Normal file
34
packages/api-gateway/src/Controller/v1/SessionsController.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, httpDelete, httpGet, httpPost } from 'inversify-express-utils'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/sessions', TYPES.StatisticsMiddleware)
|
||||
export class SessionsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpGet('/', TYPES.AuthMiddleware)
|
||||
async getSessions(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'sessions')
|
||||
}
|
||||
|
||||
@httpDelete('/:uuid', TYPES.AuthMiddleware)
|
||||
async deleteSession(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'session', {
|
||||
uuid: request.params.uuid,
|
||||
})
|
||||
}
|
||||
|
||||
@httpDelete('/', TYPES.AuthMiddleware)
|
||||
async deleteSessions(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'session/all')
|
||||
}
|
||||
|
||||
@httpPost('/refresh')
|
||||
async refreshSession(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'session/refresh', request.body)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, httpDelete, httpGet, httpPost } from 'inversify-express-utils'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/subscription-invites', TYPES.StatisticsMiddleware)
|
||||
export class SubscriptionInvitesController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/', TYPES.AuthMiddleware)
|
||||
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'subscription-invites', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/', TYPES.AuthMiddleware)
|
||||
async listInvites(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'subscription-invites', request.body)
|
||||
}
|
||||
|
||||
@httpDelete('/:inviteUuid', TYPES.AuthMiddleware)
|
||||
async cancelSubscriptionSharing(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, `subscription-invites/${request.params.inviteUuid}`)
|
||||
}
|
||||
|
||||
@httpGet('/:inviteUuid/accept')
|
||||
async acceptInvite(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, `subscription-invites/${request.params.inviteUuid}/accept`)
|
||||
}
|
||||
|
||||
@httpGet('/:inviteUuid/decline')
|
||||
async declineInvite(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`subscription-invites/${request.params.inviteUuid}/decline`,
|
||||
)
|
||||
}
|
||||
}
|
||||
18
packages/api-gateway/src/Controller/v1/TokensController.ts
Normal file
18
packages/api-gateway/src/Controller/v1/TokensController.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, httpPost } from 'inversify-express-utils'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/subscription-tokens', TYPES.StatisticsMiddleware)
|
||||
export class TokensController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/', TYPES.AuthMiddleware)
|
||||
async createToken(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'subscription-tokens', request.body)
|
||||
}
|
||||
}
|
||||
150
packages/api-gateway/src/Controller/v1/UsersController.ts
Normal file
150
packages/api-gateway/src/Controller/v1/UsersController.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
all,
|
||||
BaseHttpController,
|
||||
controller,
|
||||
httpDelete,
|
||||
httpGet,
|
||||
httpPatch,
|
||||
httpPost,
|
||||
httpPut,
|
||||
results,
|
||||
} from 'inversify-express-utils'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
import { TokenAuthenticationMethod } from '../TokenAuthenticationMethod'
|
||||
|
||||
@controller('/v1/users', TYPES.StatisticsMiddleware)
|
||||
export class UsersController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.HTTPService) private httpService: HttpServiceInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/claim-account')
|
||||
async claimAccount(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/claim-account', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/send-activation-code', TYPES.SubscriptionTokenAuthMiddleware)
|
||||
async sendActivationCode(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/pro_users/send-activation-code', request.body)
|
||||
}
|
||||
|
||||
@httpPatch('/:userId', TYPES.AuthMiddleware)
|
||||
async updateUser(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, `users/${request.params.userId}`, request.body)
|
||||
}
|
||||
|
||||
@httpPut('/:userUuid/password', TYPES.AuthMiddleware)
|
||||
async changePassword(request: Request, response: Response): Promise<void> {
|
||||
this.logger.debug(
|
||||
'[DEPRECATED] use endpoint /v1/users/:userUuid/attributes/credentials instead of /v1/users/:userUuid/password',
|
||||
)
|
||||
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`users/${request.params.userUuid}/attributes/credentials`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPut('/:userUuid/attributes/credentials', TYPES.AuthMiddleware)
|
||||
async changeCredentials(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`users/${request.params.userUuid}/attributes/credentials`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/:userId/params', TYPES.AuthMiddleware)
|
||||
async getKeyParams(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'auth/params')
|
||||
}
|
||||
|
||||
@all('/:userId/mfa', TYPES.AuthMiddleware)
|
||||
async blockMFA(): Promise<results.StatusCodeResult> {
|
||||
return this.statusCode(401)
|
||||
}
|
||||
|
||||
@httpPost('/:userUuid/integrations/listed', TYPES.AuthMiddleware)
|
||||
async createListedAccount(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'listed', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/')
|
||||
async register(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'auth', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/:userUuid/settings', TYPES.AuthMiddleware)
|
||||
async listSettings(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, `users/${request.params.userUuid}/settings`)
|
||||
}
|
||||
|
||||
@httpPut('/:userUuid/settings', TYPES.AuthMiddleware)
|
||||
async putSetting(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, `users/${request.params.userUuid}/settings`, request.body)
|
||||
}
|
||||
|
||||
@httpGet('/:userUuid/settings/:settingName', TYPES.AuthMiddleware)
|
||||
async getSetting(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`users/${request.params.userUuid}/settings/${request.params.settingName}`,
|
||||
)
|
||||
}
|
||||
|
||||
@httpDelete('/:userUuid/settings/:settingName', TYPES.AuthMiddleware)
|
||||
async deleteSetting(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`users/${request.params.userUuid}/settings/${request.params.settingName}`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/:userUuid/subscription-settings/:subscriptionSettingName', TYPES.AuthMiddleware)
|
||||
async getSubscriptionSetting(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`users/${request.params.userUuid}/subscription-settings/${request.params.subscriptionSettingName}`,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/:userUuid/features', TYPES.AuthMiddleware)
|
||||
async getFeatures(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, `users/${request.params.userUuid}/features`)
|
||||
}
|
||||
|
||||
@httpGet('/:userUuid/subscription', TYPES.AuthMiddleware)
|
||||
async getSubscription(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, `users/${request.params.userUuid}/subscription`)
|
||||
}
|
||||
|
||||
@httpGet('/subscription', TYPES.SubscriptionTokenAuthMiddleware)
|
||||
async getSubscriptionBySubscriptionToken(request: Request, response: Response): Promise<void> {
|
||||
if (response.locals.tokenAuthenticationMethod === TokenAuthenticationMethod.OfflineSubscriptionToken) {
|
||||
await this.httpService.callAuthServer(request, response, 'offline/users/subscription')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.httpService.callAuthServer(request, response, `users/${response.locals.userUuid}/subscription`)
|
||||
}
|
||||
|
||||
@httpDelete('/:userUuid', TYPES.AuthMiddleware)
|
||||
async deleteUser(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/account', request.body)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, httpDelete, httpPost } from 'inversify-express-utils'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/sockets')
|
||||
export class WebSocketsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/', TYPES.AuthMiddleware)
|
||||
async createWebSocketConnection(request: Request, response: Response): Promise<void> {
|
||||
if (!request.headers.connectionid) {
|
||||
response.status(400).send('Missing connection id in the request')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.httpService.callAuthServer(request, response, `sockets/${request.headers.connectionid}`, request.body)
|
||||
}
|
||||
|
||||
@httpDelete('/')
|
||||
async deleteWebSocketConnection(request: Request, response: Response): Promise<void> {
|
||||
if (!request.headers.connectionid) {
|
||||
response.status(400).send('Missing connection id in the request')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.httpService.callAuthServer(request, response, `sockets/${request.headers.connectionid}`, request.body)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { BaseHttpController, controller, httpPost } from 'inversify-express-utils'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v2', TYPES.StatisticsMiddleware)
|
||||
export class ActionsControllerV2 extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/login')
|
||||
async login(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'auth/pkce_sign_in', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/login-params')
|
||||
async loginParams(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'auth/pkce_params', request.body)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { BaseHttpController, controller, httpDelete, httpGet, httpPatch, httpPost } from 'inversify-express-utils'
|
||||
import { inject } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v2', TYPES.StatisticsMiddleware)
|
||||
export class PaymentsControllerV2 extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpGet('/subscriptions')
|
||||
async getSubscriptionsWithFeatures(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/features', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/subscriptions/tailored', TYPES.SubscriptionTokenAuthMiddleware)
|
||||
async getTailoredSubscriptionsWithFeatures(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/features', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/subscriptions/deltas', TYPES.SubscriptionTokenAuthMiddleware)
|
||||
async getSubscriptionDeltasForChangingPlan(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/deltas', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/subscriptions/deltas/apply', TYPES.SubscriptionTokenAuthMiddleware)
|
||||
async applySubscriptionDelta(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/deltas/apply', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/subscriptions/:subscriptionId', TYPES.SubscriptionTokenAuthMiddleware)
|
||||
async getSubscription(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(
|
||||
request,
|
||||
response,
|
||||
`api/subscriptions/${request.params.subscriptionId}`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpDelete('/subscriptions/:subscriptionId', TYPES.SubscriptionTokenAuthMiddleware)
|
||||
async cancelSubscription(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(
|
||||
request,
|
||||
response,
|
||||
`api/subscriptions/${request.params.subscriptionId}`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPatch('/subscriptions/:subscriptionId', TYPES.SubscriptionTokenAuthMiddleware)
|
||||
async updateSubscription(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(
|
||||
request,
|
||||
response,
|
||||
`api/subscriptions/${request.params.subscriptionId}`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import * as IORedis from 'ioredis'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
|
||||
import { CrossServiceTokenCacheInterface } from '../../Service/Cache/CrossServiceTokenCacheInterface'
|
||||
|
||||
@injectable()
|
||||
export class RedisCrossServiceTokenCache implements CrossServiceTokenCacheInterface {
|
||||
private readonly PREFIX = 'cst'
|
||||
private readonly USER_CST_PREFIX = 'user-cst'
|
||||
|
||||
constructor(@inject(TYPES.Redis) private redisClient: IORedis.Redis) {}
|
||||
|
||||
async set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void> {
|
||||
const pipeline = this.redisClient.pipeline()
|
||||
|
||||
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.authorizationHeaderValue)
|
||||
pipeline.expireat(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
|
||||
|
||||
pipeline.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
|
||||
pipeline.expireat(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
|
||||
|
||||
await pipeline.exec()
|
||||
}
|
||||
|
||||
async get(authorizationHeaderValue: string): Promise<string | null> {
|
||||
return this.redisClient.get(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
}
|
||||
|
||||
async invalidate(userUuid: string): Promise<void> {
|
||||
const userAuthorizationHeaderValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
const pipeline = this.redisClient.pipeline()
|
||||
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
|
||||
pipeline.del(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
}
|
||||
pipeline.del(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
await pipeline.exec()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface CrossServiceTokenCacheInterface {
|
||||
set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void>
|
||||
get(authorizationHeaderValue: string): Promise<string | null>
|
||||
invalidate(userUuid: string): Promise<void>
|
||||
}
|
||||
231
packages/api-gateway/src/Service/Http/HttpService.ts
Normal file
231
packages/api-gateway/src/Service/Http/HttpService.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { AxiosInstance, AxiosError, AxiosResponse, Method } from 'axios'
|
||||
import { Request, Response } from 'express'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
|
||||
import { HttpServiceInterface } from './HttpServiceInterface'
|
||||
|
||||
@injectable()
|
||||
export class HttpService implements HttpServiceInterface {
|
||||
constructor(
|
||||
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
|
||||
@inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
|
||||
@inject(TYPES.SYNCING_SERVER_JS_URL) private syncingServerJsUrl: string,
|
||||
@inject(TYPES.PAYMENTS_SERVER_URL) private paymentsServerUrl: string,
|
||||
@inject(TYPES.FILES_SERVER_URL) private filesServerUrl: string,
|
||||
@inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
|
||||
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async callSyncingServer(
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void> {
|
||||
await this.callServer(this.syncingServerJsUrl, request, response, endpoint, payload)
|
||||
}
|
||||
|
||||
async callLegacySyncingServer(
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void> {
|
||||
await this.callServerWithLegacyFormat(this.syncingServerJsUrl, request, response, endpoint, payload)
|
||||
}
|
||||
|
||||
async callAuthServer(
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void> {
|
||||
await this.callServer(this.authServerUrl, request, response, endpoint, payload)
|
||||
}
|
||||
|
||||
async callPaymentsServer(
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void> {
|
||||
if (!this.paymentsServerUrl) {
|
||||
this.logger.debug('Payments Server URL not defined. Skipped request to Payments API.')
|
||||
|
||||
return
|
||||
}
|
||||
await this.callServerWithLegacyFormat(this.paymentsServerUrl, request, response, endpoint, payload)
|
||||
}
|
||||
|
||||
async callAuthServerWithLegacyFormat(
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void> {
|
||||
await this.callServerWithLegacyFormat(this.authServerUrl, request, response, endpoint, payload)
|
||||
}
|
||||
|
||||
private async getServerResponse(
|
||||
serverUrl: string,
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<AxiosResponse | undefined> {
|
||||
try {
|
||||
const headers: Record<string, string> = {}
|
||||
for (const headerName of Object.keys(request.headers)) {
|
||||
headers[headerName] = request.headers[headerName] as string
|
||||
}
|
||||
|
||||
delete headers.host
|
||||
delete headers['content-length']
|
||||
|
||||
if (response.locals.authToken) {
|
||||
headers['X-Auth-Token'] = response.locals.authToken
|
||||
}
|
||||
|
||||
if (response.locals.offlineAuthToken) {
|
||||
headers['X-Auth-Offline-Token'] = response.locals.offlineAuthToken
|
||||
}
|
||||
|
||||
this.logger.debug(`Calling [${request.method}] ${serverUrl}/${endpoint},
|
||||
headers: ${JSON.stringify(headers)},
|
||||
query: ${JSON.stringify(request.query)},
|
||||
payload: ${JSON.stringify(payload)}`)
|
||||
|
||||
const serviceResponse = await this.httpClient.request({
|
||||
method: request.method as Method,
|
||||
headers,
|
||||
url: `${serverUrl}/${endpoint}`,
|
||||
data: this.getRequestData(payload),
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
params: request.query,
|
||||
timeout: this.httpCallTimeout,
|
||||
validateStatus: (status: number) => {
|
||||
return status >= 200 && status < 500
|
||||
},
|
||||
})
|
||||
|
||||
if (serviceResponse.headers['x-invalidate-cache']) {
|
||||
const userUuid = serviceResponse.headers['x-invalidate-cache']
|
||||
await this.crossServiceTokenCache.invalidate(userUuid)
|
||||
}
|
||||
|
||||
return serviceResponse
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error).message
|
||||
|
||||
this.logger.error(`Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${errorMessage}`)
|
||||
|
||||
this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
|
||||
|
||||
if ((error as AxiosError).response?.headers['content-type']) {
|
||||
response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
|
||||
}
|
||||
|
||||
const errorCode = (error as AxiosError).isAxiosError ? +((error as AxiosError).code as string) : 500
|
||||
|
||||
response.status(errorCode).send(errorMessage)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
private async callServer(
|
||||
serverUrl: string,
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void> {
|
||||
const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload)
|
||||
|
||||
this.logger.debug(`Response from underlying server: ${JSON.stringify(serviceResponse?.data)},
|
||||
headers: ${JSON.stringify(serviceResponse?.headers)}`)
|
||||
|
||||
if (!serviceResponse) {
|
||||
return
|
||||
}
|
||||
|
||||
this.applyResponseHeaders(serviceResponse, response)
|
||||
|
||||
response.status(serviceResponse.status).send({
|
||||
meta: {
|
||||
auth: {
|
||||
userUuid: response.locals.userUuid,
|
||||
roles: response.locals.roles,
|
||||
},
|
||||
server: {
|
||||
filesServerUrl: this.filesServerUrl,
|
||||
},
|
||||
},
|
||||
data: serviceResponse.data,
|
||||
})
|
||||
}
|
||||
|
||||
private async callServerWithLegacyFormat(
|
||||
serverUrl: string,
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void> {
|
||||
const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload)
|
||||
|
||||
if (!serviceResponse) {
|
||||
return
|
||||
}
|
||||
|
||||
this.applyResponseHeaders(serviceResponse, response)
|
||||
|
||||
if (serviceResponse.request._redirectable._redirectCount > 0) {
|
||||
response.status(302).redirect(serviceResponse.request.res.responseUrl)
|
||||
} else {
|
||||
response.status(serviceResponse.status).send(serviceResponse.data)
|
||||
}
|
||||
}
|
||||
|
||||
private getRequestData(
|
||||
payload: Record<string, unknown> | string | undefined,
|
||||
): Record<string, unknown> | string | undefined {
|
||||
if (
|
||||
payload === '' ||
|
||||
payload === null ||
|
||||
payload === undefined ||
|
||||
(typeof payload === 'object' && Object.keys(payload).length === 0)
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
private applyResponseHeaders(serviceResponse: AxiosResponse, response: Response): void {
|
||||
const returnedHeadersFromUnderlyingService = [
|
||||
'access-control-allow-methods',
|
||||
'access-control-allow-origin',
|
||||
'access-control-expose-headers',
|
||||
'authorization',
|
||||
'content-type',
|
||||
'x-ssjs-version',
|
||||
'x-auth-version',
|
||||
]
|
||||
|
||||
returnedHeadersFromUnderlyingService.map((headerName) => {
|
||||
const headerValue = serviceResponse.headers[headerName]
|
||||
if (headerValue) {
|
||||
response.setHeader(headerName, headerValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
export interface HttpServiceInterface {
|
||||
callAuthServer(
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
callAuthServerWithLegacyFormat(
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
callSyncingServer(
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
callLegacySyncingServer(
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
callPaymentsServer(
|
||||
request: Request,
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
}
|
||||
0
packages/api-gateway/test-setup.ts
Normal file
0
packages/api-gateway/test-setup.ts
Normal file
12
packages/api-gateway/tsconfig.json
Normal file
12
packages/api-gateway/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"bin/**/*",
|
||||
],
|
||||
"references": []
|
||||
}
|
||||
17
packages/api-gateway/wait-for.sh
Executable file
17
packages/api-gateway/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,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.2](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.1...@standardnotes/auth-server@1.1.2) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.1.1](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0...@standardnotes/auth-server@1.1.1) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.1.0](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.8...@standardnotes/auth-server@1.1.0) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.1.0-alpha.8](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.7...@standardnotes/auth-server@1.1.0-alpha.8) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.1.0-alpha.7](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.6...@standardnotes/auth-server@1.1.0-alpha.7) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.1.0-alpha.6](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.5...@standardnotes/auth-server@1.1.0-alpha.6) (2022-06-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing curl to docker image for healthcheck purposes ([7efb48d](https://github.com/standardnotes/auth/commit/7efb48dd2a6066c29601d34bfcbfe6231f644c50))
|
||||
|
||||
# [1.1.0-alpha.5](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.4...@standardnotes/auth-server@1.1.0-alpha.5) (2022-06-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make DISABLE_USER_REGISTRATION env var optional ([3110c20](https://github.com/standardnotes/auth/commit/3110c20596b52da8a43551432cfef94e68046385))
|
||||
|
||||
# [1.1.0-alpha.4](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.3...@standardnotes/auth-server@1.1.0-alpha.4) (2022-06-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.1.0-alpha.3](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.2...@standardnotes/auth-server@1.1.0-alpha.3) (2022-06-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.1.0-alpha.2](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.1...@standardnotes/auth-server@1.1.0-alpha.2) (2022-06-22)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM node:16.15.1-alpine AS builder
|
||||
|
||||
# Install dependencies for building native libraries
|
||||
RUN apk add --update git openssh-client python3 alpine-sdk
|
||||
RUN apk add --update git openssh-client curl python3 alpine-sdk
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.1.0-alpha.2",
|
||||
"version": "1.1.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -357,7 +357,9 @@ export class ContainerConfigLoader {
|
||||
container.bind(TYPES.PSEUDO_KEY_PARAMS_KEY).toConstantValue(env.get('PSEUDO_KEY_PARAMS_KEY'))
|
||||
container.bind(TYPES.EPHEMERAL_SESSION_AGE).toConstantValue(env.get('EPHEMERAL_SESSION_AGE'))
|
||||
container.bind(TYPES.REDIS_URL).toConstantValue(env.get('REDIS_URL'))
|
||||
container.bind(TYPES.DISABLE_USER_REGISTRATION).toConstantValue(env.get('DISABLE_USER_REGISTRATION') === 'true')
|
||||
container
|
||||
.bind(TYPES.DISABLE_USER_REGISTRATION)
|
||||
.toConstantValue(env.get('DISABLE_USER_REGISTRATION', true) === 'true')
|
||||
container.bind(TYPES.ANALYTICS_ENABLED).toConstantValue(env.get('ANALYTICS_ENABLED', true) === 'true')
|
||||
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))
|
||||
|
||||
35
packages/files/.env.sample
Normal file
35
packages/files/.env.sample
Normal file
@@ -0,0 +1,35 @@
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
VERSION=development
|
||||
|
||||
PORT=3000
|
||||
|
||||
REDIS_URL=redis://cache
|
||||
REDIS_EVENTS_CHANNEL=events
|
||||
|
||||
VALET_TOKEN_SECRET=change-me-!
|
||||
|
||||
MAX_CHUNK_BYTES=1000000
|
||||
|
||||
# (Optional) New Relic Setup
|
||||
NEW_RELIC_ENABLED=false
|
||||
NEW_RELIC_APP_NAME=Syncing Server JS
|
||||
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
|
||||
|
||||
# (Optional) AWS Setup
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
S3_BUCKET_NAME=
|
||||
S3_AWS_REGION=
|
||||
S3_ENDPOINT=
|
||||
SNS_TOPIC_ARN=
|
||||
SNS_AWS_REGION=
|
||||
SQS_QUEUE_URL=
|
||||
SQS_AWS_REGION=
|
||||
|
||||
# (Optional) File upload path (relative to root directory)
|
||||
FILE_UPLOAD_PATH=
|
||||
3
packages/files/.eslintignore
Normal file
3
packages/files/.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
dist
|
||||
test-setup.ts
|
||||
data
|
||||
6
packages/files/.eslintrc
Normal file
6
packages/files/.eslintrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../.eslintrc",
|
||||
"parserOptions": {
|
||||
"project": "./linter.tsconfig.json"
|
||||
}
|
||||
}
|
||||
50
packages/files/CHANGELOG.md
Normal file
50
packages/files/CHANGELOG.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 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.2](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.1.1...@standardnotes/files-server@1.1.2) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.1.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.1.0...@standardnotes/files-server@1.1.1) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
# [1.1.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.1.0-alpha.6...@standardnotes/files-server@1.1.0) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
# [1.1.0-alpha.6](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.1.0-alpha.5...@standardnotes/files-server@1.1.0-alpha.6) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
# [1.1.0-alpha.5](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.1.0-alpha.4...@standardnotes/files-server@1.1.0-alpha.5) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
# [1.1.0-alpha.4](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.1.0-alpha.3...@standardnotes/files-server@1.1.0-alpha.4) (2022-06-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing curl to docker image for healthcheck purposes ([7efb48d](https://github.com/standardnotes/files/commit/7efb48dd2a6066c29601d34bfcbfe6231f644c50))
|
||||
|
||||
# [1.1.0-alpha.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.1.0-alpha.2...@standardnotes/files-server@1.1.0-alpha.3) (2022-06-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
# [1.1.0-alpha.2](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.1.0-alpha.1...@standardnotes/files-server@1.1.0-alpha.2) (2022-06-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add api-gateway package ([57c3b9c](https://github.com/standardnotes/files/commit/57c3b9c29e5b16449c864e59dbc1fd11689125f9))
|
||||
|
||||
# [1.1.0-alpha.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.1.0-alpha.0...@standardnotes/files-server@1.1.0-alpha.1) (2022-06-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
# 1.1.0-alpha.0 (2022-06-22)
|
||||
|
||||
### Features
|
||||
|
||||
* add files server package ([7a8a5fc](https://github.com/standardnotes/files/commit/7a8a5fcfdfe0f9cad51114b43cdae748e297b543))
|
||||
25
packages/files/Dockerfile
Normal file
25
packages/files/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM node:16.15.1-alpine AS builder
|
||||
|
||||
# Install dependencies for building native libraries
|
||||
RUN apk add --update git openssh-client curl 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
|
||||
|
||||
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/files/docker/entrypoint.sh" ]
|
||||
|
||||
CMD [ "start-web" ]
|
||||
104
packages/files/bin/server.ts
Normal file
104
packages/files/bin/server.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import 'newrelic'
|
||||
|
||||
import * as Sentry from '@sentry/node'
|
||||
import * as busboy from 'connect-busboy'
|
||||
|
||||
import '../src/Controller/HealthCheckController'
|
||||
import '../src/Controller/FilesController'
|
||||
|
||||
import * as helmet from 'helmet'
|
||||
import * as cors from 'cors'
|
||||
import { urlencoded, json, raw, 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-Files-Version', container.get(TYPES.VERSION))
|
||||
next()
|
||||
})
|
||||
app.use(
|
||||
busboy({
|
||||
highWaterMark: 2 * 1024 * 1024,
|
||||
}),
|
||||
)
|
||||
/* eslint-disable */
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["https: 'self'"],
|
||||
baseUri: ["'self'"],
|
||||
childSrc: ["*", "blob:"],
|
||||
connectSrc: ["*"],
|
||||
fontSrc: ["*", "'self'"],
|
||||
formAction: ["'self'"],
|
||||
frameAncestors: ["*", "*.standardnotes.org", "*.standardnotes.com"],
|
||||
frameSrc: ["*", "blob:"],
|
||||
imgSrc: ["'self'", "*", "data:"],
|
||||
manifestSrc: ["'self'"],
|
||||
mediaSrc: ["'self'"],
|
||||
objectSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'"]
|
||||
}
|
||||
}
|
||||
}))
|
||||
/* eslint-enable */
|
||||
app.use(json({ limit: '50mb' }))
|
||||
app.use(raw({ limit: '50mb', type: 'application/octet-stream' }))
|
||||
app.use(urlencoded({ extended: true, limit: '50mb' }))
|
||||
app.use(
|
||||
cors({
|
||||
exposedHeaders: ['Content-Range', 'Accept-Ranges'],
|
||||
}),
|
||||
)
|
||||
|
||||
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}`)
|
||||
})
|
||||
29
packages/files/bin/worker.ts
Normal file
29
packages/files/bin/worker.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
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'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
dayjs.extend(utc)
|
||||
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const logger: Logger = container.get(TYPES.Logger)
|
||||
|
||||
logger.info('Starting worker...')
|
||||
|
||||
const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory)
|
||||
subscriberFactory.create().start()
|
||||
|
||||
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
|
||||
})
|
||||
27
packages/files/docker/entrypoint.sh
Executable file
27
packages/files/docker/entrypoint.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
COMMAND=$1 && shift 1
|
||||
|
||||
case "$COMMAND" in
|
||||
'start-local')
|
||||
echo "Starting Web in Local Mode..."
|
||||
yarn workspace @standardnotes/files-server start:local
|
||||
;;
|
||||
|
||||
'start-web' )
|
||||
echo "Starting Web..."
|
||||
yarn workspace @standardnotes/files-server start
|
||||
;;
|
||||
|
||||
'start-worker' )
|
||||
echo "Starting Worker..."
|
||||
yarn workspace @standardnotes/files-server worker
|
||||
;;
|
||||
|
||||
* )
|
||||
echo "Unknown command"
|
||||
;;
|
||||
esac
|
||||
|
||||
exec "$@"
|
||||
19
packages/files/jest.config.js
Normal file
19
packages/files/jest.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const base = require('../../jest.config');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: 'tsconfig.json',
|
||||
},
|
||||
},
|
||||
coveragePathIgnorePatterns: [
|
||||
'/Bootstrap/',
|
||||
'HealthCheckController',
|
||||
"/Infra/FS"
|
||||
],
|
||||
setupFilesAfterEnv: [
|
||||
'./test-setup.ts'
|
||||
]
|
||||
};
|
||||
4
packages/files/linter.tsconfig.json
Normal file
4
packages/files/linter.tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist", "test-setup.ts"]
|
||||
}
|
||||
73
packages/files/package.json
Normal file
73
packages/files/package.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.1.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
"description": "Standard Notes Files Server",
|
||||
"main": "dist/src/index.js",
|
||||
"typings": "dist/src/index.d.ts",
|
||||
"repository": "git@github.com:standardnotes/files.git",
|
||||
"authors": [
|
||||
"Karol Sójko <karol@standardnotes.com>"
|
||||
],
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"clean": "rm -fr dist",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@newrelic/native-metrics": "7.0.2",
|
||||
"@sentry/node": "^6.16.1",
|
||||
"@standardnotes/auth": "^3.18.9",
|
||||
"@standardnotes/common": "^1.19.4",
|
||||
"@standardnotes/domain-events": "^2.27.6",
|
||||
"@standardnotes/domain-events-infra": "^1.4.93",
|
||||
"@standardnotes/sncrypto-common": "^1.3.0",
|
||||
"@standardnotes/sncrypto-node": "^1.3.0",
|
||||
"@standardnotes/time": "^1.4.5",
|
||||
"aws-sdk": "^2.1158.0",
|
||||
"connect-busboy": "^1.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-winston": "^4.0.5",
|
||||
"helmet": "^4.3.1",
|
||||
"inversify": "^6.0.1",
|
||||
"inversify-express-utils": "^6.4.3",
|
||||
"ioredis": "^5.0.6",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"newrelic": "^7.3.1",
|
||||
"nodemon": "^2.0.15",
|
||||
"prettyjson": "^1.2.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"ts-node": "^10.4.0",
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@standardnotes/config": "2.0.1",
|
||||
"@types/connect-busboy": "^1.0.0",
|
||||
"@types/cors": "^2.8.9",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/jest": "^28.1.3",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/newrelic": "^7.0.1",
|
||||
"@types/prettyjson": "^0.0.29",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^28.1.1",
|
||||
"nodemon": "^2.0.16",
|
||||
"ts-jest": "^28.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
}
|
||||
226
packages/files/src/Bootstrap/Container.ts
Normal file
226
packages/files/src/Bootstrap/Container.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import * as winston from 'winston'
|
||||
import Redis from 'ioredis'
|
||||
import * as AWS from 'aws-sdk'
|
||||
import { Container } from 'inversify'
|
||||
|
||||
import { Env } from './Env'
|
||||
import TYPES from './Types'
|
||||
import { UploadFileChunk } from '../Domain/UseCase/UploadFileChunk/UploadFileChunk'
|
||||
import { ValetTokenAuthMiddleware } from '../Controller/ValetTokenAuthMiddleware'
|
||||
import { TokenDecoder, TokenDecoderInterface, ValetTokenData } from '@standardnotes/auth'
|
||||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
|
||||
import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
|
||||
import {
|
||||
RedisDomainEventPublisher,
|
||||
RedisDomainEventSubscriberFactory,
|
||||
RedisEventMessageHandler,
|
||||
SNSDomainEventPublisher,
|
||||
SQSDomainEventSubscriberFactory,
|
||||
SQSEventMessageHandler,
|
||||
SQSNewRelicEventMessageHandler,
|
||||
} from '@standardnotes/domain-events-infra'
|
||||
import { StreamDownloadFile } from '../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
|
||||
import { FileDownloaderInterface } from '../Domain/Services/FileDownloaderInterface'
|
||||
import { S3FileDownloader } from '../Infra/S3/S3FileDownloader'
|
||||
import { FileUploaderInterface } from '../Domain/Services/FileUploaderInterface'
|
||||
import { S3FileUploader } from '../Infra/S3/S3FileUploader'
|
||||
import { FSFileDownloader } from '../Infra/FS/FSFileDownloader'
|
||||
import { FSFileUploader } from '../Infra/FS/FSFileUploader'
|
||||
import { CreateUploadSession } from '../Domain/UseCase/CreateUploadSession/CreateUploadSession'
|
||||
import { FinishUploadSession } from '../Domain/UseCase/FinishUploadSession/FinishUploadSession'
|
||||
import { UploadRepositoryInterface } from '../Domain/Upload/UploadRepositoryInterface'
|
||||
import { RedisUploadRepository } from '../Infra/Redis/RedisUploadRepository'
|
||||
import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata'
|
||||
import { FileRemoverInterface } from '../Domain/Services/FileRemoverInterface'
|
||||
import { S3FileRemover } from '../Infra/S3/S3FileRemover'
|
||||
import { FSFileRemover } from '../Infra/FS/FSFileRemover'
|
||||
import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
|
||||
import {
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventMessageHandlerInterface,
|
||||
DomainEventSubscriberFactoryInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { MarkFilesToBeRemoved } from '../Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
|
||||
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
|
||||
import { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(): Promise<Container> {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const container = new Container()
|
||||
|
||||
const logger = this.createLogger({ env })
|
||||
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.S3_BUCKET_NAME).toConstantValue(env.get('S3_BUCKET_NAME', true))
|
||||
container.bind(TYPES.S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true))
|
||||
container.bind(TYPES.VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET'))
|
||||
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.REDIS_URL).toConstantValue(env.get('REDIS_URL'))
|
||||
container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
|
||||
container.bind(TYPES.MAX_CHUNK_BYTES).toConstantValue(+env.get('MAX_CHUNK_BYTES'))
|
||||
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
|
||||
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
|
||||
container
|
||||
.bind(TYPES.FILE_UPLOAD_PATH)
|
||||
.toConstantValue(env.get('FILE_UPLOAD_PATH', true) ?? `${__dirname}/../../uploads`)
|
||||
|
||||
const redisUrl = container.get(TYPES.REDIS_URL) as string
|
||||
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)
|
||||
|
||||
if (env.get('AWS_ACCESS_KEY_ID', true)) {
|
||||
AWS.config.credentials = new AWS.EnvironmentCredentials('AWS')
|
||||
}
|
||||
|
||||
if (env.get('S3_AWS_REGION', true) || env.get('S3_ENDPOINT', true)) {
|
||||
const s3Opts: AWS.S3.Types.ClientConfiguration = {
|
||||
apiVersion: 'latest',
|
||||
}
|
||||
if (env.get('S3_AWS_REGION', true)) {
|
||||
s3Opts.region = env.get('S3_AWS_REGION', true)
|
||||
}
|
||||
if (env.get('S3_ENDPOINT', true)) {
|
||||
s3Opts.endpoint = new AWS.Endpoint(env.get('S3_ENDPOINT', true))
|
||||
}
|
||||
const s3Client = new AWS.S3(s3Opts)
|
||||
container.bind<AWS.S3>(TYPES.S3).toConstantValue(s3Client)
|
||||
container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(S3FileDownloader)
|
||||
container.bind<FileUploaderInterface>(TYPES.FileUploader).to(S3FileUploader)
|
||||
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(S3FileRemover)
|
||||
} else {
|
||||
container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(FSFileDownloader)
|
||||
container
|
||||
.bind<FileUploaderInterface>(TYPES.FileUploader)
|
||||
.toConstantValue(new FSFileUploader(container.get(TYPES.FILE_UPLOAD_PATH), container.get(TYPES.Logger)))
|
||||
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(FSFileRemover)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
// use cases
|
||||
container.bind<UploadFileChunk>(TYPES.UploadFileChunk).to(UploadFileChunk)
|
||||
container.bind<StreamDownloadFile>(TYPES.StreamDownloadFile).to(StreamDownloadFile)
|
||||
container.bind<CreateUploadSession>(TYPES.CreateUploadSession).to(CreateUploadSession)
|
||||
container.bind<FinishUploadSession>(TYPES.FinishUploadSession).to(FinishUploadSession)
|
||||
container.bind<GetFileMetadata>(TYPES.GetFileMetadata).to(GetFileMetadata)
|
||||
container.bind<RemoveFile>(TYPES.RemoveFile).to(RemoveFile)
|
||||
container.bind<MarkFilesToBeRemoved>(TYPES.MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
|
||||
|
||||
// middleware
|
||||
container.bind<ValetTokenAuthMiddleware>(TYPES.ValetTokenAuthMiddleware).to(ValetTokenAuthMiddleware)
|
||||
|
||||
// services
|
||||
container
|
||||
.bind<TokenDecoderInterface<ValetTokenData>>(TYPES.ValetTokenDecoder)
|
||||
.toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.VALET_TOKEN_SECRET)))
|
||||
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
||||
container.bind<DomainEventFactoryInterface>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||
|
||||
// repositories
|
||||
container.bind<UploadRepositoryInterface>(TYPES.UploadRepository).to(RedisUploadRepository)
|
||||
|
||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||
container
|
||||
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
|
||||
} else {
|
||||
container
|
||||
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||
.toConstantValue(
|
||||
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
|
||||
)
|
||||
}
|
||||
|
||||
// Handlers
|
||||
container
|
||||
.bind<AccountDeletionRequestedEventHandler>(TYPES.AccountDeletionRequestedEventHandler)
|
||||
.to(AccountDeletionRequestedEventHandler)
|
||||
container
|
||||
.bind<SharedSubscriptionInvitationCanceledEventHandler>(TYPES.SharedSubscriptionInvitationCanceledEventHandler)
|
||||
.to(SharedSubscriptionInvitationCanceledEventHandler)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
|
||||
[
|
||||
'SHARED_SUBSCRIPTION_INVITATION_CANCELED',
|
||||
container.get(TYPES.SharedSubscriptionInvitationCanceledEventHandler),
|
||||
],
|
||||
])
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
createLogger({ env }: { env: Env }): winston.Logger {
|
||||
return winston.createLogger({
|
||||
level: env.get('LOG_LEVEL') || 'info',
|
||||
format: winston.format.combine(winston.format.splat(), winston.format.json()),
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
|
||||
})
|
||||
}
|
||||
}
|
||||
24
packages/files/src/Bootstrap/Env.ts
Normal file
24
packages/files/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]
|
||||
}
|
||||
}
|
||||
58
packages/files/src/Bootstrap/Types.ts
Normal file
58
packages/files/src/Bootstrap/Types.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
const TYPES = {
|
||||
Logger: Symbol.for('Logger'),
|
||||
HTTPClient: Symbol.for('HTTPClient'),
|
||||
Redis: Symbol.for('Redis'),
|
||||
S3: Symbol.for('S3'),
|
||||
SNS: Symbol.for('SNS'),
|
||||
SQS: Symbol.for('SQS'),
|
||||
|
||||
// use cases
|
||||
UploadFileChunk: Symbol.for('UploadFileChunk'),
|
||||
StreamDownloadFile: Symbol.for('StreamDownloadFile'),
|
||||
CreateUploadSession: Symbol.for('CreateUploadSession'),
|
||||
FinishUploadSession: Symbol.for('FinishUploadSession'),
|
||||
GetFileMetadata: Symbol.for('GetFileMetadata'),
|
||||
RemoveFile: Symbol.for('RemoveFile'),
|
||||
MarkFilesToBeRemoved: Symbol.for('MarkFilesToBeRemoved'),
|
||||
|
||||
// services
|
||||
ValetTokenDecoder: Symbol.for('ValetTokenDecoder'),
|
||||
Timer: Symbol.for('Timer'),
|
||||
DomainEventFactory: Symbol.for('DomainEventFactory'),
|
||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||
FileUploader: Symbol.for('FileUploader'),
|
||||
FileDownloader: Symbol.for('FileDownloader'),
|
||||
FileRemover: Symbol.for('FileRemover'),
|
||||
|
||||
// repositories
|
||||
UploadRepository: Symbol.for('UploadRepository'),
|
||||
|
||||
// middleware
|
||||
ValetTokenAuthMiddleware: Symbol.for('ValetTokenAuthMiddleware'),
|
||||
|
||||
// env vars
|
||||
AWS_ACCESS_KEY_ID: Symbol.for('AWS_ACCESS_KEY_ID'),
|
||||
AWS_SECRET_ACCESS_KEY: Symbol.for('AWS_SECRET_ACCESS_KEY'),
|
||||
S3_ENDPOINT: Symbol.for('S3_ENDPOINT'),
|
||||
S3_BUCKET_NAME: Symbol.for('S3_BUCKET_NAME'),
|
||||
S3_AWS_REGION: Symbol.for('S3_AWS_REGION'),
|
||||
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'),
|
||||
VALET_TOKEN_SECRET: Symbol.for('VALET_TOKEN_SECRET'),
|
||||
REDIS_URL: Symbol.for('REDIS_URL'),
|
||||
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
||||
MAX_CHUNK_BYTES: Symbol.for('MAX_CHUNK_BYTES'),
|
||||
VERSION: Symbol.for('VERSION'),
|
||||
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
|
||||
FILE_UPLOAD_PATH: Symbol.for('FILE_UPLOAD_PATH'),
|
||||
|
||||
// Handlers
|
||||
DomainEventMessageHandler: Symbol.for('DomainEventMessageHandler'),
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||
SharedSubscriptionInvitationCanceledEventHandler: Symbol.for('SharedSubscriptionInvitationCanceledEventHandler'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user