Compare commits

...

18 Commits

Author SHA1 Message Date
standardci
90359d61d9 chore(release): publish
- @standardnotes/api-gateway@1.1.0-alpha.0
 - @standardnotes/files-server@1.1.0-alpha.2
 - @standardnotes/syncing-server@1.1.0-alpha.4
2022-06-23 09:34:02 +00:00
Karol Sójko
57c3b9c29e feat: add api-gateway package 2022-06-23 11:33:14 +02:00
Karol Sójko
b25f2e8c54 fix: remove unnessary cp of dotenv file 2022-06-23 08:50:55 +02:00
standardci
5be40fa99c chore(release): publish
- @standardnotes/auth-server@1.1.0-alpha.5
 - @standardnotes/files-server@1.1.0-alpha.1
 - @standardnotes/scheduler-server@1.1.0-alpha.13
 - @standardnotes/syncing-server@1.1.0-alpha.3
2022-06-22 14:48:20 +00:00
Karol Sójko
bc909dd3aa Merge branch 'develop' of github.com:standardnotes/server into develop 2022-06-22 16:47:36 +02:00
Karol Sójko
3110c20596 fix: make DISABLE_USER_REGISTRATION env var optional 2022-06-22 16:47:30 +02:00
standardci
69a9c5555b chore(release): publish
- @standardnotes/auth-server@1.1.0-alpha.4
 - @standardnotes/files-server@1.1.0-alpha.0
 - @standardnotes/scheduler-server@1.1.0-alpha.12
 - @standardnotes/syncing-server@1.1.0-alpha.2
2022-06-22 14:46:11 +00:00
Karol Sójko
cca6c7f65e Merge branch 'develop' of github.com:standardnotes/server into develop 2022-06-22 16:45:02 +02:00
Karol Sójko
7a8a5fcfdf feat: add files server package 2022-06-22 16:44:45 +02:00
standardci
845d310af9 chore(release): publish
- @standardnotes/auth-server@1.1.0-alpha.3
 - @standardnotes/scheduler-server@1.1.0-alpha.11
 - @standardnotes/syncing-server@1.1.0-alpha.1
2022-06-22 14:24:30 +00:00
Karol Sójko
d61e6f338e Merge branch 'develop' of github.com:standardnotes/server into develop 2022-06-22 16:23:43 +02:00
Karol Sójko
f2ddbc82d0 fix: bump @standardnotes/time dependency 2022-06-22 16:23:25 +02:00
standardci
8708b10dfc chore(release): publish
- @standardnotes/auth-server@1.1.0-alpha.2
 - @standardnotes/scheduler-server@1.1.0-alpha.10
 - @standardnotes/syncing-server@1.1.0-alpha.0
2022-06-22 14:17:02 +00:00
Karol Sójko
165ee50dc8 Merge branch 'develop' of github.com:standardnotes/server into develop 2022-06-22 16:16:13 +02:00
Karol Sójko
6cc4ef90db feat: add syncing-server package 2022-06-22 16:16:03 +02:00
standardci
ad8a07b003 chore(release): publish
- @standardnotes/auth-server@1.1.0-alpha.1
 - @standardnotes/scheduler-server@1.1.0-alpha.9
2022-06-22 12:43:19 +00:00
Karol Sójko
923fe2a19b Merge branch 'develop' of github.com:standardnotes/server into develop 2022-06-22 14:42:31 +02:00
Karol Sójko
f3c6fff20f fix: docker entrypoint for auth 2022-06-22 14:41:56 +02:00
330 changed files with 17391 additions and 216 deletions

View File

@@ -0,0 +1,125 @@
name: Api Gateway Dev
concurrency:
group: api_gateway_dev_environment
cancel-in-progress: true
on:
push:
tags:
- '@standardnotes/api-gateway@[0-9]*.[0-9]*.[0-9]*-alpha.[0-9]*'
- '@standardnotes/api-gateway@[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:api-gateway
publish-aws-ecr:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- 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:dev
docker push $ECR_REGISTRY/$ECR_REPOSITORY:dev
publish-docker-hub:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: standardnotes/api-gateway
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 api-gateway-dev --query taskDefinition > task-definition.json
- name: 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: 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: api-gateway-dev
image: ${{ secrets.AWS_ECR_REGISTRY }}/api-gateway:${{ 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: api-gateway-dev
cluster: dev
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_DEV }}
revision: "${{ github.sha }}"
description: "Automated Deployment via Github Actions"
user: "${{ github.actor }}"
notify_discord:
needs: deploy-web
runs-on: ubuntu-latest
steps:
- name: Run Discord Webhook
uses: johnnyhuy/actions-discord-git-webhook@main
with:
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}

168
.github/workflows/files.release.dev.yml vendored Normal file
View File

@@ -0,0 +1,168 @@
name: Files Server Dev
concurrency:
group: files_dev_environment
cancel-in-progress: true
on:
push:
tags:
- '@standardnotes/files-server@[0-9]*.[0-9]*.[0-9]*-alpha.[0-9]*'
- '@standardnotes/files-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:files
- run: yarn test:files
publish-aws-ecr:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- 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:dev
docker push $ECR_REGISTRY/$ECR_REPOSITORY:dev
publish-docker-hub:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: standardnotes/files
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 files-dev --query taskDefinition > task-definition.json
- name: 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: 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: files-dev
image: ${{ secrets.AWS_ECR_REGISTRY }}/files:${{ 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: files-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 files-worker-dev --query taskDefinition > task-definition.json
- name: 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: 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: files-worker-dev
image: ${{ secrets.AWS_ECR_REGISTRY }}/files:${{ 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: files-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_FILES_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_FILES_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 }}

View File

@@ -0,0 +1,168 @@
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
- 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
- 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 }}

3
.gitignore vendored
View File

@@ -15,3 +15,6 @@ newrelic_agent.log
!.yarn/unplugged
!.yarn/sdks
!.yarn/versions
packages/files/uploads/*
!packages/files/uploads/.gitkeep

1105
.pnp.cjs generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -14,17 +14,30 @@
"lint": "yarn workspaces foreach -p -j 10 --verbose run lint",
"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",
"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:beta": "lerna version --conventional-prerelease --conventional-commits --yes -m \"chore(release): publish\""
},
"devDependencies": {

View 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=

View File

@@ -0,0 +1,2 @@
dist
test-setup.ts

View File

@@ -0,0 +1,6 @@
{
"extends": "../../.eslintrc",
"parserOptions": {
"project": "./linter.tsconfig.json"
}
}

View File

@@ -0,0 +1,10 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# 1.1.0-alpha.0 (2022-06-23)
### Features
* add api-gateway package ([57c3b9c](https://github.com/standardnotes/api-gateway/commit/57c3b9c29e5b16449c864e59dbc1fd11689125f9))

View 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 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" ]

View 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)
})
})

View 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}`)
})

View 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 "$@"

View 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'
]
};

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["dist", "test-setup.ts"]
}

View File

@@ -0,0 +1,60 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.1.0-alpha.0",
"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"
}
}

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

View 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]
}
}

View 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

View 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)
}
}

View File

@@ -0,0 +1,9 @@
import { controller, httpGet } from 'inversify-express-utils'
@controller('/healthcheck')
export class HealthCheckController {
@httpGet('/')
public async get(): Promise<string> {
return 'OK'
}
}

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

View 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()
}
}

View File

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

View File

@@ -0,0 +1,4 @@
export enum TokenAuthenticationMethod {
OfflineSubscriptionToken = 'OfflineSubscriptionToken',
SubscriptionToken = 'SubscriptionToken',
}

View 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,
)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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,
)
}
}

View 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)
}
}

View File

@@ -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}`,
)
}
}

View 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)
}
}

View File

@@ -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`,
)
}
}

View 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)
}
}

View 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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)
}
})
}
}

View File

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

View File

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
},
"include": [
"src/**/*",
"bin/**/*",
],
"references": []
}

View 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

View File

@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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
* add syncing-server package ([6cc4ef9](https://github.com/standardnotes/auth/commit/6cc4ef90db9e79d53f9646311d234432ef4cf1e3))
# [1.1.0-alpha.1](https://github.com/standardnotes/auth/compare/@standardnotes/auth-server@1.1.0-alpha.0...@standardnotes/auth-server@1.1.0-alpha.1) (2022-06-22)
### Bug Fixes
* docker entrypoint for auth ([f3c6fff](https://github.com/standardnotes/auth/commit/f3c6fff20f98a9b23b7221e59e6853255f54a369))
# 1.1.0-alpha.0 (2022-06-22)
### Features

View File

@@ -6,42 +6,42 @@ COMMAND=$1 && shift 1
case "$COMMAND" in
'start-local' )
echo "Starting Web..."
yarn start:local
yarn workspace @standardnotes/auth-server start:local
;;
'start-web' )
echo "Starting Web..."
yarn start
yarn workspace @standardnotes/auth-server start
;;
'start-worker' )
echo "Starting Worker..."
yarn worker
yarn workspace @standardnotes/auth-server worker
;;
'email-daily-backup' )
echo "Starting Email Daily Backup..."
yarn daily-backup:email
yarn workspace @standardnotes/auth-server daily-backup:email
;;
'email-weekly-backup' )
echo "Starting Email Weekly Backup..."
yarn weekly-backup:email
yarn workspace @standardnotes/auth-server weekly-backup:email
;;
'dropbox-daily-backup' )
echo "Starting Dropbox Daily Backup..."
yarn daily-backup:dropbox
yarn workspace @standardnotes/auth-server daily-backup:dropbox
;;
'google-drive-daily-backup' )
echo "Starting Google Drive Daily Backup..."
yarn daily-backup:google_drive
yarn workspace @standardnotes/auth-server daily-backup:google_drive
;;
'one-drive-daily-backup' )
echo "Starting One Drive Daily Backup..."
yarn daily-backup:one_drive
yarn workspace @standardnotes/auth-server daily-backup:one_drive
;;
* )

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.1.0-alpha.0",
"version": "1.1.0-alpha.5",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

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

View File

@@ -58,7 +58,7 @@ export const AppDataSource = new DataSource({
SubscriptionSetting,
AnalyticsEntity,
],
migrations: [env.get('DB_MIGRATIONS_PATH')],
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
migrationsRun: true,
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),
})

View 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=

View File

@@ -0,0 +1,3 @@
dist
test-setup.ts
data

6
packages/files/.eslintrc Normal file
View File

@@ -0,0 +1,6 @@
{
"extends": "../../.eslintrc",
"parserOptions": {
"project": "./linter.tsconfig.json"
}
}

View File

@@ -0,0 +1,20 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.1.0-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
View 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 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" ]

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