├── .env ├── .github ├── dependabot.yml └── workflows │ ├── build-dashboard.yml │ ├── build.yml │ ├── release-dashboard.yml │ ├── release.yml │ └── stale.yml ├── .gitignore ├── .slugignore ├── LICENSE ├── README.md ├── Singularity ├── app.json ├── dashboard.env ├── docker-compose.dashboard.yml ├── docker-compose.mongo.dashboard.yml ├── docker-compose.mongo.yml ├── docker-compose.no.hipaa.yml ├── docker-compose.yml ├── general.env ├── heroku.yml ├── nginx └── sites-enabled │ ├── default-ssl.conf │ └── default.conf ├── parse ├── Dockerfile ├── Dockerfile.dashboard ├── Dockerfile.heroku ├── cloud │ ├── carePlan.js │ ├── contact.js │ ├── files.js │ ├── main.js │ ├── note.js │ ├── outcome.js │ ├── outcomeValue.js │ ├── patient.js │ └── task.js ├── docker-compose.test.yml ├── ecosystem.config.js ├── index.js ├── parse-dashboard-config.json ├── process.yml └── scripts │ ├── parse_idempotency_delete_expired_records.sh │ ├── setup-dbs.sh │ ├── setup-parse-index.sh │ ├── setup-pgaudit.sh │ └── wait-for-postgres.sh ├── scripts └── wait-for-postgres.sh └── singularity-compose.yml /.env: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=postgres 2 | POSTGRES_PASSWORD=postgres 3 | PG_PARSE_USER=parse 4 | PG_PARSE_PASSWORD=parse 5 | PG_PARSE_DB=parse_hipaa 6 | PMM_USER=pmm 7 | PMM_PASSWORD=pmm 8 | PMM_PORT=80 9 | PMM_TLS_PORT=443 10 | MONGO_PARSE_USER=parse 11 | MONGO_PARSE_PASSWORD=parse 12 | MONGO_PARSE_DB=parse_hipaa 13 | PORT=1337 14 | MOUNT_PATH=/parse 15 | DB_PORT=5432 16 | DB_MONGO_PORT=27017 17 | DASHBOARD_PORT=4040 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "docker" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/workflows/build-dashboard.yml: -------------------------------------------------------------------------------- 1 | name: build-dashboard 2 | 3 | on: 4 | schedule: 5 | - cron: '19 17 * * *' 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | merge_group: 11 | branches: [ main ] 12 | 13 | env: 14 | REGISTRY: docker.io 15 | IMAGE_NAME: ${{ github.repository }} 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | docker: 23 | runs-on: ubuntu-latest 24 | permissions: 25 | contents: read 26 | packages: write 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | 32 | - name: Set up QEMU 33 | id: qemu 34 | uses: docker/setup-qemu-action@v3 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - name: Log into dockerhub 40 | if: github.event_name != 'pull_request' && github.event_name != 'merge_group' 41 | uses: docker/login-action@v3 42 | with: 43 | username: ${{ secrets.DOCKERHUB_USERNAME }} 44 | password: ${{ secrets.DOCKERHUB_TOKEN }} 45 | 46 | - name: Extract Docker metadata 47 | id: meta 48 | uses: docker/metadata-action@v5 49 | with: 50 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 51 | flavor: | 52 | latest=false 53 | suffix=-dashboard 54 | 55 | - name: Build and push Docker image 56 | uses: docker/build-push-action@v6 57 | with: 58 | context: parse/. 59 | file: parse/Dockerfile.dashboard 60 | platforms: linux/amd64, linux/arm64/v8 61 | push: ${{ github.event_name != 'pull_request' && github.event_name != 'merge_group' }} 62 | tags: ${{ steps.meta.outputs.tags }} 63 | labels: ${{ steps.meta.outputs.labels }} 64 | 65 | singularity: 66 | needs: docker 67 | runs-on: ubuntu-latest 68 | container: 69 | image: quay.io/singularity/singularity:v3.11.5 70 | strategy: 71 | fail-fast: false 72 | matrix: 73 | recipe: ["Singularity"] 74 | steps: 75 | - name: Check out code for the container build 76 | uses: actions/checkout@v4 77 | 78 | - name: Continue if Singularity recipe exists 79 | run: | 80 | if [[ -f "${{ matrix.recipe }}" ]]; then 81 | echo "keepgoing=true" >> $GITHUB_ENV 82 | fi 83 | 84 | - name: Get build release version 85 | run: echo "TAG=${GITHUB_REF##*/}-dashboard" >> $GITHUB_ENV 86 | 87 | - name: Update Singularity file tag 88 | if: github.event_name != 'pull_request' && github.event_name != 'merge_group' 89 | run: | 90 | sed -i "s/latest/$TAG/" ./Singularity 91 | 92 | - name: Build Singularity image 93 | if: ${{ env.keepgoing == 'true' }} 94 | env: 95 | recipe: ${{ matrix.recipe }} 96 | run: | 97 | ls 98 | if [ -f "${{ matrix.recipe }}" ]; then 99 | sudo -E singularity build container.sif ${{ matrix.recipe }} 100 | tag=$(echo "${recipe/Singularity\./}") 101 | if [ "$tag" == "Singularity" ]; then 102 | tag=$TAG 103 | fi 104 | # Build the container and name by tag 105 | echo "Tag is $tag." 106 | echo "tag=$tag" >> $GITHUB_ENV 107 | else 108 | echo "${{ matrix.recipe }} is not found." 109 | echo "Present working directory: $PWD" 110 | ls 111 | fi 112 | 113 | - name: Login and Deploy Container 114 | if: (github.event_name != 'pull_request' && github.event_name != 'merge_group') 115 | env: 116 | keepgoing: ${{ env.keepgoing }} 117 | run: | 118 | if [[ "${keepgoing}" == "true" ]]; then 119 | echo ${{ secrets.GITHUB_TOKEN }} | singularity remote login -u ${{ secrets.GHCR_USERNAME }} --password-stdin oras://ghcr.io 120 | singularity push container.sif oras://ghcr.io/${GITHUB_REPOSITORY}:${tag} 121 | fi 122 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | schedule: 5 | - cron: '19 17 * * *' 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | merge_group: 11 | branches: [ main ] 12 | 13 | env: 14 | REGISTRY: docker.io 15 | IMAGE_NAME: ${{ github.repository }} 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | docker: 23 | runs-on: ubuntu-latest 24 | permissions: 25 | contents: read 26 | packages: write 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | 32 | - name: Set up QEMU 33 | id: qemu 34 | uses: docker/setup-qemu-action@v3 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - name: Log into dockerhub 40 | if: github.event_name != 'pull_request' && github.event_name != 'merge_group' 41 | uses: docker/login-action@v3 42 | with: 43 | username: ${{ secrets.DOCKERHUB_USERNAME }} 44 | password: ${{ secrets.DOCKERHUB_TOKEN }} 45 | 46 | - name: Extract Docker metadata 47 | id: meta 48 | uses: docker/metadata-action@v5 49 | with: 50 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 51 | flavor: | 52 | latest=false 53 | 54 | - name: Build and push Docker image 55 | uses: docker/build-push-action@v6 56 | with: 57 | context: parse/. 58 | platforms: linux/amd64, linux/arm64/v8 59 | push: ${{ github.event_name != 'pull_request' && github.event_name != 'merge_group' }} 60 | tags: ${{ steps.meta.outputs.tags }} 61 | labels: ${{ steps.meta.outputs.labels }} 62 | 63 | singularity: 64 | needs: docker 65 | runs-on: ubuntu-latest 66 | container: 67 | image: quay.io/singularity/singularity:v3.11.5 68 | strategy: 69 | fail-fast: false 70 | matrix: 71 | recipe: ["Singularity"] 72 | steps: 73 | - name: Check out code for the container build 74 | uses: actions/checkout@v4 75 | 76 | - name: Continue if Singularity recipe exists 77 | run: | 78 | if [[ -f "${{ matrix.recipe }}" ]]; then 79 | echo "keepgoing=true" >> $GITHUB_ENV 80 | fi 81 | 82 | - name: Get build release version 83 | run: echo "TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV 84 | 85 | - name: Update Singularity file tag 86 | if: github.event_name != 'pull_request' && github.event_name != 'merge_group' 87 | run: | 88 | sed -i "s/latest/$TAG/" ./Singularity 89 | 90 | - name: Build Singularity image 91 | if: ${{ env.keepgoing == 'true' }} 92 | env: 93 | recipe: ${{ matrix.recipe }} 94 | run: | 95 | ls 96 | if [ -f "${{ matrix.recipe }}" ]; then 97 | sudo -E singularity build container.sif ${{ matrix.recipe }} 98 | tag=$(echo "${recipe/Singularity\./}") 99 | if [ "$tag" == "Singularity" ]; then 100 | tag=$TAG 101 | fi 102 | # Build the container and name by tag 103 | echo "Tag is $tag." 104 | echo "tag=$tag" >> $GITHUB_ENV 105 | else 106 | echo "${{ matrix.recipe }} is not found." 107 | echo "Present working directory: $PWD" 108 | ls 109 | fi 110 | 111 | - name: Login and deploy container 112 | if: (github.event_name != 'pull_request' && github.event_name != 'merge_group') 113 | env: 114 | keepgoing: ${{ env.keepgoing }} 115 | run: | 116 | if [[ "${keepgoing}" == "true" ]]; then 117 | echo ${{ secrets.GITHUB_TOKEN }} | singularity remote login -u ${{ secrets.GHCR_USERNAME }} --password-stdin oras://ghcr.io 118 | singularity push container.sif oras://ghcr.io/${GITHUB_REPOSITORY}:${tag} 119 | fi 120 | -------------------------------------------------------------------------------- /.github/workflows/release-dashboard.yml: -------------------------------------------------------------------------------- 1 | name: release-dashboard 2 | 3 | on: 4 | push: 5 | tags: [ '*.*.*', '*.*.*-*' ] 6 | 7 | env: 8 | REGISTRY: docker.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | docker: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | packages: write 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Set up QEMU 27 | id: qemu 28 | uses: docker/setup-qemu-action@v3 29 | 30 | - name: Set up Docker Buildx 31 | uses: docker/setup-buildx-action@v3 32 | 33 | - name: Log into dockerhub 34 | if: github.event_name != 'pull_request' 35 | uses: docker/login-action@v3 36 | with: 37 | username: ${{ secrets.DOCKERHUB_USERNAME }} 38 | password: ${{ secrets.DOCKERHUB_TOKEN }} 39 | 40 | - name: Extract Docker metadata 41 | id: meta 42 | uses: docker/metadata-action@v5 43 | with: 44 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 45 | flavor: | 46 | latest=false 47 | suffix=-dashboard 48 | 49 | - name: Build and push Docker image 50 | uses: docker/build-push-action@v6 51 | with: 52 | context: parse/. 53 | file: parse/Dockerfile.dashboard 54 | platforms: linux/amd64, linux/arm64/v8 55 | push: ${{ github.event_name != 'pull_request' }} 56 | tags: ${{ steps.meta.outputs.tags }} 57 | labels: ${{ steps.meta.outputs.labels }} 58 | 59 | singularity: 60 | needs: docker 61 | runs-on: ubuntu-latest 62 | container: 63 | image: quay.io/singularity/singularity:v3.11.5 64 | strategy: 65 | fail-fast: false 66 | matrix: 67 | recipe: ["Singularity"] 68 | steps: 69 | - name: Check out code for the container build 70 | uses: actions/checkout@v4 71 | 72 | - name: Continue if Singularity recipe exists 73 | run: | 74 | if [[ -f "${{ matrix.recipe }}" ]]; then 75 | echo "keepgoing=true" >> $GITHUB_ENV 76 | fi 77 | 78 | - name: Get release version 79 | run: echo "TAG=${GITHUB_REF/refs\/tags\//}-dashboard" >> $GITHUB_ENV 80 | 81 | - name: Update Singularity file tag 82 | run: | 83 | sed -i "s/latest/$TAG/" ./Singularity 84 | 85 | - name: Build Singularity image 86 | env: 87 | recipe: ${{ matrix.recipe }} 88 | run: | 89 | ls 90 | if [ -f "${{ matrix.recipe }}" ]; then 91 | sudo -E singularity build container.sif ${{ matrix.recipe }} 92 | tag=$(echo "${recipe/Singularity\./}") 93 | if [ "$tag" == "Singularity" ]; then 94 | tag=$TAG 95 | fi 96 | # Build the container and name by tag 97 | echo "Tag is $tag." 98 | echo "tag=$tag" >> $GITHUB_ENV 99 | else 100 | echo "${{ matrix.recipe }} is not found." 101 | echo "Present working directory: $PWD" 102 | ls 103 | fi 104 | 105 | - name: Login and Deploy Container 106 | env: 107 | keepgoing: ${{ env.keepgoing }} 108 | run: | 109 | if [[ "${keepgoing}" == "true" ]]; then 110 | echo ${{ secrets.GITHUB_TOKEN }} | singularity remote login -u ${{ secrets.GHCR_USERNAME }} --password-stdin oras://ghcr.io 111 | singularity push container.sif oras://ghcr.io/${GITHUB_REPOSITORY}:${tag} 112 | fi 113 | 114 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: [ '*.*.*', '*.*.*-*' ] 6 | 7 | env: 8 | LATEST: '8.2.1' 9 | REGISTRY: docker.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | docker: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | packages: write 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Set up QEMU 28 | id: qemu 29 | uses: docker/setup-qemu-action@v3 30 | 31 | - name: Set up Docker Buildx 32 | uses: docker/setup-buildx-action@v3 33 | 34 | - name: Log into dockerhub 35 | if: github.event_name != 'pull_request' 36 | uses: docker/login-action@v3 37 | with: 38 | username: ${{ secrets.DOCKERHUB_USERNAME }} 39 | password: ${{ secrets.DOCKERHUB_TOKEN }} 40 | 41 | - name: Get release version 42 | run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 43 | 44 | - name: Extract Docker metadata for latest 45 | if: env.TAG == env.LATEST 46 | id: meta-latest 47 | uses: docker/metadata-action@v5 48 | with: 49 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 50 | flavor: | 51 | latest=true 52 | 53 | - name: Extract Docker metadata 54 | if: env.TAG != env.LATEST 55 | id: meta 56 | uses: docker/metadata-action@v5 57 | with: 58 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 59 | flavor: | 60 | latest=false 61 | 62 | - name: Build and push Docker image for latest 63 | if: env.TAG == env.LATEST 64 | uses: docker/build-push-action@v6 65 | with: 66 | context: parse/. 67 | platforms: linux/amd64, linux/arm64/v8 68 | push: ${{ github.event_name != 'pull_request' }} 69 | tags: ${{ steps.meta-latest.outputs.tags }} 70 | labels: ${{ steps.meta-latest.outputs.labels }} 71 | 72 | - name: Build and push Docker image 73 | if: env.TAG != env.LATEST 74 | uses: docker/build-push-action@v6 75 | with: 76 | context: parse/. 77 | platforms: linux/amd64, linux/arm64/v8 78 | push: ${{ github.event_name != 'pull_request' }} 79 | tags: ${{ steps.meta.outputs.tags }} 80 | labels: ${{ steps.meta.outputs.labels }} 81 | 82 | singularity: 83 | needs: docker 84 | runs-on: ubuntu-latest 85 | container: 86 | image: quay.io/singularity/singularity:v3.11.5 87 | strategy: 88 | fail-fast: false 89 | matrix: 90 | recipe: ["Singularity"] 91 | 92 | steps: 93 | - name: Check out code for the container build 94 | uses: actions/checkout@v4 95 | 96 | - name: Continue if Singularity recipe exists 97 | run: | 98 | if [[ -f "${{ matrix.recipe }}" ]]; then 99 | echo "keepgoing=true" >> $GITHUB_ENV 100 | fi 101 | 102 | - name: Get release version 103 | run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 104 | 105 | - name: Update Singularity file tag 106 | run: | 107 | sed -i "s/latest/$TAG/" ./Singularity 108 | 109 | - name: Build Singularity image 110 | env: 111 | recipe: ${{ matrix.recipe }} 112 | run: | 113 | ls 114 | if [ -f "${{ matrix.recipe }}" ]; then 115 | sudo -E singularity build container.sif ${{ matrix.recipe }} 116 | tag=$(echo "${recipe/Singularity\./}") 117 | if [ "$tag" == "Singularity" ]; then 118 | tag=$TAG 119 | fi 120 | # Build the container and name by tag 121 | echo "Tag is $tag." 122 | echo "tag=$tag" >> $GITHUB_ENV 123 | else 124 | echo "${{ matrix.recipe }} is not found." 125 | echo "Present working directory: $PWD" 126 | ls 127 | fi 128 | 129 | - name: Login and deploy container 130 | env: 131 | keepgoing: ${{ env.keepgoing }} 132 | run: | 133 | if [[ "${keepgoing}" == "true" ]]; then 134 | echo ${{ secrets.GITHUB_TOKEN }} | singularity remote login -u ${{ secrets.GHCR_USERNAME }} --password-stdin oras://ghcr.io 135 | singularity push container.sif oras://ghcr.io/${GITHUB_REPOSITORY}:${tag} 136 | fi 137 | 138 | - name: Login and deploy latest container 139 | if: env.TAG == env.LATEST 140 | env: 141 | keepgoing: ${{ env.keepgoing }} 142 | run: | 143 | if [[ "${keepgoing}" == "true" ]]; then 144 | echo ${{ secrets.GITHUB_TOKEN }} | singularity remote login -u ${{ secrets.GHCR_USERNAME }} --password-stdin oras://ghcr.io 145 | singularity push container.sif oras://ghcr.io/${GITHUB_REPOSITORY}:latest 146 | fi 147 | 148 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v9 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'Stale issue message' 17 | stale-pr-message: 'Stale pull request message' 18 | stale-issue-label: 'no-issue-activity' 19 | stale-pr-label: 'no-pr-activity' 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.key 3 | *.pem 4 | *.srl 5 | *.crt 6 | *.csr 7 | data 8 | docker-compose-private-debug.yml 9 | files/ 10 | -------------------------------------------------------------------------------- /.slugignore: -------------------------------------------------------------------------------- 1 | .env 2 | /.github 3 | /clamscan 4 | /dashboard 5 | /mongo 6 | /nginx 7 | /no-hipaa 8 | /sut 9 | docker-compose.yml 10 | docker-compose.no.hipaa.yml 11 | docker-compose.mongo.yml 12 | Singularity 13 | singularity-compose.yml 14 | general.env 15 | dashboard.env 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Network Reconnaissance Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parse-hipaa 2 | 3 | [![](https://dockeri.co/image/netreconlab/parse-hipaa)](https://hub.docker.com/r/netreconlab/parse-hipaa) 4 | [![build](https://github.com/netreconlab/parse-hipaa/actions/workflows/build.yml/badge.svg)](https://github.com/netreconlab/parse-hipaa/actions/workflows/build.yml) 5 | [![build](https://github.com/netreconlab/parse-hipaa/actions/workflows/build-dashboard.yml/badge.svg)](https://github.com/netreconlab/parse-hipaa/actions/workflows/build-dashboard.yml) 6 | [![release](https://github.com/netreconlab/parse-hipaa/actions/workflows/release.yml/badge.svg)](https://github.com/netreconlab/parse-hipaa/actions/workflows/release.yml) 7 | [![release](https://github.com/netreconlab/parse-hipaa/actions/workflows/release-dashboard.yml/badge.svg)](https://github.com/netreconlab/parse-hipaa/actions/workflows/release-dashboard.yml) 8 | 9 | --- 10 | 11 | ![dashboard](https://user-images.githubusercontent.com/8621344/102236202-38f32080-3ec1-11eb-88d7-24e38e95f68d.png) 12 | 13 | Run your own HIPAA & GDPR compliant [parse-server](https://github.com/parse-community/parse-server) with [PostgreSQL](https://www.postgresql.org) or [MongoDB](https://github.com/netreconlab/parse-hipaa/blob/main/docker-compose.mongo.yml). parse-hipaa also includes [parse-dashboard](https://github.com/parse-community/parse-dashboard) for viewing/modifying your data in the Cloud. Since [parse-hipaa](https://github.com/netreconlab/parse-hipaa) is a pare-server, it can be used for [iOS](https://docs.parseplatform.org/ios/guide/), [Android](https://docs.parseplatform.org/android/guide/), [Flutter](https://github.com/parse-community/Parse-SDK-Flutter/tree/master/packages/flutter#getting-started), and web based apps ([JS, React Native, etc](https://docs.parseplatform.org/js/guide/)). API's such as [GraphQL](https://docs.parseplatform.org/graphql/guide/) and [REST](https://docs.parseplatform.org/rest/guide/) are enabled by default in parse-hipaa and can be tested directly or via the "API Console" in the Parse Dashboard. See the [Parse SDK documentation](https://parseplatform.org/#sdks) for details and examples of how to leverage parse-hipaa for your language(s) of interest. parse-hipaa includes the necessary database auditing and logging for HIPAA compliance. 14 | 15 | `parse-hipaa` provides the following: 16 | - [x] Auditing & logging at server-admin level (Parse) and at the database level (postgres or mongo) 17 | - [x] The User class (and the ParseCareKit classes if you are using them) are locked down and doesn't allow unauthenticated access (the standard parse-server allows unauthenticated read access by default) 18 | - [x] The creation of new Parse Classes and the addition of adding fields from the client-side are disabled. These can be created/added on the server-side using Parse Dashboard (the standard parse-server allows Class and field creation on the client-side by default) 19 | - [x] Ready for encryption in transit - parse-hipaa and it's companion images are setup to run behind a proxy with files & directions on how to [complete the process](https://github.com/netreconlab/parse-hipaa#deploying-on-a-real-system) with Nginx and LetsEncrypt 20 | - [x] File uploads are only allowed by authenticated users (the standard parse-server allows unauthenticated uploads by default) 21 | - [x] File uploads are encrypted with AES-256-GCM by default (the standard parse-server doesn't encrypt files by default) 22 | - [x] ~~File uploads can be scanned for viruses and malware by [clamav](https://docs.clamav.net/manual/Installing/Docker.html) before they are saved to parse-hipaa local storage. If any virus or malware is detected the files won't be persisted to the file system~~ (this has been turned off by default. Examples of how to handle can be found in [files.js](https://github.com/netreconlab/parse-hipaa/blob/main/parse/cloud/files.js) and enabled in [main.js](https://github.com/netreconlab/parse-hipaa/blob/37f79bdb99781b634780b3af6a7e33e6beae44a0/parse/cloud/main.js#L8)) 23 | 24 | You will still need to setup the following on your own to be fully HIPAA & GDPR compliant: 25 | 26 | - [ ] Encryption in transit - you will need to [complete the process](https://github.com/netreconlab/parse-hipaa#deploying-on-a-real-system) 27 | - [ ] Encryption at rest - Mount to your own encrypted storage drive for your database (Linux and macOS have API's for this) and store the drive in a "safe" location 28 | - [ ] Be sure to do anything else HIPAA & GDPR requires 29 | - [ ] If you are hosting using a remote service like Heroku, you may need to pay for additional services such as [Shield Spaces](https://devcenter.heroku.com/articles/heroku-postgres-and-private-spaces) 30 | 31 | The [CareKitSample-ParseCareKit](https://github.com/netreconlab/CareKitSample-ParseCareKit), uses parse-hipaa along with [ParseCareKit](https://github.com/netreconlab/ParseCareKit). 32 | 33 | **Use at your own risk. There is not promise that this is HIPAA compliant and we are not responsible for any mishandling of your data** 34 | 35 | ## What is inside parse-hipaa? 36 | 37 | Parse-HIPAA is derived from the [parse-server image](https://hub.docker.com/r/parseplatform/parse-server) and contains the following additional packages: 38 | - [parse-hipaa-dashboard](https://github.com/netreconlab/parse-hipaa-dashboard) 39 | - [parse-server-carekit](https://github.com/netreconlab/parse-server-carekit) 40 | - [clamscan](https://www.npmjs.com/package/clamscan) 41 | - [newrelic](https://www.npmjs.com/package/newrelic) - automatically configured with Heroku deployments, needs additional configuration if you want to use elsewhere 42 | - [parse-server-any-analytics-adapter](https://github.com/netreconlab/parse-server-any-analytics-adapter) - needs additional configuration if you want to use 43 | - [@analytics/google-analytics](https://www.npmjs.com/package/@analytics/google-analytics) - needs additional configuration if you want to use 44 | - [@analytics/google-analytics-v3](https://www.npmjs.com/package/@analytics/google-analytics-v3) - needs additional configuration if you want to use 45 | - [@parse/s3-files-adapter](https://www.npmjs.com/package/@parse/s3-files-adapter) - needs additional configuration if you want to use 46 | - [parse-server-api-mail-adapter](https://www.npmjs.com/package/parse-server-api-mail-adapter) - needs additional configuration if you want to use 47 | - [mailgun.js](https://www.npmjs.com/package/mailgun.js) - needs additional configuration if you want to use 48 | 49 | ## Images 50 | Images of parse-hipaa are automatically built for your convenience. Images can be found at the following locations: 51 | - [Docker - Hosted on Docker Hub](https://hub.docker.com/r/netreconlab/parse-hipaa) 52 | - [Singularity - Hosted on GitHub Container Registry](https://github.com/netreconlab/parse-hipaa/pkgs/container/parse-hipaa) 53 | 54 | ### Flavors and Tags 55 | 56 | #### Production 57 | - `latest` - Points to the newest released version. **This is smallest possible image of `parse-hipaa` and it does not contain [parse-hipaa-dashboard](https://github.com/netreconlab/parse-hipaa-dashboard)** 58 | - `x.x.x` - Points to a specific released version. These version numbers match their respective [parse-server](https://github.com/parse-community/parse-server#flavors--branches) released versions. **This is smallest possible image of `parse-hipaa` and it does not contain [parse-hipaa-dashboard](https://github.com/netreconlab/parse-hipaa-dashboard)** 59 | - `x.x.x-dashboard` - Points to a specific released version. These version numbers match their respective [parse-server](https://github.com/parse-community/parse-server#flavors--branches) released versions. This version of `parse-hipaa` is **built with [parse-hipaa-dashboard](https://github.com/netreconlab/parse-hipaa-dashboard) and is a larger image** 60 | 61 | 62 | #### Development 63 | - `main` - Points to the most up-to-date code and depends on the latest release of parse-server. This version of `parse-hipaa` is **built with [parse-hipaa-dashboard](https://github.com/netreconlab/parse-hipaa-dashboard)**. This tag can contain breaking changes 64 | - `x.x.x-alpha/beta` - Points to most up-to-date code and depends on the respective [alha/beta releases of parse-server](https://github.com/parse-community/parse-server#flavors--branches). This version of parse-hipaa is **built with [parse-hipaa-dashboard](https://github.com/netreconlab/parse-hipaa-dashboard)**. This tag can contain breaking changes 65 | 66 | ### Recommendations 67 | Any/all of the tagged servers can be used in combination with each other to build a [High Availability](https://en.wikipedia.org/wiki/High-availability_cluster)(HA) server-side application. For example, your HA cluster may consist of: (1) [nginx](https://www.nginx.com/resources/glossary/nginx/) reverse proxy/load balancer, (1) `x.x.x-dashboard` `parse-hipaa` server, (2) `x.x.x` `parse-hipaa` servers, and (1) [Percona Monitor and Management](https://www.percona.com/software/database-tools/percona-monitoring-and-management) server. 68 | 69 | #### Standard (without parse-hipaa-dashboard) 70 | - `latest` or `x.x.x` - Use one or more of these images if you plan to have multiple `parse-hipaa` servers working together to create [HA](https://en.wikipedia.org/wiki/High-availability_cluster) or just need a stand-alone server. Note that if all of your `parse-hipaa` servers are `x.x.x`, you may want to add a stand-alone [parse-hipaa-dashboard](https://github.com/netreconlab/parse-hipaa-dashboard) or [parse-server-dashoard](https://github.com/parse-community/parse-dashboard) 71 | - See [docker-compose.yml](https://github.com/netreconlab/parse-hipaa/blob/main/docker-compose.yml) for an example 72 | - `-dashboard` - Use one of these images only if you plan to have one stand-alone `parse-hipaa` server or you want one of your servers to also provide [parse-hipaa-dashboard](https://github.com/netreconlab/parse-hipaa-dashboard) ability 73 | - See [docker-compose-dashboard.yml](https://github.com/netreconlab/parse-hipaa/blob/main/docker-compose-dashboard.yml) for an example 74 | - `main` or `x.x.x-alpha/beta` - Use only as a development server for testing/debugging the latest features. It is recommended not to use these tags for deployed systems 75 | 76 | ## Deployment 77 | `parse-hipaa` can be easily deployed or tested remote or locally. 78 | 79 | ### Remote 80 | 81 | #### Heroku 82 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 83 | 84 | You can use the one-button-click deployment to quickly deploy to Heroko. **Note that this is non-HIPAA compliant when using Heroku's free services**, so you need to view [Heroku's compliance certifications](https://www.heroku.com/compliance), and upgrade your plans to [Shield Spaces](https://devcenter.heroku.com/articles/heroku-postgres-and-private-spaces). You can [view this document for detailed instructions](https://docs.google.com/document/d/1QDZ65k0DQaq33NdrYuOcC1T8RuCg1irM/edit?usp=sharing&ouid=116811443756382677101&rtpof=true&sd=true). **If you need a Parse Server Heroku deployment for non-ParseCareKit based apps, use the Heroku button on the [snapcat](https://github.com/netreconlab/parse-hipaa/blob/snapcat/README.md#heroku) branch instead of this one.** Once you click the Heroku button do the following: 85 | 86 | 1. Select your **App name** 87 | 2. Under the **Config vars** section, fill in the following environment variables: 88 | - Set the value for `NEW_RELIC_APP_NAME` to the **App name** in step 1 89 | - Add a value for `PARSE_DASHBOARD_USER_ID` so you can log into your Parse Dashboard 90 | - Add the hash of your password as the value for `PARSE_DASHBOARD_USER_PASSWORD` so you can log into your Parse Dashboard. You can get the hash of your desired password from [bcrypt-generator.com](https://bcrypt-generator.com) 91 | - You can leave all other **Config vars** as they are or modify them as needed 92 | 3. If you don't plan on using `parse-hipaa` with `ParseCareKit` you should set `PARSE_SERVER_USING_PARSECAREKIT=false` under **Config vars**. This will ensure that ParseCareKit classes/tables are not created on the parse-hipaa server 93 | 4. Scroll to the bottom of the page and press **Deploy app** 94 | 5. When finished you can access your respective server and dashboard by visiting **https://YOUR_APP_NAME.herokuapp.com/parse** or **https://YOUR_APP_NAME.herokuapp.com/dashboard**. The mount points are based on `PARSE_SERVER_MOUNT_PATH` and `PARSE_DASHBOARD_MOUNT_PATH` 95 | 6. Be sure to go to `Settings->Reveal Config Vars` to get your `PARSE_SERVER_APPLICATION_ID`. Add the `PARSE_SERVER_APPLICATION_ID` and **https://YOUR_APP_NAME.herokuapp.com/parse** as `applicationId` and `serverURL` respectively to your client app to connect your parse-hipaa server 96 | 97 | #### Using your own files for Heroku deployment 98 | 1. Fork the parse-hipaa repo 99 | 2. Edit `heroku.yml` in your repo by changing `parse/Dockerfile.heroku` to `parse/Dockerfile`. This will build from your respective repo instead of using the pre-built docker image 100 | 3. You can now edit `parse/index.js` and `parse/cloud` as you wish 101 | 4. You can then follow the directions on heroku's site for [deployment](https://devcenter.heroku.com/articles/git) and [integration](https://devcenter.heroku.com/articles/github-integration) 102 | 103 | ### Local: Using Docker Image with Postgres or Mongo 104 | By default, the `docker-compose.yml` uses [hipaa-postgres](https://github.com/netreconlab/hipaa-postgres/). The `docker-compose.mongo.yml` uses [hipaa-mongo](https://github.com/netreconlab/hipaa-mongo/). 105 | 106 | #### Postgres 107 | To use the Postgres HIPAA compliant variant of parse-hipaa, simply type: 108 | 109 | ```docker-compose up``` 110 | 111 | #### Mongo 112 | To use the Mongo HIPAA compliant variant of parse-hipaa, simply type: 113 | 114 | ```docker-compose -f docker-compose.mongo.yml up``` 115 | 116 | #### Postgres (Non-HIPAA Compliant) 117 | If you would like to use a non-HIPAA compliant postgres version: 118 | 119 | ```docker-compose -f docker-compose.no.hipaa.yml up``` 120 | 121 | #### Mongo (Non-HIPAA Compliant) 122 | A non-HIPAA compliant mongo version isn't provided as this is the default [parse-server](https://github.com/parse-community/parse-server#inside-a-docker-container) deployment and many examples of how to set this up already exist. 123 | 124 | #### Getting started 125 | parse-hipaa is made up of four (4) seperate docker images (you use 3 of them at a time) that work together as one system. It's important to set the environment variables for your parse-hipaa server. 126 | 127 | ##### Environment Variables 128 | 129 | For a complete list of enviroment variables, look at [app.json](https://github.com/netreconlab/parse-hipaa/blob/main/app.json). 130 | 131 | ###### netreconlab/parse-hipaa 132 | ```bash 133 | PARSE_SERVER_APPLICATION_ID # Unique string value 134 | PARSE_SERVER_PRIMARY_KEY # Unique string value 135 | PARSE_SERVER_READ_ONLY_PRIMARY_KEY # Unique string value 136 | PARSE_SERVER_ENCRYPTION_KEY # Unique string used for encrypting files stored by parse-hipaa 137 | PARSE_SERVER_OBJECT_ID_SIZE # Integer value, parse defaults to 10, 32 is probably better for medical apps and large tables 138 | PARSE_SERVER_DATABASE_URI # URI to connect to parse-hipaa. postgres://${PG_PARSE_USER}:${PG_PARSE_PASSWORD}@db:5432/${PG_PARSE_DB} or mongodb://${MONGO_PARSE_USER}:${MONGO_PARSE_PASSWORD}@db:27017/${MONGO_PARSE_DB} 139 | PORT # Port for parse-hipaa, default is 1337 140 | PARSE_SERVER_MOUNT_PATH: # Mounting path for parse-hipaa, default is /parse 141 | PARSE_SERVER_URL # Server URL, default is http://parse:${PORT}/parse 142 | PARSE_SERVER_PUBLIC_URL # Public Server URL, default is http://localhost:${PORT}/parse 143 | PARSE_SERVER_CLOUD # Path to cloud code, default is /parse/cloud/main.js 144 | PARSE_SERVER_MOUNT_GRAPHQL # Enable graphql, default is 'true' 145 | PARSE_SET_USER_CLP # Set the Class Level Permissios of the _User schema so only authenticated users can access, default 1 146 | PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION # String value of 'false' or 'true'. Prohibits class creation on the client side. Classes can still be created using Parse Dashboard by `useMasterKey`, default 'false' 147 | PARSE_SERVER_ALLOW_CUSTOM_OBJECTID # Required to be true for ParseCareKit 148 | PARSE_SERVER_ENABLE_SCHEMA_HOOKS # Keeps the schema in sync across all instances 149 | PARSE_SERVER_DIRECT_ACCESS # Known to cause crashes when true on single instance of server and not behind public server 150 | PARSE_SERVER_ENABLE_PRIVATE_USERS # Set to 'true' if new users should be created without public read and write access 151 | PARSE_SERVER_USING_PARSECAREKIT # If you are not using ParseCareKit, set this to 'false', or else enable with 'true'. The default value is 'true' 152 | PARSE_VERBOSE # Enable verbose output on the server 153 | POSTGRES_PASSWORD: # Needed for wait-for-postgres.sh. Should be the same as POSTGRES_PASSWORD in netreconlab/hipaa-postgres 154 | ``` 155 | 156 | ###### netreconlab/hipaa-postgres 157 | ```bash 158 | POSTGRES_PASSWORD # Password for postgress db cluster 159 | PG_PARSE_USER # Username for logging into PG_PARSE_DB 160 | PG_PARSE_PASSWORD # Password for logging into PG_PARSE_DB 161 | PG_PARSE_DB # Name of parse-hipaa database 162 | ``` 163 | 164 | ###### netreconlab/hipaa-mongo 165 | ```bash 166 | # Warning, if you want to make changes to the vars below they need to be changed manually in /scripts/mongo-init.js as the env vars are not passed to the script 167 | MONGO_INITDB_ROOT_USERNAME # Username for mongo db. Username for logging into mongo db for parse-hipaa. 168 | MONGO_INITDB_ROOT_PASSWORD # Password for mongo db. Password for logging into mongo db for parse-hipaa. 169 | MONGO_INITDB_DATABASE # Name of mongo db for parse-hipaa 170 | ``` 171 | 172 | ###### netreconlab/parse-hipaa-dashboard 173 | ```bash 174 | PARSE_DASHBOARD_TRUST_PROXY: # Set this to 1 (or anything) if the dashboard is behind a proxy. Otherwise leave empty 175 | PARSE_DASHBOARD_ALLOW_INSECURE_HTTP: # Set this to 1 (or anything) if not behind proxy and using the dashboard in docker. Note that either PARSE_DASHBOARD_ALLOW_INSECURE_HTTP or PARSE_DASHBOARD_TRUST_PROXY should be set at the same time, choose one or the other. Otherwise leave empty 176 | PARSE_DASHBOARD_COOKIE_SESSION_SECRET: # Unique string. This should be constant across all deployments on your system 177 | PARSE_DASHBOARD_MOUNT_PATH: # The default is "/dashboard". This needs to be exactly what you plan it to be behind the proxy, i.e. If you want to access cs.uky.edu/dashboard it should be "/dashboard" 178 | ``` 179 | 180 | ###### parseplatform/parse-dashboard 181 | ```bash 182 | PARSE_DASHBOARD_TRUST_PROXY: # Set this to 1 (or anything) if the dashboard is behind a proxy. Otherwise leave empty 183 | PARSE_DASHBOARD_ALLOW_INSECURE_HTTP: # Set this to 1 (or anything) if not behind proxy and using the dashboard in docker. Note that either PARSE_DASHBOARD_ALLOW_INSECURE_HTTP or PARSE_DASHBOARD_TRUST_PROXY should be set at the same time, choose one or the other. Otherwise leave empty 184 | PARSE_DASHBOARD_COOKIE_SESSION_SECRET: # Unique string. This should be constant across all deployments on your system 185 | MOUNT_PATH: # The default is "/dashboard". This needs to be exactly what you plan it to be behind the proxy, i.e. If you want to access cs.uky.edu/dashboard it should be "/dashboard" 186 | ``` 187 | 188 | ##### Starting up parse-hipaa 189 | 190 | - For the default HIPAA compliant postgres version: ```docker-compose up``` 191 | - or for the HIPAA compliant mongo version: ```docker-compose -f docker-compose.mongo.yml up``` 192 | - or for the non-HIPAA compliant postgres version: ```docker-compose -f docker-compose.no.hipaa.yml up``` 193 | - A non-HIPAA compliant mongo version isn't provided in this repo as that's just a standard parse-server 194 | 195 | Imporant Note: On the very first run, the "parse-server"(which will show up as "parse_1" in the console) will sleep and error a few times because it can't connect to postgres (the "db") container. This is suppose to happen and is due to postgres needing to configure and initialize, install the necessary extensions, and setup it's databases. Let it keep running and eventually you will see something like: 196 | 197 | ```bash 198 | db_1 | PostgreSQL init process complete; ready for start up. 199 | ``` 200 | 201 | The parse-server container will automatically keep attempting to connect to the postgres container and when it's connected you will see: 202 | 203 | ```bash 204 | parse_1 | parse-server running on port 1337. 205 | parse_1 | publicServerURL: http://localhost:1337/parse, serverURL: http://parse:1337/parse 206 | parse_1 | GraphQL API running on http://localhost:1337/parsegraphql 207 | parse_1 | info: Parse LiveQuery Server starts running 208 | ``` 209 | 210 | You may also see output such as the following in the console or log files: 211 | 212 | ```bash 213 | db_1 | 2020-03-18 21:59:21.550 UTC [105] ERROR: duplicate key value violates unique constraint "pg_type_typname_nsp_index" 214 | db_1 | 2020-03-18 21:59:21.550 UTC [105] DETAIL: Key (typname, typnamespace)=(_SCHEMA, 2200) already exists. 215 | db_1 | 2020-03-18 21:59:21.550 UTC [105] STATEMENT: CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") ) 216 | db_1 | 2020-03-18 21:59:21.550 UTC [106] ERROR: duplicate key value violates unique constraint "pg_type_typname_nsp_index" 217 | ... 218 | ``` 219 | 220 | The lines above are console output from parse because they attempt to check and configure the postgres database if necessary. They doesn't hurt or slow down your parse-hipaa server. 221 | 222 | ### Local: Using Singularity Image with Postgres 223 | There are equivalent [Singularity](https://sylabs.io/singularity/) images that can be configured in a similar fashion to Docker. The singularity images are hosted on GitHub Container Registry and can be found [here](https://github.com/netreconlab/parse-hipaa/pkgs/container/parse-hipaa). An example of of how to use this image can be found in [singularity-compose.yml](https://github.com/netreconlab/parse-hipaa/blob/main/singularity-compose.yml). 224 | 225 | ## Parse Server 226 | Your parse-server is binded to all of your interfaces on port 1337/parse and be can be accessed as such, e.g. `http://localhost:1337/parse`. 227 | 228 | The standard configuration can be modified to your liking by editing [index.js](https://github.com/netreconlab/parse-hipaa/blob/main/index.js). Here you can add/modify things like push notifications, password resets, [adapters](https://github.com/parse-community/parse-server#available-adapters), etc. This file as an express app and some examples provided from parse can be found [here](https://github.com/parse-community/parse-server#using-expressjs). Note that there is no need to rebuild your image when modifying files in the "index.js" file since it is volume mounted, but you will need to restart the parse container for your changes to take effect. 229 | 230 | ### Configuring 231 | Default values for environment variables: `PARSE_SERVER_APPLICATION_ID` and `PARSE_SERVER_PRIMARY_KEY` are provided in [docker-compose.yml](https://github.com/netreconlab/parse-hipaa/blob/main/docker-compose.yml) for quick local deployment. If you plan on using this image to deploy in production, you should definitely change both values. Environment variables, `PARSE_SERVER_DATABASE_URI, PARSE_SERVER_URL, PORT, PARSE_SERVER_PUBLIC_URL, PARSE_SERVER_CLOUD, and PARSE_SERVER_MOUNT_GRAPHQL` should not be changed unles you are confident with configuring parse-server or else you image may not work properly. In particular, changing `PORT` should only be done in [.env](https://github.com/netreconlab/parse-hipaa/blob/main/.env) and will also require you to change the port manually in the [parse-dashboard-config.json](https://github.com/netreconlab/parse-hipaa/blob/main/parse/parse-dashboard-config.json#L4) for both "serverURL" and "graphQLServerURL" to have the Parse Dashboard work correctly. 232 | 233 | #### Running in production for ParseCareKit 234 | If you are plan on using parse-hipaa in production. You should run the additional scripts to create the rest of the indexes for optimized queries. 235 | 236 | ##### Postgres 237 | If you are using `hipaa_postgres`, the `setup-parse-index.sh` is already in the container. You just have to run it. 238 | 239 | 1. Log into your docker container, type: ```docker exec -u postgres -ti parse-hipaa_db_1 bash``` 240 | 2. Run the script, type: ```./usr/local/bin/setup-parse-index.h``` 241 | 242 | If you are using your own postgres image, you should copy [setup-parse-index.sh](https://github.com/netreconlab/hipaa-postgres/blob/main/scripts/setup-parse-index.sh) to your container and complete the login and run steps above (be sure to switch `parse-hipaa_db_1` to your actual running container name on your system). 243 | 244 | More information about configuring can be found on [hipaa-postgres](https://github.com/netreconlab/hipaa-postgres#configuring). 245 | 246 | ###### Idempotency 247 | You most likely want to enable Idempotency. Read more about how to configure on [Parse Server](https://github.com/parse-community/parse-server#idempotency-enforcement). For Postgres, you can setup a [cron](https://en.wikipedia.org/wiki/Cron) or scheduler to run [parse_idempotency_delete_expired_records.sh](https://github.com/netreconlab/parse-hipaa/blob/main/parse/scripts/parse_idempotency_delete_expired_records.sh) at a desired frequency to remove stale data. 248 | 249 | ##### Mongo 250 | Information about configuring can be found on [hipaa-mongo](https://github.com/netreconlab/hipaa-mongo). 251 | 252 | ###### Idempotency 253 | You most likely want to enable Idempotency. Read more about how to configure on [Parse Server](https://github.com/parse-community/parse-server#idempotency-enforcement). For Postgres, you can setup a [cron](https://en.wikipedia.org/wiki/Cron) or scheduler to run [parse_idempotency_delete_expired_records.sh](https://github.com/netreconlab/parse-hipaa/blob/main/parse/scripts/parse_idempotency_delete_expired_records.sh) at a desired frequency to remove stale data. 254 | 255 | #### Cloud Code 256 | For verfying and cleaning your data along with other added functionality, you can add [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/) to the [cloud](https://github.com/netreconlab/parse-hipaa/tree/main/parse/cloud) folder. Note that there is no need to rebuild your image when modifying files in the "cloud" folder since this is volume mounted, but you may need to restart the parse container for your changes to take effect. 257 | 258 | ## Viewing Your Data via Parse Dashboard 259 | 260 | ### Dashboard on Heroku 261 | Follow the directions in the [parse-hipaa-dashboard](https://github.com/netreconlab/parse-hipaa-dashboard#remote) repo for one-button deployment of dashboard. 262 | 263 | ### Local (Docker or Singularity) 264 | 265 | #### parseplatform/parse-dashboard (docker-compose.yml, docker-compose.no.hipaa.yml, docker-compose.mongo.yml) 266 | Parse-Dashboard is binded to your `localhost` on port `4040` and can be accessed as such, e.g. http://localhost:4040/dashboard. The default login for the parse dashboard is username: "parse", password: "1234". For production you should change the usernames and passwords in the [postgres-dashboard-config.json](https://github.com/netreconlab/parse-hipaa/blob/main/parse/parse-dashboard-config.json#L13-L21). Note that the password is hashed by using [bcrypt-generator](https://bcrypt-generator.com) or similar. Authentication can also occur through [multi factor authentication](https://github.com/parse-community/parse-dashboard#multi-factor-authentication-one-time-password). 267 | 268 | #### netreconlab/parse-hipaa-dashboard (docker-compose.dashboard.yml and docker-compose.mongo.dashboard.yml) 269 | Parse-Hipaa-Dashboard is binded to your `localhost` on port `1337`, mounted to the `/dashboard` endpoint, and can be accessed as such, e.g. http://localhost:1337/dashboard. The default login for the parse dashboard is username: "parse", password: "1234". For production you should change the usernames and passwords in the [docker-compose.yml](https://github.com/netreconlab/parse-hipaa/blob/37f79bdb99781b634780b3af6a7e33e6beae44a0/docker-compose.yml#L30-L32) along with setting `PARSE_DASHBOARD_USER_PASSWORD_ENCRYPTED: 'true'`. Note that the password should be hashed using a [bcrypt-generator](https://bcrypt-generator.com) or similar. Authentication can also occur through [multi factor authentication](https://github.com/parse-community/parse-dashboard#multi-factor-authentication-one-time-password). 270 | 271 | 1. Open your browser and and depending on how your dashboard is mounted, go to http://localhost:4040/dashboard or http://localhost:1337/dashboard 272 | 2. Username: `parse` # You can use `parseRead` to login as a read only user 273 | 3. Password: `1234` 274 | 4. Be sure to refresh your browser to see new changes synched from your CareKitSample app 275 | 276 | #### Configuring 277 | If you plan on using this image to deploy in production, it is recommended to run this behind a proxy and add the environment variable `PARSE_DASHBOARD_TRUST_PROXY=1` to the dashboard container. 278 | 279 | ## Postgres 280 | The image used is [postgis](https://hub.docker.com/r/postgis/postgis) which is an extention built on top of the [official postgres image](https://hub.docker.com/_/postgres). Note that postgres is not binded to your interfaces and is only local to the docker virtual network. This was done on purpose as the parse and parse-desktop is already exposed. 281 | 282 | If you want to persist the data in the database, you can uncomment the volume lines in [docker-compose](https://github.com/netreconlab/parse-hipaa/blob/main/docker-compose.yml#L41) 283 | 284 | ### Configuring 285 | Default values for environment variables: `POSTGRES_PASSWORD, PG_PARSE_USER, PG_PARSE_PASSWORD, PG_PARSE_DB` are provided in [docker-compose.yml](https://github.com/netreconlab/parse-hipaa/blob/main/docker-compose.yml) for quick local deployment. If you plan on using this image to deploy in production, you should definitely change `POSTGRES_PASSWORD, PG_PARSE_USER, PG_PARSE_PASSWORD`. Note that the postgres image provides a default user of "postgres" to configure the database cluster, you can change the password for the "postgres" user by changing `POSTGRES_PASSWORD`. There are plenty of [postgres environment variables](https://hub.docker.com/_/postgres) that can be modified. Environment variables should not be changed unles you are confident with configuring postgres or else you image may not work properly. Note that changes to the aforementioned paramaters will only take effect if you do them before the first build and run of the image. Afterwards, you will need to make all changes by connecting to the image typing: 286 | 287 | ```docker exec -u postgres -ti parse-hipaa_db_1 bash``` 288 | 289 | You can then make modifications using [psql](http://postgresguide.com/utilities/psql.html). Through psql, you can also add multiple databases and users to support a number of parse apps. Note that you will also need to add the respecting parse-server containers (copy parse container in the .yml and rename to your new app) along with the added app in [postgres-dashboard-config.json](https://github.com/netreconlab/parse-hipaa/blob/main/parse/parse-dashboard-config.json). 290 | 291 | ## Deploying on a real system 292 | The docker yml's here are intended to run behind a proxy that properly has ssl configured to encrypt data in transit. To create a proxy to parse-hipaa, nginx files are provided [here](https://github.com/netreconlab/parse-hipaa/tree/main/nginx/sites-enabled). Simply add the [sites-available](https://github.com/netreconlab/parse-hipaa/tree/main/nginx/sites-enabled) folder to your nginx directory and add the following to "http" in your nginx.conf: 293 | 294 | ```bash 295 | http { 296 | include /usr/local/etc/nginx/sites-enabled/*.conf; # Add this line to end. This is for macOS, do whatever is appropriate on your system 297 | } 298 | ``` 299 | 300 | Setup your free certificates using [LetsEncrypt](https://letsencrypt.org), follow the directions [here](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/). Be sure to change the certificate and key lines to point to correct location in [default-ssl.conf](https://github.com/netreconlab/parse-hipaa/blob/main/nginx/sites-enabled/default-ssl.conf). 301 | 302 | ## Is there a mongo version available? 303 | The mongo equivalent is available in this repo. The same steps as above. but use: 304 | 305 | ```docker-compose -f docker-compose.mongo.yml up``` 306 | -------------------------------------------------------------------------------- /Singularity: -------------------------------------------------------------------------------- 1 | Bootstrap: docker 2 | From: netreconlab/parse-hipaa:latest 3 | 4 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Parse HIPAA Server", 3 | "description": "A Parse API server designed to support ParseCareKit based apps.", 4 | "repository": "https://github.com/netreconlab/parse-hipaa", 5 | "logo": "https://avatars0.githubusercontent.com/u/1294580?v=3&s=200", 6 | "keywords": ["node", "express", "parse", "parse-server", "carekit", "hipaa", "postgres"], 7 | "env": { 8 | "NEW_RELIC_APP_NAME": { 9 | "description": "Name of your Heroku App. Should be the same as \"App name\"", 10 | "required": true 11 | }, 12 | "CLUSTER_INSTANCES": { 13 | "description": "The amount of pm2 clusters to deploy, defaults to 4. Currently doesn't work on Heroku, change instances in ecosystem.config.js instead", 14 | "value": "4" 15 | }, 16 | "NODE_TLS_REJECT_UNAUTHORIZED": { 17 | "description": "Enable (or disable) rejecting unauthorized certificates (needs to be 0 for redis), defaults to 1.", 18 | "value": "1" 19 | }, 20 | "PARSE_DASHBOARD_START": { 21 | "description": "Starts the dashboard, default true.", 22 | "value": "true" 23 | }, 24 | "PARSE_DASHBOARD_USERNAMES": { 25 | "description": "Specify the usernames to connect to the dashboard. Multiple usernames should be a list: username1, username2, etc. The list should be the same size as PARSE_DASHBOARD_USER_PASSWORDS", 26 | "required": true 27 | }, 28 | "PARSE_DASHBOARD_USER_PASSWORDS": { 29 | "description": "Specify the user passwords to connect to the dashboard. Multiple passwords should be a list: password1, pasword2, etc. The list should be the same size as PARSE_DASHBOARD_USERNAMES. This should be a hash of the password if PARSE_DASHBOARD_USER_PASSWORD_ENCRYPTED=true. Can use a hasher for generating, e.g. https://bcrypt-generator.com.", 30 | "required": true 31 | }, 32 | "PARSE_DASHBOARD_USER_PASSWORD_ENCRYPTED": { 33 | "description": "Specify if the user parseword should be encrypted (true) or in plaintext (false).", 34 | "value": "true" 35 | }, 36 | "PARSE_DASHBOARD_COOKIE_SESSION_SECRET": { 37 | "description": "The constant cookie session for dashboard.", 38 | "generator": "secret" 39 | }, 40 | "PARSE_DASHBOARD_CONFIG": { 41 | "description": "The config file for dashboard.", 42 | "required": false 43 | }, 44 | "PARSE_DASHBOARD_ALLOW_INSECURE_HTTP": { 45 | "description": "Specify if insecure http should be allowed.", 46 | "value": "0" 47 | }, 48 | "PARSE_DASHBOARD_TRUST_PROXY": { 49 | "description": "Specify the trust proxy setting.", 50 | "required": false 51 | }, 52 | "PARSE_DASHBOARD_MOUNT_PATH": { 53 | "description": "The mount path for the dashboard.", 54 | "value": "/dashboard" 55 | }, 56 | "PARSE_SERVER_ENABLE": { 57 | "description": "Enable the parse server.", 58 | "value": "true" 59 | }, 60 | "PARSE_SERVER_TRUST_PROXY": { 61 | "description": "The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the 'https://expressjs.com/en/guide/behind-proxies.html', express trust proxy settings documentation. Defaults to `true`.", 62 | "value": "true" 63 | }, 64 | "PARSE_SERVER_APPLICATION_ID": { 65 | "description": "A unique identifier for your app.", 66 | "generator": "secret" 67 | }, 68 | "PARSE_SERVER_MAINTENANCE_KEY": { 69 | "description": "The maintenance key is used for modifying internal and read-only fields of Parse Server.

\u26A0\uFE0F This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server", 70 | "generator": "secret" 71 | }, 72 | "PARSE_SERVER_MAINTENANCE_KEY_IPS": { 73 | "description": "Restricts the use of maintenance key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the maintenance key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the maintenance key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the maintenance key", 74 | "value": "0.0.0.0/0, ::1" 75 | }, 76 | "PARSE_SERVER_PRIMARY_KEY": { 77 | "description": "A key that overrides all permissions. Keep this secret. This is the masterKey on the parse-server", 78 | "generator": "secret" 79 | }, 80 | "PARSE_SERVER_READ_ONLY_PRIMARY_KEY": { 81 | "description": "A key that allows read only access. Keep this secret. This is the read-only masterKey on the parse-server", 82 | "generator": "secret" 83 | }, 84 | "PARSE_SERVER_PRIMARY_KEY_IPS": { 85 | "description": "Restricts the use of primary key permissions to a list of IP addresses. This option accepts a list of single IP addresses, for example: 10.0.0.1, 10.0.0.2. This is the masterKeyIps on the parse-server", 86 | "value": "0.0.0.0/0, ::1" 87 | }, 88 | "PARSE_SERVER_URL": { 89 | "description": "Specify URL to connect to your Heroku instance if not https://yourappname.herokuapp.com/parse (update with your app's name + PARSE_SERVER_MOUNT_PATH)", 90 | "required": false 91 | }, 92 | "PARSE_SERVER_WEBHOOK_KEY": { 93 | "description": "Key sent with outgoing webhook calls. Keep this secret.", 94 | "generator": "secret" 95 | }, 96 | "PARSE_SERVER_PUSH": { 97 | "description": "Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications.", 98 | "required": false 99 | }, 100 | "PARSE_SERVER_AUTH_PROVIDERS": { 101 | "description": "Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication.", 102 | "required": false 103 | }, 104 | "PARSE_SERVER_ENABLE_ANON_USERS": { 105 | "description": "Enable (or disable) anonymous users, defaults to true.", 106 | "value": "true" 107 | }, 108 | "PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_PUBLIC": { 109 | "description": "Is true if file upload should be allowed for anyone, regardless of user authentication.", 110 | "value": "false" 111 | }, 112 | "PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_ANONYMOUS_USER": { 113 | "description": "Is true if file upload should be allowed for anonymous users.", 114 | "value": "true" 115 | }, 116 | "PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_AUTHENTICATED_USER": { 117 | "description": "Is true if file upload should be allowed for authenticated users.", 118 | "value": "true" 119 | }, 120 | "PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS": { 121 | "description": "Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern. It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage. Defaults to `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files.", 122 | "value": "^[^hH][^tT][^mM][^lL]?$" 123 | }, 124 | "PARSE_SERVER_MAX_UPLOAD_SIZE": { 125 | "description": "Max file size for uploads, defaults to 20mb.", 126 | "value": "20mb" 127 | }, 128 | "PARSE_SERVER_S3_BUCKET": { 129 | "description": "The name of your S3 Bucket.", 130 | "required": false 131 | }, 132 | "PARSE_SERVER_S3_BUCKET_REGION": { 133 | "description": "The region for the S3 Bucket.", 134 | "value": "us-east-2" 135 | }, 136 | "PARSE_SERVER_S3_BUCKET_ENCRYPTION": { 137 | "description": "AES256 or aws:kms, or if you do not pass this, encryption won't be done.", 138 | "value": "AES256" 139 | }, 140 | "AWS_ACCESS_KEY_ID": { 141 | "description": "The access key to your S3 Bucket.", 142 | "required": false 143 | }, 144 | "AWS_SECRET_ACCESS_KEY": { 145 | "description": "The secret access key to your S3 Bucket.", 146 | "required": false 147 | }, 148 | "PARSE_SERVER_MOUNT_PATH": { 149 | "description": "Mount path for the server, defaults to /parse.", 150 | "value": "/parse" 151 | }, 152 | "PARSE_SERVER_GRAPHQL_PATH": { 153 | "description": "Mount path for the GraphQL endpoint, defaults to /graphql.", 154 | "value": "/graphql" 155 | }, 156 | "PARSE_SERVER_ENCRYPTION_KEY": { 157 | "description": "Unique string used for encrypting files stored by parse-hipaa.", 158 | "generator": "secret" 159 | }, 160 | "PARSE_SERVER_OBJECT_ID_SIZE": { 161 | "description": "Integer value, parse defaults to 10, 32 is probably better for medical apps and large tables.", 162 | "value": "32" 163 | }, 164 | "PARSE_SERVER_START_LIVE_QUERY_SERVER": { 165 | "description": "Starts the liveQuery server, default true.", 166 | "value": "true" 167 | }, 168 | "PARSE_SERVER_START_LIVE_QUERY_SERVER_NO_PARSE": { 169 | "description": "Starts the liveQuery server as a standalone server, default false.", 170 | "value": "false" 171 | }, 172 | "PARSE_SERVER_RATE_LIMIT": { 173 | "description": "Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Server as a whole from denial-of-service (DoS) attacks. Mind the following limitations: rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses; if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable; this feature provides basic protection against DoS attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.", 174 | "value": "false" 175 | }, 176 | "PARSE_SERVER_RATE_LIMIT_ERROR_RESPONSE_MESSAGE": { 177 | "description": "The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is `Too many requests.`", 178 | "value": "Too many requests." 179 | }, 180 | "PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS": { 181 | "description": "If `true` the rate limit will also apply to requests that are made in by Cloud Code, default is `false`. Note that a public Cloud Code function that triggers internal requests may circumvent rate limiting and be vulnerable to attacks.", 182 | "value": "false" 183 | }, 184 | "PARSE_SERVER_RATE_LIMIT_INCLUDE_PRIMARY_KEY": { 185 | "description": "If `true` the rate limit will also apply to requests using the `primaryKey`, default is `false`. Note that a public Cloud Code function that triggers internal requests using the `primaryKey` may circumvent rate limiting and be vulnerable to attacks.", 186 | "value": "false" 187 | }, 188 | "PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT": { 189 | "description": "The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied. Defaults to 100.", 190 | "value": "100" 191 | }, 192 | "PARSE_SERVER_RATE_LIMIT_REQUEST_TIME_WINDOW": { 193 | "description": "The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied. Defaults to 600000 or 10 minutes.", 194 | "value": "600000" 195 | }, 196 | "PARSE_SERVER_RATE_LIMIT_REQUEST_METHODS": { 197 | "description": "A list of HTTP request methods to which the rate limit should be applied, default is all methods.", 198 | "required": false 199 | }, 200 | "PARSE_SERVER_RATE_LIMIT_REQUEST_PATH": { 201 | "description": "The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expression. See: https://expressjs.com/en/guide/routing.html'. Defaults to '*' which is all paths.", 202 | "value": "*" 203 | }, 204 | "PORT": { 205 | "description": "Port for parse-hipaa, default is 1337.", 206 | "value": "1337" 207 | }, 208 | "PARSE_SERVER_CLOUD": { 209 | "description": "Path to cloud code, default is /parse/cloud/main.js.", 210 | "value": "/parse-server/cloud/main.js" 211 | }, 212 | "PARSE_SERVER_REDIS_URL": { 213 | "description": "Redis cache url. For details, see: https://github.com/redis/node-redis/blob/master/docs/client-configuration.md.", 214 | "required": false 215 | }, 216 | "PARSE_SERVER_CACHE_MAX_SIZE": { 217 | "description": "Sets the maximum size for the in memory cache, defaults to 10000.", 218 | "value": "10000" 219 | }, 220 | "PARSE_SERVER_CACHE_TTL": { 221 | "description": "Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds).", 222 | "value": "5000" 223 | }, 224 | "PARSE_SERVER_MOUNT_GRAPHQL": { 225 | "description": "Enable graphql, default is 'false'.", 226 | "value": "false" 227 | }, 228 | "PARSE_SERVER_GRAPH_QLSCHEMA": { 229 | "description": "Full path to your GraphQL custom schema.graphql file.", 230 | "required": false 231 | }, 232 | "PARSE_SERVER_MOUNT_PLAYGROUND": { 233 | "description": "Mounts the GraphQL Playground - never use this option in production. Default is 'false'.", 234 | "value": "false" 235 | }, 236 | "PARSE_SERVER_PLAYGROUND_PATH": { 237 | "description": "Mount path for the GraphQL Playground, defaults to /playground", 238 | "value": "/playground" 239 | }, 240 | "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION": { 241 | "description": "Don't allow classes to be created on the client side..", 242 | "value": "false" 243 | }, 244 | "PARSE_SERVER_ALLOW_CUSTOM_OBJECTID": { 245 | "description": "Required to be true for ParseCareKit.", 246 | "value": "true" 247 | }, 248 | "PARSE_SERVER_DATABASE_ENABLE_SCHEMA_HOOKS": { 249 | "description": "Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart.", 250 | "value": "true" 251 | }, 252 | "PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION": { 253 | "description": "If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`. The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.", 254 | "value": "true" 255 | }, 256 | "PARSE_SERVER_PAGES_ENABLE_ROUTER": { 257 | "description": "Is true if the pages router should be enabled; this is required for any of the pages options to take effect.", 258 | "value": "true" 259 | }, 260 | "PARSE_SERVER_DIRECT_ACCESS": { 261 | "description": "WARNING: Setting to 'true' is known to cause crashes on parse-hipaa running postgres.", 262 | "value": "false" 263 | }, 264 | "PARSE_SERVER_ENFORCE_PRIVATE_USERS": { 265 | "description": "Set to 'true' if new users should be created without public read and write access.", 266 | "value": "true" 267 | }, 268 | "PARSE_SERVER_ENABLE_IDEMPOTENCY": { 269 | "description": "Enable idempotency on all requests. defaults to false.", 270 | "value": "false" 271 | }, 272 | "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS": { 273 | "description": "A list of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.", 274 | "value": ".*" 275 | }, 276 | "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL": { 277 | "description": "The duration in seconds after which a request record is discarded from the database.", 278 | "value": "300" 279 | }, 280 | "PARSE_SERVER_USING_PARSECAREKIT": { 281 | "description": "Set to 'true' when your app is designed for ParseCareKit. Otherwise set to 'false' to use as a general Parse Server.", 282 | "value": "true" 283 | }, 284 | "PARSE_SERVER_USING_PARSECAREKIT_AUDIT": { 285 | "description": "Set to 'true' to audit ParseCareKit tables. Note that auditing takes up more space in your database.", 286 | "value": "false" 287 | }, 288 | "PARSE_VERBOSE": { 289 | "description": "Enable verbose output on the server.", 290 | "value": "false" 291 | }, 292 | "PARSE_SERVER_ALLOW_EXPIRED_AUTH_DATA_TOKEN": { 293 | "description": "Allow a user to log in even if the 3rd party authentication token that was used to sign in to their account has expired. If this is set to false, then the token will be validated every time the user signs in to their account. This refers to the token that is stored in the _User.authData field.", 294 | "value": "true" 295 | }, 296 | "PARSE_SERVER_ALLOW_HEADERS": { 297 | "description": "Add headers to Access-Control-Allow-Headers.", 298 | "required": false 299 | }, 300 | "PARSE_SERVER_ALLOW_ORIGIN": { 301 | "description": "Sets the origin to Access-Control-Allow-Origin.", 302 | "required": false 303 | }, 304 | "PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID": { 305 | "description": "Set to true if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.", 306 | "value": "false" 307 | }, 308 | "PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS": { 309 | "description": "Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.", 310 | "value": "true" 311 | }, 312 | "PARSE_SERVER_HOST": { 313 | "description": "The host to serve ParseServer on.", 314 | "value": "0.0.0.0" 315 | }, 316 | "JSON_LOGS": { 317 | "description": "Log as structured JSON objects.", 318 | "value": "false" 319 | }, 320 | "PARSE_SERVER_LOGS_FOLDER": { 321 | "description": "Folder for the logs (defaults to './logs'); set to null to disable file based logging.", 322 | "value": "./logs" 323 | }, 324 | "PARSE_SERVER_MAX_LIMIT": { 325 | "description": "Max value for limit option on queries, defaults to unlimited.", 326 | "required": false 327 | }, 328 | "PARSE_SERVER_PRESERVE_FILE_NAME": { 329 | "description": "Enable (or disable) the addition of a unique hash to the file names.", 330 | "value": "false" 331 | }, 332 | "PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET": { 333 | "description": "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.", 334 | "value": "true" 335 | }, 336 | "PARSE_SERVER_SESSION_LENGTH": { 337 | "description": "Session duration, in seconds, defaults to 1 year.", 338 | "value": "31536000" 339 | }, 340 | "PARSE_SERVER_VERIFY_USER_EMAILS": { 341 | "description": "Set to true to require users to verify their email address to complete the sign-up process.", 342 | "value": "false" 343 | }, 344 | "PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION": { 345 | "description": "Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. If the option is not set or set to `undefined`, then the token never expires. For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).", 346 | "value": "86400" 347 | }, 348 | "PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL": { 349 | "description": "Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required. Requires option verifyUserEmails: true", 350 | "value": "false" 351 | }, 352 | "PARSE_SERVER_LIVEQUERY_CLASSNAMES": { 353 | "description": "parse-server's LiveQuery classNames. Example: Clock, Contact.", 354 | "value": "Clock, RevisionRecord" 355 | }, 356 | "PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT": { 357 | "description": "Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients.", 358 | "value": "10000" 359 | }, 360 | "PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT": { 361 | "description": "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details.", 362 | "value": "5000" 363 | }, 364 | "PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION": { 365 | "description": "Set the duration in minutes that a locked-out account remains locked out before automatically becoming unlocked. Valid values are greater than 0 and less than 100000.", 366 | "value": "5" 367 | }, 368 | "PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD": { 369 | "description": "Set the number of failed sign-in attempts that will cause a user account to be locked. If the account is locked. The account will unlock after the duration set in the `duration` option has passed and no further login attempts have been made. Valid values are greater than 0 and less than 1000.", 370 | "value": "3" 371 | }, 372 | "PARSE_SERVER_ACCOUNT_LOCKOUT_UNLOCK_ON_PASSWORD_RESET": { 373 | "description": "Set to true if the account should be unlocked after a successful password reset. Requires options duration and threshold to be set.", 374 | "value": "false" 375 | }, 376 | "PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME": { 377 | "description": "Set to true to disallow the username as part of the password.", 378 | "value": "true" 379 | }, 380 | "PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE": { 381 | "description": "Set the number of days after which a password expires. Login attempts fail if the user does not reset the password before expiration.", 382 | "required": false 383 | }, 384 | "PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY": { 385 | "description": "Set the number of previous password that will not be allowed to be set as new password. If the option is not set or set to 0, no previous passwords will be considered. Valid values are >= 0 and <= 20.", 386 | "value": "5" 387 | }, 388 | "PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID": { 389 | "description": "Set to true if a password reset token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.", 390 | "value": "false" 391 | }, 392 | "PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION": { 393 | "description": "Set the validity duration of the password reset token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires. For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).", 394 | "value": "86400" 395 | }, 396 | "PARSE_SERVER_PASSWORD_POLICY_VALIDATION_ERROR": { 397 | "description": "Set the error message to be sent.", 398 | "value": "Password must have at least 8 characters, contain at least 1 digit, 1 lower case, 1 upper case, and contain at least one special character." 399 | }, 400 | "PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN": { 401 | "description": "Set the regular expression validation pattern a password must match to be accepted. Defaults to enforcing passwords to have atleast: 8 chars with at least 1 lower case, 1 upper case and 1 digit", 402 | "required": false 403 | }, 404 | "PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER": { 405 | "description": "Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterSaveFile`, `afterDeleteFile`, `afterFind`, `afterLogout`.", 406 | "value": "info" 407 | }, 408 | "PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR": { 409 | "description": "Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on error.", 410 | "value": "error" 411 | }, 412 | "PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS": { 413 | "description": "Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on success.", 414 | "value": "info" 415 | } 416 | }, 417 | "stack": "container", 418 | "formation": { 419 | "web": { 420 | "quantity": 1, 421 | "size": "basic" 422 | } 423 | }, 424 | "addons": [ 425 | { 426 | "plan": "heroku-postgresql:essential-0", 427 | "as": "db" 428 | }, 429 | { 430 | "plan": "scheduler" 431 | }, 432 | { 433 | "plan": "newrelic" 434 | }, 435 | { 436 | "plan": "papertrail" 437 | } 438 | ] 439 | } 440 | -------------------------------------------------------------------------------- /dashboard.env: -------------------------------------------------------------------------------- 1 | export PARSE_DASHBOARD_ALLOW_INSECURE_HTTP=1 2 | export PARSE_DASHBOARD_COOKIE_SESSION_SECRET=AB8849B6-D725-4A75-AA73-AB7103F0363F # This should be constant across all deployments on your system 3 | export PARSE_DASHBOARD_MOUNT_PATH=/dashboard 4 | -------------------------------------------------------------------------------- /docker-compose.dashboard.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | parse: 5 | image: netreconlab/parse-hipaa:6.0.0-dashboard 6 | environment: 7 | CLUSTER_INSTANCES: 1 8 | PARSE_SERVER_APPLICATION_ID: E036A0C5-6829-4B40-9B3B-3E05F6DF32B2 9 | PARSE_SERVER_MAINTENANCE_KEY: 785F83D9-9E56-4BA6-91FE-6A9CA9674738 10 | PARSE_SERVER_PRIMARY_KEY: E2466756-93CF-4C05-BA44-FF5D9C34E99F 11 | PARSE_SERVER_READ_ONLY_PRIMARY_KEY: 367F7395-2E3A-46B1-ABA3-963A25D533C3 12 | PARSE_SERVER_WEBHOOK_KEY: 553D229E-64DF-4928-99F5-B71CCA94A44A 13 | PARSE_SERVER_ENCRYPTION_KEY: 72F8F23D-FDDB-4792-94AE-72897F0688F9 14 | PARSE_SERVER_TRUST_PROXY: 'true' 15 | PARSE_SERVER_OBJECT_ID_SIZE: 32 16 | PARSE_SERVER_DATABASE_URI: postgres://${PG_PARSE_USER}:${PG_PARSE_PASSWORD}@db:${DB_PORT}/${PG_PARSE_DB} 17 | PORT: ${PORT} 18 | PARSE_SERVER_MOUNT_PATH: ${MOUNT_PATH} 19 | PARSE_SERVER_URL: http://parse:${PORT}${MOUNT_PATH} 20 | PARSE_SERVER_PUBLIC_URL: http://localhost:${PORT}${MOUNT_PATH} 21 | PARSE_SERVER_CLOUD: /parse-server/cloud/main.js 22 | PARSE_SERVER_MOUNT_GRAPHQL: 'false' 23 | PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION: 'false' # Don't allow classes to be created on the client side. You can create classes by using ParseDashboard instead 24 | PARSE_SERVER_ALLOW_CUSTOM_OBJECTID: 'true' # Required to be true for ParseCareKit 25 | PARSE_SERVER_ENABLE_SCHEMA_HOOKS: 'true' 26 | PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION: 'true' 27 | PARSE_SERVER_PAGES_ENABLE_ROUTER: 'true' 28 | PARSE_SERVER_DIRECT_ACCESS: 'false' # WARNING: Setting to 'true' is known to cause crashes on parse-hipaa running postgres 29 | PARSE_SERVER_ENABLE_PRIVATE_USERS: 'true' 30 | PARSE_SERVER_USING_PARSECAREKIT: 'true' # If you are not using ParseCareKit, set this to 'false' 31 | PARSE_SERVER_RATE_LIMIT: 'false' 32 | PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT: '100' 33 | PARSE_SERVER_RATE_LIMIT_INCLUDE_PRIMARY_KEY: 'false' 34 | PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS: 'false' 35 | PARSE_DASHBOARD_START: 'true' 36 | PARSE_DASHBOARD_APP_NAME: Parse HIPAA 37 | PARSE_DASHBOARD_SERVER_URL: http://localhost:${PORT}${MOUNT_PATH} 38 | PARSE_DASHBOARD_USERNAMES: parse, parseRead 39 | PARSE_DASHBOARD_USER_PASSWORDS: 1234, 1234 40 | PARSE_DASHBOARD_USER_PASSWORD_ENCRYPTED: 'false' 41 | PARSE_DASHBOARD_ALLOW_INSECURE_HTTP: 1 42 | PARSE_DASHBOARD_COOKIE_SESSION_SECRET: AB8849B6-D725-4A75-AA73-AB7103F0363F # This should be constant across all deployments on your system 43 | PARSE_DASHBOARD_MOUNT_PATH: /dashboard # This needs to be exactly what you plan it to be behind the proxy, i.e. If you want to access cs.uky.edu/dashboard it should be "/dashboard" 44 | PARSE_VERBOSE: 'false' 45 | POSTGRES_USER: ${POSTGRES_USER} 46 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # Needed for wait-for-postgres.sh 47 | ports: 48 | - 127.0.0.1:${PORT}:${PORT} 49 | volumes: 50 | - ./scripts/wait-for-postgres.sh:/parse-server/wait-for-postgres.sh 51 | - ./parse/index.js:/parse-server/index.js 52 | - ./parse/cloud:/parse-server/cloud 53 | - ./files:/parse-server/files # All files uploaded from users are stored to an ecrypted drive locally for HIPAA compliance 54 | restart: always 55 | command: ["./wait-for-postgres.sh", "db", "node", "index.js"] 56 | depends_on: 57 | - db 58 | db: 59 | image: netreconlab/hipaa-postgres:latest 60 | environment: 61 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 62 | PG_PARSE_USER: ${PG_PARSE_USER} 63 | PG_PARSE_PASSWORD: ${PG_PARSE_PASSWORD} 64 | PG_PARSE_DB: ${PG_PARSE_DB} 65 | PMM_USER: ${PMM_USER} 66 | PMM_PASSWORD: ${PMM_PASSWORD} 67 | restart: always 68 | ports: 69 | - 127.0.0.1:${DB_PORT}:${DB_PORT} 70 | # Uncomment volumes below to persist postgres data. Make sure to change directory to store data locally 71 | # volumes: 72 | # - /My/Encrypted/Drive/data:/var/lib/postgresql/data #Mount your own drive 73 | # - /My/Encrypted/Drive/archivedir:/var/lib/postgresql/archivedir #Mount your own drive 74 | # monitor: 75 | # image: percona/pmm-server:2 76 | # restart: always 77 | # ports: 78 | # - 127.0.0.1:1080:${PMM_PORT} # Unsecure connections 79 | # - 127.0.0.1:1443:${PMM_TLS_PORT} # Secure connections 80 | # Uncomment volumes below to persist postgres data. Make sure to change directory to store data locally 81 | # volumes: 82 | # - /My/Encrypted/Drive/srv:/srv 83 | # scan: 84 | # image: clamav/clamav:latest 85 | # restart: always 86 | -------------------------------------------------------------------------------- /docker-compose.mongo.dashboard.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | parse: 5 | image: netreconlab/parse-hipaa:6.0.0-dashboard 6 | environment: 7 | CLUSTER_INSTANCES: 1 8 | PARSE_SERVER_APPLICATION_ID: E036A0C5-6829-4B40-9B3B-3E05F6DF32B2 9 | PARSE_SERVER_MAINTENANCE_KEY: 785F83D9-9E56-4BA6-91FE-6A9CA9674738 10 | PARSE_SERVER_PRIMARY_KEY: E2466756-93CF-4C05-BA44-FF5D9C34E99F 11 | PARSE_SERVER_READ_ONLY_PRIMARY_KEY: 367F7395-2E3A-46B1-ABA3-963A25D533C3 12 | PARSE_SERVER_WEBHOOK_KEY: 553D229E-64DF-4928-99F5-B71CCA94A44A 13 | PARSE_SERVER_ENCRYPTION_KEY: 72F8F23D-FDDB-4792-94AE-72897F0688F9 14 | PARSE_SERVER_TRUST_PROXY: 'true' 15 | PARSE_SERVER_OBJECT_ID_SIZE: 32 16 | PARSE_SERVER_DATABASE_URI: mongodb://${MONGO_PARSE_USER}:${MONGO_PARSE_PASSWORD}@db:${DB_MONGO_PORT}/${MONGO_PARSE_DB} 17 | PORT: ${PORT} 18 | PARSE_SERVER_MOUNT_PATH: ${MOUNT_PATH} 19 | PARSE_SERVER_URL: http://parse:${PORT}${MOUNT_PATH} 20 | PARSE_PUBLIC_SERVER_URL: http://localhost:${PORT}${MOUNT_PATH} 21 | PARSE_SERVER_CLOUD: /parse-server/cloud/main.js 22 | PARSE_SERVER_MOUNT_GRAPHQL: 'false' 23 | PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION: 'false' # Don't allow classes to be created on the client side. You can create classes by using ParseDashboard instead 24 | PARSE_SERVER_ALLOW_CUSTOM_OBJECTID: 'true' # Required to be true for ParseCareKit 25 | PARSE_SERVER_ENABLE_SCHEMA_HOOKS: 'true' 26 | PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION: 'true' 27 | PARSE_SERVER_PAGES_ENABLE_ROUTER: 'true' 28 | PARSE_SERVER_DIRECT_ACCESS: 'false' # WARNING: Setting to 'true' is known to cause crashes on parse-hipaa running postgres 29 | PARSE_SERVER_ENABLE_PRIVATE_USERS: 'true' 30 | PARSE_SERVER_USING_PARSECAREKIT: 'true' # If you are not using ParseCareKit, set this to 'false' 31 | PARSE_SERVER_RATE_LIMIT: 'false' 32 | PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT: '100' 33 | PARSE_SERVER_RATE_LIMIT_INCLUDE_PRIMARY_KEY: 'false' 34 | PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS: 'false' 35 | PARSE_DASHBOARD_START: 'true' 36 | PARSE_DASHBOARD_APP_NAME: Parse HIPAA 37 | PARSE_DASHBOARD_SERVER_URL: http://localhost:${PORT}${MOUNT_PATH} 38 | PARSE_DASHBOARD_USERNAMES: parse, parseRead 39 | PARSE_DASHBOARD_USER_PASSWORDS: 1234, 1234 40 | PARSE_DASHBOARD_USER_PASSWORD_ENCRYPTED: 'false' 41 | PARSE_DASHBOARD_ALLOW_INSECURE_HTTP: 1 42 | PARSE_DASHBOARD_COOKIE_SESSION_SECRET: AB8849B6-D725-4A75-AA73-AB7103F0363F # This should be constant across all deployments on your system 43 | PARSE_DASHBOARD_MOUNT_PATH: /dashboard # This needs to be exactly what you plan it to be behind the proxy, i.e. If you want to access cs.uky.edu/dashboard it should be "/dashboard" 44 | PARSE_VERBOSE: 'false' 45 | ports: 46 | - 127.0.0.1:${PORT}:${PORT} 47 | volumes: 48 | - ./parse/index.js:/parse-server/index.js 49 | - ./parse/cloud:/parse-server/cloud 50 | restart: always 51 | depends_on: 52 | - db 53 | command: ["node", "index.js"] 54 | db: 55 | image: netreconlab/hipaa-mongo:latest 56 | environment: 57 | MONGO_INITDB_ROOT_USERNAME: ${MONGO_PARSE_USER} 58 | MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PARSE_PASSWORD} 59 | MONGO_INITDB_DATABASE: ${MONGO_PARSE_DB} 60 | restart: always 61 | ports: 62 | - 127.0.0.1:${DB_MONGO_PORT}:${DB_MONGO_PORT} 63 | # Uncomment volumes below to persist postgres data. Make sure to change directory to store data locally 64 | #volumes: 65 | # - /My/Encrypted/Drive/db:/data/db 66 | # - /My/Encrypted/Drive/logs/:/logs 67 | #scan: 68 | # image: clamav/clamav:latest 69 | # restart: always 70 | -------------------------------------------------------------------------------- /docker-compose.mongo.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | parse: 5 | image: netreconlab/parse-hipaa:latest 6 | environment: 7 | CLUSTER_INSTANCES: 1 8 | PARSE_SERVER_APPLICATION_ID: E036A0C5-6829-4B40-9B3B-3E05F6DF32B2 9 | PARSE_SERVER_MAINTENANCE_KEY: 785F83D9-9E56-4BA6-91FE-6A9CA9674738 10 | PARSE_SERVER_PRIMARY_KEY: E2466756-93CF-4C05-BA44-FF5D9C34E99F 11 | PARSE_SERVER_READ_ONLY_PRIMARY_KEY: 367F7395-2E3A-46B1-ABA3-963A25D533C3 12 | PARSE_SERVER_WEBHOOK_KEY: 553D229E-64DF-4928-99F5-B71CCA94A44A 13 | PARSE_SERVER_ENCRYPTION_KEY: 72F8F23D-FDDB-4792-94AE-72897F0688F9 14 | PARSE_SERVER_TRUST_PROXY: 'true' 15 | PARSE_SERVER_OBJECT_ID_SIZE: 32 16 | PARSE_SERVER_DATABASE_URI: mongodb://${MONGO_PARSE_USER}:${MONGO_PARSE_PASSWORD}@db:${DB_MONGO_PORT}/${MONGO_PARSE_DB} 17 | PORT: ${PORT} 18 | PARSE_SERVER_MOUNT_PATH: ${MOUNT_PATH} 19 | PARSE_SERVER_URL: http://parse:${PORT}${MOUNT_PATH} 20 | PARSE_PUBLIC_SERVER_URL: http://localhost:${PORT}${MOUNT_PATH} 21 | PARSE_SERVER_CLOUD: /parse-server/cloud/main.js 22 | PARSE_SERVER_MOUNT_GRAPHQL: 'false' 23 | PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION: 'false' # Don't allow classes to be created on the client side. You can create classes by using ParseDashboard instead 24 | PARSE_SERVER_ALLOW_CUSTOM_OBJECTID: 'true' # Required to be true for ParseCareKit 25 | PARSE_SERVER_ENABLE_SCHEMA_HOOKS: 'true' 26 | PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION: 'true' 27 | PARSE_SERVER_PAGES_ENABLE_ROUTER: 'true' 28 | PARSE_SERVER_DIRECT_ACCESS: 'false' # WARNING: Setting to 'true' is known to cause crashes on parse-hipaa running postgres 29 | PARSE_SERVER_ENABLE_PRIVATE_USERS: 'true' 30 | PARSE_SERVER_USING_PARSECAREKIT: 'true' # If you are not using ParseCareKit, set this to 'false' 31 | PARSE_SERVER_RATE_LIMIT: 'false' 32 | PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT: '100' 33 | PARSE_SERVER_RATE_LIMIT_INCLUDE_PRIMARY_KEY: 'false' 34 | PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS: 'false' 35 | PARSE_VERBOSE: 'false' 36 | ports: 37 | - 127.0.0.1:${PORT}:${PORT} 38 | volumes: 39 | - ./parse/index.js:/parse-server/index.js 40 | - ./parse/cloud:/parse-server/cloud 41 | restart: always 42 | depends_on: 43 | - db 44 | command: ["node", "index.js"] 45 | db: 46 | image: netreconlab/hipaa-mongo:latest 47 | environment: 48 | MONGO_INITDB_ROOT_USERNAME: ${MONGO_PARSE_USER} 49 | MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PARSE_PASSWORD} 50 | MONGO_INITDB_DATABASE: ${MONGO_PARSE_DB} 51 | restart: always 52 | ports: 53 | - 127.0.0.1:${DB_MONGO_PORT}:${DB_MONGO_PORT} 54 | # Uncomment volumes below to persist postgres data. Make sure to change directory to store data locally 55 | #volumes: 56 | # - /My/Encrypted/Drive/db:/data/db 57 | # - /My/Encrypted/Drive/logs/:/logs 58 | dashboard: 59 | image: netreconlab/parse-hipaa-dashboard:latest 60 | environment: 61 | PARSE_DASHBOARD_ALLOW_INSECURE_HTTP: 1 62 | PARSE_DASHBOARD_COOKIE_SESSION_SECRET: AB8849B6-D725-4A75-AA73-AB7103F0363F # This should be constant across all deployments on your system 63 | MOUNT_PATH: /dashboard # This needs to be exactly what you plan it to be behind the proxy, i.e. If you want to access cs.uky.edu/dashboard it should be "/dashboard" 64 | volumes: 65 | - ./parse/parse-dashboard-config.json:/parse-hipaa-dashboard/lib/parse-dashboard-config.json 66 | ports: 67 | - 127.0.0.1:${DASHBOARD_PORT}:${DASHBOARD_PORT} 68 | depends_on: 69 | - parse 70 | #scan: 71 | # image: clamav/clamav:latest 72 | # restart: always 73 | -------------------------------------------------------------------------------- /docker-compose.no.hipaa.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | parse: 5 | image: netreconlab/parse-hipaa:latest 6 | environment: 7 | CLUSTER_INSTANCES: 1 8 | PARSE_SERVER_APPLICATION_ID: E036A0C5-6829-4B40-9B3B-3E05F6DF32B2 9 | PARSE_SERVER_MAINTENANCE_KEY: 785F83D9-9E56-4BA6-91FE-6A9CA9674738 10 | PARSE_SERVER_PRIMARY_KEY: E2466756-93CF-4C05-BA44-FF5D9C34E99F 11 | PARSE_SERVER_READ_ONLY_PRIMARY_KEY: 367F7395-2E3A-46B1-ABA3-963A25D533C3 12 | PARSE_SERVER_WEBHOOK_KEY: 553D229E-64DF-4928-99F5-B71CCA94A44A 13 | PARSE_SERVER_ENCRYPTION_KEY: 72F8F23D-FDDB-4792-94AE-72897F0688F9 14 | PARSE_SERVER_TRUST_PROXY: 'true' 15 | PARSE_SERVER_OBJECT_ID_SIZE: 10 16 | PARSE_SERVER_DATABASE_URI: postgres://${PG_PARSE_USER}:${PG_PARSE_PASSWORD}@db:${DB_PORT}/${PG_PARSE_DB} 17 | PORT: ${PORT} 18 | PARSE_SERVER_MOUNT_PATH: ${MOUNT_PATH} 19 | PARSE_SERVER_URL: http://parse:${PORT}${MOUNT_PATH} 20 | PARSE_SERVER_PUBLIC_URL: http://localhost:${PORT}${MOUNT_PATH} 21 | PARSE_SERVER_CLOUD: /parse-server/cloud/main.js 22 | PARSE_SERVER_MOUNT_GRAPHQL: 'true' 23 | PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION: 'false' # Don't allow classes to be created on the client side. You can create classes by using ParseDashboard instead 24 | PARSE_SERVER_ALLOW_CUSTOM_OBJECTID: 'true' # Required to be true for ParseCareKit 25 | PARSE_SERVER_ENABLE_SCHEMA_HOOKS: 'true' 26 | PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION: 'true' 27 | PARSE_SERVER_PAGES_ENABLE_ROUTER: 'true' 28 | PARSE_SERVER_DIRECT_ACCESS: 'false' # WARNING: Setting to 'true' is known to cause crashes on parse-hipaa running postgres 29 | PARSE_SERVER_ENABLE_PRIVATE_USERS: 'true' 30 | PARSE_SERVER_USING_PARSECAREKIT: 'false' # If you are not using ParseCareKit, set this to 'false' 31 | PARSE_SERVER_RATE_LIMIT: 'false' 32 | PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT: '100' 33 | PARSE_SERVER_RATE_LIMIT_INCLUDE_PRIMARY_KEY: 'false' 34 | PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS: 'false' 35 | PARSE_VERBOSE: 'false' 36 | POSTGRES_USER: ${POSTGRES_USER} 37 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # Needed for wait-for-postgres.sh 38 | ports: 39 | - 127.0.0.1:${PORT}:${PORT} 40 | volumes: 41 | - ./scripts/wait-for-postgres.sh:/parse-server/wait-for-postgres.sh 42 | - ./parse/index.js:/parse-server/index.js 43 | - ./parse/cloud:/parse-server/cloud 44 | - ./files:/parse-server/files # All files uploaded from users are stored to an ecrypted drive locally for HIPAA compliance 45 | depends_on: 46 | - db 47 | command: ["./wait-for-postgres.sh", "db", "node", "index.js"] 48 | dashboard: 49 | image: parseplatform/parse-dashboard:latest 50 | environment: 51 | PARSE_DASHBOARD_TRUST_PROXY: 1 52 | PARSE_DASHBOARD_COOKIE_SESSION_SECRET: AB8849B6-D725-4A75-AA73-AB7103F0363F # This should be constant across all deployments on your system 53 | MOUNT_PATH: /dashboard # This needs to be exactly what you plan it to be behind the proxy, i.e. If you want to access cs.uky.edu/dashboard it should be "/dashboard" 54 | command: parse-dashboard --dev 55 | volumes: 56 | - ./parse/parse-dashboard-config.json:/src/Parse-Dashboard/parse-dashboard-config.json 57 | ports: 58 | - 127.0.0.1:${DASHBOARD_PORT}:${DASHBOARD_PORT} 59 | depends_on: 60 | - parse 61 | db: 62 | image: postgis/postgis:latest 63 | environment: 64 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 65 | PG_PARSE_USER: ${PG_PARSE_USER} 66 | PG_PARSE_PASSWORD: ${PG_PARSE_PASSWORD} 67 | PG_PARSE_DB: ${PG_PARSE_DB} 68 | restart: always 69 | ports: 70 | - 127.0.0.1:${DB_PORT}:${DB_PORT} 71 | # Uncomment volumes below to persist postgres data. Make sure to change directory to store data locally 72 | #volumes: 73 | # - ./data:/var/lib/postgresql/data 74 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | parse: 5 | image: netreconlab/parse-hipaa:latest 6 | environment: 7 | CLUSTER_INSTANCES: 1 8 | PARSE_SERVER_APPLICATION_ID: E036A0C5-6829-4B40-9B3B-3E05F6DF32B2 9 | PARSE_SERVER_MAINTENANCE_KEY: 785F83D9-9E56-4BA6-91FE-6A9CA9674738 10 | PARSE_SERVER_PRIMARY_KEY: E2466756-93CF-4C05-BA44-FF5D9C34E99F 11 | PARSE_SERVER_READ_ONLY_PRIMARY_KEY: 367F7395-2E3A-46B1-ABA3-963A25D533C3 12 | PARSE_SERVER_WEBHOOK_KEY: 553D229E-64DF-4928-99F5-B71CCA94A44A 13 | PARSE_SERVER_ENCRYPTION_KEY: 72F8F23D-FDDB-4792-94AE-72897F0688F9 14 | PARSE_SERVER_TRUST_PROXY: 'true' 15 | PARSE_SERVER_OBJECT_ID_SIZE: 32 16 | PARSE_SERVER_DATABASE_URI: postgres://${PG_PARSE_USER}:${PG_PARSE_PASSWORD}@db:${DB_PORT}/${PG_PARSE_DB} 17 | PORT: ${PORT} 18 | PARSE_SERVER_MOUNT_PATH: ${MOUNT_PATH} 19 | PARSE_SERVER_URL: http://parse:${PORT}${MOUNT_PATH} 20 | PARSE_SERVER_PUBLIC_URL: http://localhost:${PORT}${MOUNT_PATH} 21 | PARSE_SERVER_CLOUD: /parse-server/cloud/main.js 22 | PARSE_SERVER_MOUNT_GRAPHQL: 'false' 23 | PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION: 'false' # Don't allow classes to be created on the client side. You can create classes by using ParseDashboard instead 24 | PARSE_SERVER_ALLOW_CUSTOM_OBJECTID: 'true' # Required to be true for ParseCareKit 25 | PARSE_SERVER_ENABLE_SCHEMA_HOOKS: 'true' 26 | PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION: 'true' 27 | PARSE_SERVER_PAGES_ENABLE_ROUTER: 'true' 28 | PARSE_SERVER_DIRECT_ACCESS: 'false' # WARNING: Setting to 'true' is known to cause crashes on parse-hipaa running postgres 29 | PARSE_SERVER_ENABLE_PRIVATE_USERS: 'true' 30 | PARSE_SERVER_USING_PARSECAREKIT: 'true' # If you are not using ParseCareKit, set this to 'false' 31 | PARSE_SERVER_RATE_LIMIT: 'false' 32 | PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT: '100' 33 | PARSE_SERVER_RATE_LIMIT_INCLUDE_PRIMARY_KEY: 'false' 34 | PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS: 'false' 35 | PARSE_VERBOSE: 'false' 36 | POSTGRES_USER: ${POSTGRES_USER} 37 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # Needed for wait-for-postgres.sh 38 | ports: 39 | - 127.0.0.1:${PORT}:${PORT} 40 | volumes: 41 | - ./scripts/wait-for-postgres.sh:/parse-server/wait-for-postgres.sh 42 | - ./parse/index.js:/parse-server/index.js 43 | - ./parse/cloud:/parse-server/cloud 44 | - ./files:/parse-server/files # All files uploaded from users are stored to an ecrypted drive locally for HIPAA compliance 45 | command: ["./wait-for-postgres.sh", "db", "node", "index.js"] 46 | depends_on: 47 | - db 48 | db: 49 | image: netreconlab/hipaa-postgres:latest 50 | environment: 51 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 52 | PG_PARSE_USER: ${PG_PARSE_USER} 53 | PG_PARSE_PASSWORD: ${PG_PARSE_PASSWORD} 54 | PG_PARSE_DB: ${PG_PARSE_DB} 55 | PMM_USER: ${PMM_USER} 56 | PMM_PASSWORD: ${PMM_PASSWORD} 57 | restart: always 58 | ports: 59 | - 127.0.0.1:${DB_PORT}:${DB_PORT} 60 | # Uncomment volumes below to persist postgres data. Make sure to change directory to store data locally 61 | # volumes: 62 | # - /My/Encrypted/Drive/data:/var/lib/postgresql/data #Mount your own drive 63 | # - /My/Encrypted/Drive/archivedir:/var/lib/postgresql/archivedir #Mount your own drive 64 | dashboard: 65 | image: netreconlab/parse-hipaa-dashboard:latest 66 | environment: 67 | PARSE_DASHBOARD_ALLOW_INSECURE_HTTP: 1 68 | PARSE_DASHBOARD_COOKIE_SESSION_SECRET: AB8849B6-D725-4A75-AA73-AB7103F0363F # This should be constant across all deployments on your system 69 | MOUNT_PATH: /dashboard # This needs to be exactly what you plan it to be behind the proxy, i.e. If you want to access cs.uky.edu/dashboard it should be "/dashboard" 70 | volumes: 71 | - ./parse/parse-dashboard-config.json:/parse-hipaa-dashboard/lib/parse-dashboard-config.json 72 | ports: 73 | - 127.0.0.1:${DASHBOARD_PORT}:${DASHBOARD_PORT} 74 | depends_on: 75 | - parse 76 | # monitor: 77 | # image: percona/pmm-server:2 78 | # restart: always 79 | # ports: 80 | # - 127.0.0.1:1080:${PMM_PORT} # Unsecure connections 81 | # - 127.0.0.1:1443:${PMM_TLS_PORT} # Secure connections 82 | # Uncomment volumes below to persist postgres data. Make sure to change directory to store data locally 83 | # volumes: 84 | # - /My/Encrypted/Drive/srv:/srv 85 | # scan: 86 | # image: clamav/clamav:latest 87 | # restart: always 88 | -------------------------------------------------------------------------------- /general.env: -------------------------------------------------------------------------------- 1 | export POSTGRES_PASSWORD=postgres 2 | export PG_PARSE_USER=parse 3 | export PG_PARSE_PASSWORD=parse 4 | export PG_PARSE_DB=parse_hipaa 5 | export PORT=1337 6 | export PMM_USER=pmm 7 | export PMM_PASSWORD=pmm 8 | export PMM_PORT=80 9 | export PMM_TLS_PORT=443 10 | export DB_PORT=5432 11 | export DASHBOARD_PORT=4040 12 | export CLUSTER_INSTANCES=1 13 | export MOUNT_PATH=/parse 14 | export PARSE_SERVER_APPLICATION_ID=E036A0C5-6829-4B40-9B3B-3E05F6DF32B2 15 | export PARSE_SERVER_PRIMARY_KEY=E2466756-93CF-4C05-BA44-FF5D9C34E99F 16 | export PARSE_SERVER_READ_ONLY_PRIMARY_KEY=367F7395-2E3A-46B1-ABA3-963A25D533C3 17 | export PARSE_SERVER_WEBHOOK_KEY=553D229E-64DF-4928-99F5-B71CCA94A44A 18 | export PARSE_SERVER_ENCRYPTION_KEY=72F8F23D-FDDB-4792-94AE-72897F0688F9 19 | export PARSE_SERVER_OBJECT_ID_SIZE=32 20 | export PARSE_SERVER_DATABASE_URI=postgres://${PG_PARSE_USER}:${PG_PARSE_PASSWORD}@db:${DB_PORT}/${PG_PARSE_DB} 21 | export PARSE_SERVER_URL=http://parse:${PORT}${MOUNT_PATH} 22 | export PARSE_SERVER_PUBLIC_URL=http://localhost:${PORT}${MOUNT_PATH} 23 | export PARSE_SERVER_CLOUD=/parse-server/cloud/main.js 24 | export PARSE_SERVER_MOUNT_GRAPHQL="true" 25 | export PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION="false" 26 | export PARSE_SERVER_ALLOW_CUSTOM_OBJECTID="true" 27 | export PARSE_SERVER_ENABLE_SCHEMA_HOOKS="true" 28 | export PARSE_SERVER_DIRECT_ACCESS="false" 29 | export PARSE_SERVER_ENABLE_PRIVATE_USERS="true" 30 | export PARSE_SERVER_USING_PARSECAREKIT="true" 31 | export PARSE_VERBOSE="false" 32 | export PARSE_DASHBOARD_START='true' 33 | export PARSE_DASHBOARD_APP_NAME=Parse HIPAA 34 | export PARSE_DASHBOARD_SERVER_URL=http://localhost:${PORT}${MOUNT_PATH} 35 | export PARSE_DASHBOARD_USERNAMES=parse, parseRead 36 | export PARSE_DASHBOARD_USER_PASSWORDS=1234, 1234 37 | export PARSE_DASHBOARD_USER_PASSWORD_ENCRYPTED='false' 38 | export PARSE_DASHBOARD_ALLOW_INSECURE_HTTP=1 39 | export PARSE_DASHBOARD_COOKIE_SESSION_SECRET=AB8849B6-D725-4A75-AA73-AB7103F0363F # This should be constant across all deployments on your system 40 | export PARSE_DASHBOARD_MOUNT_PATH=/dashboard 41 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: parse/Dockerfile.heroku 4 | run: 5 | web: ./scripts/wait-for-postgres.sh node index.js 6 | -------------------------------------------------------------------------------- /nginx/sites-enabled/default-ssl.conf: -------------------------------------------------------------------------------- 1 | server { 2 | server_name your.server.name; 3 | listen 443 ssl; # 4 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; 5 | ssl_certificate /Your/Cert/Location/cert.crt; 6 | ssl_certificate_key /Your/Key/Location/key.key; 7 | 8 | ssl_session_cache shared:SSL:50m; 9 | ssl_session_timeout 5m; 10 | 11 | ssl_protocols SSLv2 SSLv3 TLSv1.2 TLSv1.3; #TLSv1.1, TLSv1; 12 | ssl_ciphers HIGH:!aNULL:!MD5; 13 | ssl_prefer_server_ciphers on; 14 | 15 | # Pass requests for /dashboard/ to Parse Server instance at localhost:1337 16 | location /parse/ { 17 | proxy_set_header X-Real-IP $remote_addr; 18 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 19 | proxy_set_header X-NginX-Proxy true; 20 | proxy_pass http://localhost:1337/parse; 21 | proxy_ssl_session_reuse off; 22 | proxy_set_header Host $http_host; 23 | proxy_redirect off; 24 | } 25 | 26 | # Pass requests for /dashboard/ to Parse Dashboard 27 | # Note: location has to match the exact mounting path for ParseDashboard or else it won't work 28 | location /dashboard/ { 29 | proxy_set_header X-Real-IP $remote_addr; 30 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 31 | proxy_set_header X-NginX-Proxy true; 32 | proxy_pass http://localhost:4040/dashboard/; 33 | proxy_ssl_session_reuse off; 34 | proxy_set_header Host $http_host; 35 | proxy_redirect off; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /nginx/sites-enabled/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name your.server.name; 4 | return 301 https://$host$request_uri; 5 | #access_log logs/host.access.log main; 6 | 7 | location / { 8 | root root_path; 9 | index index.html index.htm; 10 | } 11 | 12 | # redirect server error pages to the static page /50x.html 13 | # 14 | error_page 500 502 503 504 /50x.html; 15 | location = /50x.html { 16 | root html; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /parse/Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################################ 2 | # Build stage 3 | ############################################################ 4 | FROM parseplatform/parse-server:latest AS build 5 | 6 | # Setup directories 7 | COPY ./scripts/ ./scripts/ 8 | 9 | # Install necessary dependencies as root 10 | USER root 11 | RUN apk --no-cache add git \ 12 | && npm install --omit=dev netreconlab/parse-server-carekit#main parse-server-any-analytics-adapter@^1.x.x @analytics/google-analytics@^1.x.x @analytics/segment@^1.x.x \ 13 | && npm install --omit=dev @parse/s3-files-adapter@^4.x.x parse-server-api-mail-adapter@^4.x.x mailgun.js@^12.x.x \ 14 | && npm install --omit=dev clamscan@^2.x.x newrelic@^12.x.x \ 15 | && mkdir ./files \ 16 | && chmod +x ./scripts/wait-for-postgres.sh ./scripts/parse_idempotency_delete_expired_records.sh ./scripts/setup-dbs.sh ./scripts/setup-parse-index.sh ./scripts/setup-pgaudit.sh \ 17 | && chown -R node ./files ./scripts 18 | 19 | ############################################################ 20 | # Release stage 21 | ############################################################ 22 | FROM parseplatform/parse-server:latest AS release 23 | 24 | # Start parse-hipaa setup as root 25 | USER root 26 | 27 | # Install apps needed for image 28 | RUN apk --no-cache add bash postgresql-client 29 | 30 | # Complete parse-hipaa setup as node 31 | USER node 32 | 33 | # Copy necessary folders/files from build phase 34 | COPY --from=build /parse-server/node_modules /parse-server/node_modules 35 | COPY --from=build /parse-server/files /parse-server/files 36 | COPY --from=build /parse-server/scripts /parse-server/scripts 37 | COPY --from=build /parse-server/package*.json /parse-server/ 38 | 39 | # Copy any other files/scripts needed 40 | COPY ./ecosystem.config.js ./ 41 | COPY ./process.yml ./ 42 | COPY ./index.js ./ 43 | COPY ./parse-dashboard-config.json ./ 44 | COPY ./cloud/ ./cloud/ 45 | 46 | ENV CLUSTER_INSTANCES=1 47 | 48 | ENTRYPOINT [] 49 | CMD ["./scripts/wait-for-postgres.sh", "node", "index.js"] 50 | -------------------------------------------------------------------------------- /parse/Dockerfile.dashboard: -------------------------------------------------------------------------------- 1 | ############################################################ 2 | # Build stage 3 | ############################################################ 4 | FROM parseplatform/parse-server:latest AS build 5 | 6 | # Setup directories 7 | COPY ./scripts/ ./scripts/ 8 | 9 | # Install necessary dependencies as root 10 | USER root 11 | RUN apk --no-cache add git \ 12 | && npm install --omit=dev netreconlab/parse-server-carekit#main parse-server-any-analytics-adapter@^1.x.x @analytics/google-analytics@^1.x.x @analytics/segment@^1.x.x \ 13 | && npm install --omit=dev @parse/s3-files-adapter@^4.x.x parse-server-api-mail-adapter@^4.x.x mailgun.js@^12.x.x \ 14 | && npm install --omit=dev parse-hipaa-dashboard@^1.x.x clamscan@^2.x.x newrelic@^12.x.x \ 15 | && mkdir ./files \ 16 | && chmod +x ./scripts/wait-for-postgres.sh ./scripts/parse_idempotency_delete_expired_records.sh ./scripts/setup-dbs.sh ./scripts/setup-parse-index.sh ./scripts/setup-pgaudit.sh \ 17 | && chown -R node ./files ./scripts 18 | 19 | ############################################################ 20 | # Release stage 21 | ############################################################ 22 | FROM parseplatform/parse-server:latest AS release 23 | 24 | # Start parse-hipaa setup as root 25 | USER root 26 | 27 | # Install apps needed for image 28 | RUN apk --no-cache add bash postgresql-client 29 | 30 | # Complete parse-hipaa setup as node 31 | USER node 32 | 33 | # Copy necessary folders/files from build phase 34 | COPY --from=build /parse-server/node_modules /parse-server/node_modules 35 | COPY --from=build /parse-server/files /parse-server/files 36 | COPY --from=build /parse-server/scripts /parse-server/scripts 37 | COPY --from=build /parse-server/package*.json /parse-server/ 38 | 39 | # Copy any files/scripts needed 40 | COPY ./ecosystem.config.js ./ 41 | COPY ./process.yml ./ 42 | COPY ./index.js ./ 43 | COPY ./parse-dashboard-config.json ./ 44 | COPY ./cloud/ ./cloud/ 45 | 46 | ENV CLUSTER_INSTANCES=1 47 | 48 | ENTRYPOINT [] 49 | CMD ["./scripts/wait-for-postgres.sh", "node", "index.js"] 50 | -------------------------------------------------------------------------------- /parse/Dockerfile.heroku: -------------------------------------------------------------------------------- 1 | FROM netreconlab/parse-hipaa:main-dashboard 2 | -------------------------------------------------------------------------------- /parse/cloud/carePlan.js: -------------------------------------------------------------------------------- 1 | //The DB Unique index handles this now. No need for the extra query 2 | /*Parse.Cloud.beforeSave("CarePlan", async (request) => { 3 | var object = request.object; 4 | 5 | if (object.isNew()){ 6 | const query = new Parse.Query("CarePlan"); 7 | query.equalTo("uuid",object.get("uuid")); 8 | const result = await query.first({useMasterKey: true}); 9 | if (result != null){ 10 | throw "Duplicate: CarePlan with this uuid already exists"; 11 | } 12 | } 13 | }); 14 | */ -------------------------------------------------------------------------------- /parse/cloud/contact.js: -------------------------------------------------------------------------------- 1 | //The DB Unique index handles this now. No need for the extra query 2 | /*Parse.Cloud.beforeSave("Contact", async (request) => { 3 | var object = request.object; 4 | 5 | if (object.isNew()){ 6 | const query = new Parse.Query("Contact"); 7 | query.equalTo("uuid",object.get("uuid")); 8 | const result = await query.first({useMasterKey: true}); 9 | if (result != null){ 10 | throw "Duplicate: Contact with this uuid already exists"; 11 | } 12 | } 13 | }); 14 | */ 15 | -------------------------------------------------------------------------------- /parse/cloud/files.js: -------------------------------------------------------------------------------- 1 | const NodeClam = require('clamscan'); 2 | const { Readable } = require('stream'); 3 | 4 | Parse.Cloud.beforeSave(Parse.File, async (request) => { 5 | const { file, user } = request; 6 | try { 7 | const fileData = Buffer.from(await file.getData(), 'base64').toString(); 8 | // Get instance by resolving ClamScan promise object 9 | const clamscan = await new NodeClam().init({ 10 | clamdscan: { 11 | host: 'scan', // IP of host to connect to TCP interface 12 | port: 3310, 13 | } 14 | }); 15 | const stream = new Readable(); 16 | stream.push(fileData); 17 | stream.push(null); 18 | const { isInfected, viruses } = await clamscan.scanStream(stream); 19 | if (isInfected) { 20 | throw `********* Virus or malware detected! This file was not uploaded. Viruses detected: (${viruses.join(',')}) *********`; 21 | } 22 | return file; 23 | } catch(error) { 24 | // Handle any errors raised by the code in the try block 25 | throw `Error scanning for virus or malware ${error}`; 26 | } 27 | }); 28 | 29 | Parse.Cloud.define("setTestSchema", async (request) => { 30 | const { params, headers, log } = request; 31 | const clp = { 32 | get: { requiresAuthentication: true }, 33 | find: { requiresAuthentication: true }, 34 | create: { requiresAuthentication: true }, 35 | update: { requiresAuthentication: true }, 36 | delete: { requiresAuthentication: true }, 37 | addField: {}, 38 | protectedFields: {} 39 | }; 40 | const testSchema = new Parse.Schema('Test'); 41 | try { 42 | await testSchema.get(); 43 | } catch { 44 | try { 45 | await testSchema.addFile('textFile') 46 | .setCLP(clp) 47 | .save(); 48 | console.log("*** Success: Test class created with default fields. Ignore any previous errors about this class ***"); 49 | } catch(error) { 50 | throw error; 51 | } 52 | } 53 | }); 54 | 55 | Parse.Cloud.job("testSaveFile", async (request) => { 56 | const { params, headers, log, message } = request; 57 | const normal_file_url = 'https://github.com/netreconlab/parse-hipaa/blob/main/README.md'; 58 | await Parse.Cloud.run("setTestSchema"); 59 | const object = new Parse.Object('Test'); 60 | const file = new Parse.File("README.md", { uri: normal_file_url }); 61 | object.set('textFile', file); 62 | try { 63 | await object.save(null, { useMasterKey: true }); 64 | message("Saved file"); 65 | } catch(error) { 66 | throw error; 67 | } 68 | }); 69 | 70 | Parse.Cloud.job("testDontSaveUnauthenticatedFile", async (request) => { 71 | const { params, headers, log, message } = request; 72 | const normal_file_url = 'https://github.com/netreconlab/parse-hipaa/blob/main/README.md'; 73 | await Parse.Cloud.run("setTestSchema"); 74 | const object = new Parse.Object('Test'); 75 | const file = new Parse.File("README.md", { uri: normal_file_url }); 76 | object.set('textFile', file); 77 | try { 78 | await object.save(); 79 | message("Saved file"); 80 | } catch(error) { 81 | throw error; 82 | } 83 | }); 84 | 85 | Parse.Cloud.job("testDontSaveVirusFile", async (request) => { 86 | const { params, headers, log, message } = request; 87 | const fake_virus_url = 'https://secure.eicar.org/eicar.com'; 88 | await Parse.Cloud.run("setTestSchema"); 89 | const object = new Parse.Object('Test'); 90 | const file = new Parse.File("eicar.com", { uri: fake_virus_url }); 91 | object.set('textFile', file); 92 | try { 93 | await object.save(null, { useMasterKey: true }); 94 | message("Saved file"); 95 | } catch(error) { 96 | throw error; 97 | } 98 | }); 99 | -------------------------------------------------------------------------------- /parse/cloud/main.js: -------------------------------------------------------------------------------- 1 | require('./patient.js'); 2 | require('./contact.js'); 3 | require('./carePlan.js'); 4 | require('./task.js'); 5 | require('./outcome.js'); 6 | require('./outcomeValue.js'); 7 | require('./note.js'); 8 | // require('./files.js'); 9 | 10 | Parse.Cloud.job("testPatientRejectDuplicates", (request) => { 11 | const { params, headers, log, message } = request; 12 | 13 | const object = new Parse.Object('Patient'); 14 | object.set('objectId', "112"); 15 | object.save({ useMasterKey: true }).then((result) => { 16 | message("Saved patient"); 17 | }) 18 | .catch(error => message(error)); 19 | }); 20 | 21 | Parse.Cloud.job("testCarePlanRejectDuplicates", (request) => { 22 | const { params, headers, log, message } = request; 23 | 24 | const object = new Parse.Object('CarePlan'); 25 | object.set('objectId', "112"); 26 | object.save({ useMasterKey: true }).then((result) => { 27 | message("Saved carePlan"); 28 | }) 29 | .catch(error => message(error)); 30 | }); 31 | 32 | Parse.Cloud.job("testContactRejectDuplicates", (request) => { 33 | const { params, headers, log, message } = request; 34 | 35 | const object = new Parse.Object('Contact'); 36 | object.set('objectId', "112"); 37 | object.save({ useMasterKey: true }).then((result) => { 38 | message("Saved contact"); 39 | }) 40 | .catch(error => message(error)); 41 | }); 42 | 43 | Parse.Cloud.job("testTaskRejectDuplicates", (request) => { 44 | const { params, headers, log, message } = request; 45 | 46 | const object = new Parse.Object('Task'); 47 | object.set('objectId', "112"); 48 | object.save({ useMasterKey: true }).then((result) => { 49 | message("Saved task"); 50 | }) 51 | .catch(error => message(error)); 52 | }); 53 | 54 | Parse.Cloud.job("testOutcomeRejectDuplicates", (request) => { 55 | const { params, headers, log, message } = request; 56 | 57 | const object = new Parse.Object('Outcome'); 58 | object.set('objectId', "112"); 59 | object.save({ useMasterKey: true }).then((result) => { 60 | message("Saved outcome"); 61 | }) 62 | .catch(error => message(error)); 63 | }); 64 | -------------------------------------------------------------------------------- /parse/cloud/note.js: -------------------------------------------------------------------------------- 1 | //Because of way ParseCareKit handles this class, comment out this check 2 | /*Parse.Cloud.beforeSave("Note", async (request) => { 3 | var object = request.object; 4 | 5 | if (object.isNew()){ 6 | const query = new Parse.Query("Note"); 7 | query.equalTo("uuid",object.get("uuid")); 8 | const result = await query.first({useMasterKey: true}); 9 | if (result != null){ 10 | throw "Duplicate: Note with this uuid already exists"; 11 | } 12 | } 13 | }); 14 | */ 15 | -------------------------------------------------------------------------------- /parse/cloud/outcome.js: -------------------------------------------------------------------------------- 1 | //The DB Unique index handles this now. No need for the extra query 2 | /*Parse.Cloud.beforeSave("Outcome", async (request) => { 3 | var object = request.object; 4 | 5 | if (object.isNew()){ 6 | const query = new Parse.Query("Outcome"); 7 | query.equalTo("uuid",object.get("uuid")); 8 | const result = await query.first({useMasterKey: true}); 9 | if (result != null){ 10 | throw "Duplicate: Outcome with this uuid already exists"; 11 | } 12 | } 13 | });*/ 14 | -------------------------------------------------------------------------------- /parse/cloud/outcomeValue.js: -------------------------------------------------------------------------------- 1 | //Because of way ParseCareKit handles this class, comment out this check 2 | /* 3 | Parse.Cloud.beforeSave("OutcomeValue", async (request) => { 4 | var object = request.object; 5 | 6 | if (object.isNew()){ 7 | const query = new Parse.Query("OutcomeValue"); 8 | query.equalTo("uuid",object.get("uuid")); 9 | const result = await query.first({useMasterKey: true}) 10 | if (result != null){ 11 | throw "Duplicate: OutcomeValue with this uuid already exists"; 12 | } 13 | } 14 | }); 15 | */ 16 | -------------------------------------------------------------------------------- /parse/cloud/patient.js: -------------------------------------------------------------------------------- 1 | //The DB Unique index handles this now. No need for the extra query 2 | /*Parse.Cloud.beforeSave("Patient", async (request) => { 3 | var object = request.object; 4 | 5 | if (object.isNew()){ 6 | const query = new Parse.Query("Patient"); 7 | query.equalTo("uuid",object.get("uuid")); 8 | const result = await query.first({useMasterKey: true}) 9 | if (result != null){ 10 | throw "Duplicate: Patient with this uuid already exists"; 11 | } 12 | } 13 | }); 14 | */ 15 | -------------------------------------------------------------------------------- /parse/cloud/task.js: -------------------------------------------------------------------------------- 1 | //The DB Unique index handles this now. No need for the extra query 2 | /*Parse.Cloud.beforeSave("Task", async (request) => { 3 | var object = request.object; 4 | 5 | if (object.isNew()){ 6 | const query = new Parse.Query("Task"); 7 | query.equalTo("uuid",object.get("uuid")); 8 | const result = await query.first({useMasterKey: true}) 9 | if (result != null){ 10 | throw "Duplicate: Task with this uuid already exists"; 11 | } 12 | } 13 | });*/ 14 | -------------------------------------------------------------------------------- /parse/docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | sut: 5 | image: netreconlab/parse-sut:latest 6 | links: 7 | - parse 8 | depends_on: 9 | - parse 10 | parse: 11 | build: 12 | context: . 13 | dockerfile: Dockerfile 14 | environment: 15 | PARSE_SERVER_APPLICATION_ID: E036A0C5-6829-4B40-9B3B-3E05F6DF32B2 16 | PARSE_SERVER_MASTER_KEY: E2466756-93CF-4C05-BA44-FF5D9C34E99F 17 | PARSE_SERVER_OBJECT_ID_SIZE: 32 18 | PARSE_SERVER_DATABASE_URI: postgres://${PG_PARSE_USER}:${PG_PARSE_PASSWORD}@db:5432/${PG_PARSE_DB} 19 | PORT: ${PORT} 20 | PARSE_SERVER_MOUNT_PATH: /parse 21 | PARSE_SERVER_URL: http://parse:${PORT}/parse 22 | PARSE_PUBLIC_SERVER_URL: http://localhost:${PORT}/parse 23 | PARSE_SERVER_CLOUD: /parse-server/cloud/main.js 24 | PARSE_SERVER_MOUNT_GRAPHQL: 1 25 | PARSE_USING_PARSECAREKIT: 0 #If you are not using ParseCareKit, set this to 0 26 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 27 | ports: 28 | - ${PORT}:${PORT} 29 | volumes: 30 | - ../scripts/wait-for-postgres.sh:/parse-server/wait-for-postgres.sh 31 | restart: always 32 | command: ["./wait-for-postgres.sh", "db", "node", "index.js"] 33 | links: 34 | - db 35 | depends_on: 36 | - db 37 | db: 38 | image: netreconlab/hipaa-postgres:12-3.0 39 | environment: 40 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 41 | PG_PARSE_USER: ${PG_PARSE_USER} 42 | PG_PARSE_PASSWORD: ${PG_PARSE_PASSWORD} 43 | PG_PARSE_DB: ${PG_PARSE_DB} 44 | restart: always 45 | 46 | -------------------------------------------------------------------------------- /parse/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps : [{ 3 | name : "parse-hipaa", 4 | script : "./index.js", 5 | ignore_watch: ["logs", "node_modules", ".pm2"], 6 | watch : true, 7 | merge_logs : true, 8 | cwd : "/parse-server", 9 | exec_mode : "cluster", 10 | instances : 4 11 | }] 12 | } 13 | -------------------------------------------------------------------------------- /parse/index.js: -------------------------------------------------------------------------------- 1 | // Example express application adding the parse-server module to expose Parse 2 | // compatible API routes. 3 | 4 | const { default: ParseServer, RedisCacheAdapter } = require('./lib'); 5 | const { GridFSBucketAdapter } = require('./lib/Adapters/Files/GridFSBucketAdapter'); 6 | const ParseAuditor = require('./node_modules/parse-auditor/src/index.js'); 7 | const express = require('express'); 8 | const path = require('path'); 9 | const cors = require('cors'); 10 | const FSFilesAdapter = require('@parse/fs-files-adapter'); 11 | 12 | const mountPath = process.env.PARSE_SERVER_MOUNT_PATH || '/parse'; 13 | const graphQLPath = process.env.PARSE_SERVER_GRAPHQL_PATH || '/graphql'; 14 | const dashboardMountPath = process.env.PARSE_DASHBOARD_MOUNT_PATH || '/dashboard'; 15 | const applicationId = process.env.PARSE_SERVER_APPLICATION_ID || 'myAppId'; 16 | const maintenanceKey = process.env.PARSE_SERVER_MAINTENANCE_KEY || 'myMaintenanceKey'; 17 | const primaryKey = process.env.PARSE_SERVER_PRIMARY_KEY || 'myKey'; 18 | const redisURL = process.env.PARSE_SERVER_REDIS_URL || process.env.REDIS_TLS_URL || process.env.REDIS_URL; 19 | const host = process.env.HOST || process.env.PARSE_SERVER_HOST || '0.0.0.0'; 20 | const port = process.env.PORT || 1337; 21 | let serverURL = process.env.PARSE_SERVER_URL || 'http://localhost:' + process.env.PORT + mountPath; 22 | let appName = 'myApp'; 23 | if ("NEW_RELIC_APP_NAME" in process.env) { 24 | require ('newrelic'); 25 | appName = process.env.NEW_RELIC_APP_NAME; 26 | if (!("PARSE_SERVER_URL" in process.env)) { 27 | serverURL = `https://${appName}.herokuapp.com${mountPath}`; 28 | } 29 | } 30 | 31 | const publicServerURL = process.env.PARSE_SERVER_PUBLIC_URL || serverURL; 32 | const url = new URL(publicServerURL); 33 | const graphURL = new URL(publicServerURL); 34 | graphURL.pathname = graphQLPath; 35 | const dashboardURL = new URL(publicServerURL); 36 | dashboardURL.pathname = dashboardMountPath; 37 | 38 | let enableParseServer = true; 39 | if (process.env.PARSE_SERVER_ENABLE == 'false') { 40 | enableParseServer = false 41 | } 42 | 43 | let startLiveQueryServer = true; 44 | if (process.env.PARSE_SERVER_START_LIVE_QUERY_SERVER == 'false') { 45 | startLiveQueryServer = false 46 | } 47 | 48 | let startLiveQueryServerNoParse = false; 49 | if (process.env.PARSE_SERVER_START_LIVE_QUERY_SERVER_NO_PARSE == 'true') { 50 | startLiveQueryServerNoParse = true 51 | } 52 | 53 | let enableDashboard = false; 54 | if (process.env.PARSE_DASHBOARD_START == 'true') { 55 | enableDashboard = true 56 | } 57 | 58 | let verbose = false; 59 | if (process.env.PARSE_VERBOSE == 'true') { 60 | verbose = true 61 | } 62 | 63 | let configuration; 64 | const logsFolder = process.env.PARSE_SERVER_LOGS_FOLDER || './logs'; 65 | const fileMaxUploadSize = process.env.PARSE_SERVER_MAX_UPLOAD_SIZE || '20mb'; 66 | const cacheMaxSize = parseInt(process.env.PARSE_SERVER_CACHE_MAX_SIZE) || 10000; 67 | const cacheTTL = parseInt(process.env.PARSE_SERVER_CACHE_TTL) || 5000; 68 | const objectIdSize = parseInt(process.env.PARSE_SERVER_OBJECT_ID_SIZE) || 10; 69 | const sessionLength = parseInt(process.env.PARSE_SERVER_SESSION_LENGTH) || 31536000; 70 | const emailVerifyTokenValidityDuration = parseInt(process.env.PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION) || 24*60*60; 71 | const accountLockoutDuration = parseInt(process.env.PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION) || 5; 72 | const accountLockoutThreshold = parseInt(process.env.PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD) || 5; 73 | const maxPasswordHistory = parseInt(process.env.PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY) || 5; 74 | const resetTokenValidityDuration = parseInt(process.env.PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION) || 24*60*60; 75 | const validationError = process.env.PARSE_SERVER_PASSWORD_POLICY_VALIDATION_ERROR || 'Password must have at least 8 characters, contain at least 1 digit, 1 lower case, 1 upper case, and contain at least one special character.'; 76 | const validatorPattern = process.env.PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN || /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/; 77 | const triggerAfter = process.env.PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER || 'info'; 78 | const triggerBeforeError = process.env.PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR || 'error'; 79 | const triggerBeforeSuccess = process.env.PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS || 'info'; 80 | const playgroundPath = process.env.PARSE_SERVER_MOUNT_PLAYGROUND || '/playground'; 81 | const websocketTimeout = process.env.PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT || 10 * 1000; 82 | const cacheTimeout = process.env.PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT || 5 * 1000; 83 | const logLevel = process.env.PARSE_LIVE_QUERY_SERVER_LOG_LEVEL || 'INFO'; 84 | let maintenanceKeyIps = process.env.PARSE_SERVER_MAINTENANCE_KEY_IPS || '172.16.0.0/12, 192.168.0.0/16, 10.0.0.0/8, 127.0.0.1, ::1'; 85 | maintenanceKeyIps = maintenanceKeyIps.split(", "); 86 | let primaryKeyIps = process.env.PARSE_SERVER_PRIMARY_KEY_IPS || '172.16.0.0/12, 192.168.0.0/16, 10.0.0.0/8, 127.0.0.1, ::1'; 87 | primaryKeyIps = primaryKeyIps.split(", "); 88 | let classNames = process.env.PARSE_SERVER_LIVEQUERY_CLASSNAMES || 'Clock, RevisionRecord'; 89 | classNames = classNames.split(", "); 90 | let trustServerProxy = process.env.PARSE_SERVER_TRUST_PROXY || false; 91 | if (trustServerProxy == 'true') { 92 | trustServerProxy = true; 93 | } 94 | 95 | let enableGraphQL = false; 96 | if (process.env.PARSE_SERVER_MOUNT_GRAPHQL == 'true') { 97 | enableGraphQL = true 98 | } 99 | 100 | let allowNewClasses = false; 101 | if (process.env.PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION == 'true') { 102 | allowNewClasses = true 103 | } 104 | 105 | let allowCustomObjectId = false; 106 | if (process.env.PARSE_SERVER_ALLOW_CUSTOM_OBJECTID == 'true') { 107 | allowCustomObjectId = true 108 | } 109 | 110 | let enableSchemaHooks = false; 111 | if (process.env.PARSE_SERVER_DATABASE_ENABLE_SCHEMA_HOOKS == 'true') { 112 | enableSchemaHooks = true 113 | } 114 | 115 | let encodeParseObjectInCloudFunction = false; 116 | if (process.env.PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION == 'true') { 117 | encodeParseObjectInCloudFunction = true 118 | } 119 | 120 | let enablePagesRouter = false; 121 | if (process.env.PARSE_SERVER_PAGES_ENABLE_ROUTER == 'true') { 122 | enablePagesRouter = true 123 | } 124 | 125 | const pagesOptions = { 126 | enableRouter: enablePagesRouter, 127 | }; 128 | 129 | let useDirectAccess = false; 130 | if (process.env.PARSE_SERVER_DIRECT_ACCESS == 'true') { 131 | useDirectAccess = true 132 | } 133 | 134 | let enforcePrivateUsers = false; 135 | if (process.env.PARSE_SERVER_ENFORCE_PRIVATE_USERS == 'true') { 136 | enforcePrivateUsers = true 137 | } 138 | 139 | let fileUploadPublic = false; 140 | if (process.env.PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_PUBLIC == 'true') { 141 | fileUploadPublic = true 142 | } 143 | 144 | let fileUploadAnonymous = true; 145 | if (process.env.PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_ANONYMOUS_USER == 'false') { 146 | fileUploadAnonymous = false 147 | } 148 | 149 | let fileUploadAuthenticated = true; 150 | if (process.env.PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_AUTHENTICATED_USER == 'false') { 151 | fileUploadAuthenticated = false 152 | } 153 | 154 | let fileExtensions = ['^[^hH][^tT][^mM][^lL]?$']; 155 | if ("PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS" in process.env) { 156 | const extensions = process.env.PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS.split(", "); 157 | fileExtensions = extensions; 158 | } 159 | 160 | let enableAnonymousUsers = true; 161 | if (process.env.PARSE_SERVER_ENABLE_ANON_USERS == 'false') { 162 | enableAnonymousUsers = false 163 | } 164 | 165 | let enableIdempotency = false; 166 | if (process.env.PARSE_SERVER_ENABLE_IDEMPOTENCY == 'true') { 167 | enableIdempotency = true 168 | } 169 | 170 | let allowExpiredAuthDataToken = false; 171 | if (process.env.PARSE_SERVER_ALLOW_EXPIRED_AUTH_DATA_TOKEN == 'true') { 172 | allowExpiredAuthDataToken = true 173 | } 174 | 175 | let emailVerifyTokenReuseIfValid = false; 176 | if (process.env.PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID == 'true') { 177 | emailVerifyTokenReuseIfValid = true 178 | } 179 | 180 | let expireInactiveSessions = true; 181 | if (process.env.PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS == 'false') { 182 | expireInactiveSessions = false 183 | } 184 | 185 | let jsonLogs = false; 186 | if (process.env.JSON_LOGS == 'true') { 187 | jsonLogs = true 188 | } 189 | 190 | let preserveFileName = false; 191 | if (process.env.PARSE_SERVER_PRESERVE_FILE_NAME == 'true') { 192 | preserveFileName = true 193 | } 194 | 195 | let revokeSessionOnPasswordReset = true; 196 | if (process.env.PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET == 'false') { 197 | revokeSessionOnPasswordReset = false 198 | } 199 | 200 | let verifyUserEmails = false; 201 | if (process.env.PARSE_SERVER_VERIFY_USER_EMAILS == 'true') { 202 | verifyUserEmails = true 203 | } 204 | 205 | let unlockOnPasswordReset = false; 206 | if (process.env.PARSE_SERVER_ACCOUNT_LOCKOUT_UNLOCK_ON_PASSWORD_RESET == 'true') { 207 | unlockOnPasswordReset = true 208 | } 209 | 210 | let doNotAllowUsername = false; 211 | if (process.env.PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME == 'true') { 212 | doNotAllowUsername = true 213 | } 214 | 215 | let resetTokenReuseIfValid = false; 216 | if (process.env.PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID == 'true') { 217 | resetTokenReuseIfValid = true 218 | } 219 | 220 | let preventLoginWithUnverifiedEmail = false; 221 | if (process.env.PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL == 'true') { 222 | preventLoginWithUnverifiedEmail = true 223 | } 224 | 225 | let mountPlayground = false; 226 | if (process.env.PARSE_SERVER_MOUNT_PLAYGROUND == 'true') { 227 | mountPlayground = true; 228 | } 229 | 230 | let pushNotifications = process.env.PARSE_SERVER_PUSH || {}; 231 | let authentication = process.env.PARSE_SERVER_AUTH_PROVIDERS || {}; 232 | 233 | let databaseUri = process.env.PARSE_SERVER_DATABASE_URI || process.env.DB_URL; 234 | if (!databaseUri) { 235 | console.log('PARSE_SERVER_DATABASE_URI or DB_URL not specified, falling back to localhost.'); 236 | } 237 | 238 | // Need to use local file adapter for postgres 239 | let filesAdapter = {}; 240 | let filesFSAdapterOptions = {} 241 | if ("PARSE_SERVER_ENCRYPTION_KEY" in process.env) { 242 | filesFSAdapterOptions.encryptionKey = process.env.PARSE_SERVER_ENCRYPTION_KEY; 243 | } 244 | 245 | if ("PARSE_SERVER_DATABASE_URI" in process.env) { 246 | if (process.env.PARSE_SERVER_DATABASE_URI.indexOf('postgres') !== -1) { 247 | filesAdapter = new FSFilesAdapter(filesFSAdapterOptions); 248 | } 249 | } else if ("DB_URL" in process.env) { 250 | if (process.env.DB_URL.indexOf('postgres') !== -1) { 251 | filesAdapter = new FSFilesAdapter(filesFSAdapterOptions); 252 | databaseUri = `${databaseUri}?ssl=true&rejectUnauthorized=false`; 253 | } 254 | } 255 | 256 | if ("PARSE_SERVER_S3_BUCKET" in process.env) { 257 | filesAdapter = { 258 | "module": "@parse/s3-files-adapter", 259 | "options": { 260 | "bucket": process.env.PARSE_SERVER_S3_BUCKET, 261 | "region": process.env.PARSE_SERVER_S3_BUCKET_REGION || 'us-east-2', 262 | "ServerSideEncryption": process.env.PARSE_SERVER_S3_BUCKET_ENCRYPTION || 'AES256', //AES256 or aws:kms, or if you do not pass this, encryption won't be done 263 | } 264 | } 265 | } 266 | 267 | if (Object.keys(filesAdapter).length === 0) { 268 | filesAdapter = new GridFSBucketAdapter( 269 | databaseUri, 270 | {}, 271 | process.env.PARSE_SERVER_ENCRYPTION_KEY 272 | ); 273 | } 274 | 275 | configuration = { 276 | databaseURI: databaseUri || 'mongodb://localhost:27017/dev', 277 | databaseOptions: { 278 | enableSchemaHooks: enableSchemaHooks, 279 | }, 280 | cloud: process.env.PARSE_SERVER_CLOUD || __dirname + '/cloud/main.js', 281 | appId: applicationId, 282 | maintenanceKey: maintenanceKey, 283 | maintenanceKeyIps: maintenanceKeyIps, 284 | masterKey: primaryKey, 285 | masterKeyIps: primaryKeyIps, 286 | webhookKey: process.env.PARSE_SERVER_WEBHOOK_KEY, 287 | encryptionKey: process.env.PARSE_SERVER_ENCRYPTION_KEY, 288 | objectIdSize: objectIdSize, 289 | serverURL: serverURL, 290 | publicServerURL: publicServerURL, 291 | host: host, 292 | port: port, 293 | trustProxy: trustServerProxy, 294 | cacheMaxSize: cacheMaxSize, 295 | cacheTTL: cacheTTL, 296 | verbose: verbose, 297 | allowClientClassCreation: allowNewClasses, 298 | allowCustomObjectId: allowCustomObjectId, 299 | enableAnonymousUsers: enableAnonymousUsers, 300 | emailVerifyTokenReuseIfValid: emailVerifyTokenReuseIfValid, 301 | expireInactiveSessions: expireInactiveSessions, 302 | filesAdapter: filesAdapter, 303 | fileUpload: { 304 | enableForPublic: fileUploadPublic, 305 | enableForAnonymousUser: fileUploadAnonymous, 306 | enableForAuthenticatedUser: fileUploadAuthenticated, 307 | fileExtensions: fileExtensions, 308 | }, 309 | maxUploadSize: fileMaxUploadSize, 310 | encodeParseObjectInCloudFunction: encodeParseObjectInCloudFunction, 311 | directAccess: useDirectAccess, 312 | allowExpiredAuthDataToken: allowExpiredAuthDataToken, 313 | enforcePrivateUsers: enforcePrivateUsers, 314 | jsonLogs: jsonLogs, 315 | logsFolder: logsFolder, 316 | pages: pagesOptions, 317 | preserveFileName: preserveFileName, 318 | revokeSessionOnPasswordReset: revokeSessionOnPasswordReset, 319 | sessionLength: sessionLength, 320 | // Setup your push adatper 321 | push: pushNotifications, 322 | auth: authentication, 323 | startLiveQueryServer: startLiveQueryServer, 324 | liveQuery: { 325 | classNames: classNames, // List of classes to support for query subscriptions 326 | }, 327 | mountGraphQL: enableGraphQL, 328 | graphQLPath: graphQLPath, 329 | mountPlayground: mountPlayground, 330 | playgroundPath: playgroundPath, 331 | verifyUserEmails: verifyUserEmails, 332 | // Setup your mail adapter 333 | /*emailAdapter: { 334 | module: 'parse-server-api-mail-adapter', 335 | /*options: { 336 | // The address that your emails come from 337 | sender: '', 338 | templates: { 339 | passwordResetEmail: { 340 | subject: 'Reset your password', 341 | pathPlainText: path.join(__dirname, 'email-templates/password_reset_email.txt'), 342 | pathHtml: path.join(__dirname, 'email-templates/password_reset_email.html'), 343 | callback: (user) => {}//{ return { firstName: user.get('firstName') }} 344 | // Now you can use {{firstName}} in your templates 345 | }, 346 | verificationEmail: { 347 | subject: 'Confirm your account', 348 | pathPlainText: path.join(__dirname, 'email-templates/verification_email.txt'), 349 | pathHtml: path.join(__dirname, 'email-templates/verification_email.html'), 350 | callback: (user) => {}//{ return { firstName: user.get('firstName') }} 351 | // Now you can use {{firstName}} in your templates 352 | }, 353 | customEmailAlert: { 354 | subject: 'Urgent notification!', 355 | pathPlainText: path.join(__dirname, 'email-templates/custom_email.txt'), 356 | pathHtml: path.join(__dirname, 'email-templates/custom_email.html'), 357 | } 358 | } 359 | } 360 | },*/ 361 | emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration, // in seconds (2 hours = 7200 seconds) 362 | // set preventLoginWithUnverifiedEmail to false to allow user to login without verifying their email 363 | // set preventLoginWithUnverifiedEmail to true to prevent user from login if their email is not verified 364 | preventLoginWithUnverifiedEmail: preventLoginWithUnverifiedEmail, // defaults to false 365 | // account lockout policy setting (OPTIONAL) - defaults to undefined 366 | // if the account lockout policy is set and there are more than `threshold` number of failed login attempts then the `login` api call returns error code `Parse.Error.OBJECT_NOT_FOUND` with error message `Your account is locked due to multiple failed login attempts. Please try again after minute(s)`. After `duration` minutes of no login attempts, the application will allow the user to try login again. 367 | accountLockout: { 368 | duration: accountLockoutDuration, // duration policy setting determines the number of minutes that a locked-out account remains locked out before automatically becoming unlocked. Set it to a value greater than 0 and less than 100000. 369 | threshold: accountLockoutThreshold, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000. 370 | unlockOnPasswordReset: unlockOnPasswordReset, 371 | }, 372 | // optional settings to enforce password policies 373 | passwordPolicy: { 374 | // Two optional settings to enforce strong passwords. Either one or both can be specified. 375 | // If both are specified, both checks must pass to accept the password 376 | // 1. a RegExp object or a regex string representing the pattern to enforce 377 | validatorPattern: validatorPattern, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit 378 | // 2. a callback function to be invoked to validate the password 379 | //validatorCallback: (password) => { return validatePassword(password) }, 380 | validationError: validationError, // optional error message to be sent instead of the default "Password does not meet the Password Policy requirements." message. 381 | doNotAllowUsername: doNotAllowUsername, // optional setting to disallow username in passwords 382 | maxPasswordHistory: maxPasswordHistory, // optional setting to prevent reuse of previous n passwords. Maximum value that can be specified is 20. Not specifying it or specifying 0 will not enforce history. 383 | //optional setting to set a validity duration for password reset links (in seconds) 384 | resetTokenReuseIfValid: resetTokenReuseIfValid, 385 | resetTokenValidityDuration: resetTokenValidityDuration, // expire after 24 hours 386 | }, 387 | logLevels: { 388 | triggerAfter: triggerAfter, 389 | triggerBeforeError: triggerBeforeError, 390 | triggerBeforeSuccess: triggerBeforeSuccess, 391 | } 392 | }; 393 | 394 | if ("PARSE_SERVER_READ_ONLY_PRIMARY_KEY" in process.env) { 395 | configuration.readOnlyMasterKey = process.env.PARSE_SERVER_READ_ONLY_PRIMARY_KEY; 396 | } 397 | 398 | if (("PARSE_SERVER_REDIS_URL" in process.env) || ("REDIS_TLS_URL" in process.env) || ("REDIS_URL" in process.env)) { 399 | const redisOptions = { url: redisURL }; 400 | configuration.cacheAdapter = new RedisCacheAdapter(redisOptions); 401 | // Set LiveQuery URL 402 | configuration.liveQuery.redisURL = redisURL; 403 | } 404 | 405 | // Rate limiting 406 | let rateLimit = false; 407 | if (process.env.PARSE_SERVER_RATE_LIMIT == 'true') { 408 | rateLimit = true; 409 | } 410 | 411 | if (rateLimit == true) { 412 | configuration.rateLimit = []; 413 | const firstRateLimit = {}; 414 | firstRateLimit.requestPath = process.env.PARSE_SERVER_RATE_LIMIT_REQUEST_PATH || '*'; 415 | firstRateLimit.errorResponseMessage = process.env.PARSE_SERVER_RATE_LIMIT_ERROR_RESPONSE_MESSAGE || 'Too many requests'; 416 | firstRateLimit.requestCount = parseInt(process.env.PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT) || 100; 417 | firstRateLimit.requestTimeWindow = parseInt(process.env.PARSE_SERVER_RATE_LIMIT_REQUEST_TIME_WINDOW) || 10 * 60 * 1000; 418 | if (process.env.PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS == 'true') { 419 | firstRateLimit.includeInternalRequests = true; 420 | } 421 | if (process.env.PARSE_SERVER_RATE_LIMIT_INCLUDE_PRIMARY_KEY == 'true') { 422 | firstRateLimit.includeMasterKey = true; 423 | } 424 | if ("PARSE_SERVER_RATE_LIMIT_REQUEST_METHODS" in process.env) { 425 | const requestMethods = process.env.PARSE_SERVER_RATE_LIMIT_REQUEST_METHODS.split(", "); 426 | firstRateLimit.requestMethods = requestMethods; 427 | } 428 | configuration.rateLimit.push(firstRateLimit); 429 | } 430 | 431 | if ("PARSE_SERVER_GRAPH_QLSCHEMA" in process.env) { 432 | configuration.graphQLSchema = process.env.PARSE_SERVER_GRAPH_QLSCHEMA; 433 | } 434 | 435 | if ("PARSE_SERVER_ALLOW_HEADERS" in process.env) { 436 | configuration.allowHeaders = process.env.PARSE_SERVER_ALLOW_HEADERS; 437 | } 438 | 439 | if ("PARSE_SERVER_ALLOW_ORIGIN" in process.env) { 440 | configuration.allowOrigin = process.env.PARSE_SERVER_ALLOW_ORIGIN; 441 | } 442 | 443 | if ("PARSE_SERVER_MAX_LIMIT" in process.env) { 444 | configuration.maxLimit = parseInt(process.env.PARSE_SERVER_MAX_LIMIT); 445 | } 446 | 447 | if ("PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE" in process.env) { 448 | configuration.passwordPolicy.maxPasswordAge = parseInt(process.env.PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE); 449 | } 450 | 451 | if (enableIdempotency) { 452 | let paths = process.env.PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS || '.*'; 453 | paths = paths.split(", "); 454 | const ttl = process.env.PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL || 300; 455 | configuration.idempotencyOptions = { 456 | paths: paths, 457 | ttl: ttl 458 | }; 459 | } 460 | 461 | let app = express(); 462 | 463 | // Enable All CORS Requests 464 | app.use(cors()); 465 | 466 | // Redirect to https if on Heroku 467 | app.use(function(request, response, next) { 468 | if (("NEW_RELIC_APP_NAME" in process.env) && !request.secure) 469 | return response.redirect("https://" + request.headers.host + request.url); 470 | next(); 471 | }); 472 | 473 | async function setupParseServer() { 474 | const parseServer = await ParseServer.startApp(configuration); 475 | app = parseServer.expressApp; 476 | 477 | // Enable All CORS Requests 478 | app.use(cors()); 479 | 480 | // Serve static assets from the /public folder 481 | app.use('/public', express.static(path.join(__dirname, '/public'))); 482 | 483 | // Parse Server plays nicely with the rest of your web routes 484 | app.get('/', function(_req, res) { 485 | res.status(200).send('I dream of being a website. Please star the parse-hipaa repo on GitHub!'); 486 | }); 487 | 488 | // Redirect to https if on Heroku 489 | app.use(function(request, response, next) { 490 | if (("NEW_RELIC_APP_NAME" in process.env) && !request.secure) 491 | return response.redirect("https://" + request.headers.host + request.url); 492 | next(); 493 | }); 494 | 495 | setupDashboard(); 496 | 497 | console.log('Public access: ' + url.hostname + ', Local access: ' + serverURL); 498 | console.log(`REST API running on ${url.href}`); 499 | if (startLiveQueryServer) 500 | console.log(`LiveQuery server is now available at ${url.href}`); 501 | if (enableGraphQL) 502 | console.log(`GraphQL API running on ${graphURL.href}`); 503 | if (enableDashboard) 504 | console.log(`Dashboard is now available at ${dashboardURL.href}`); 505 | 506 | if (process.env.PARSE_SERVER_USING_PARSECAREKIT == 'true') { 507 | const { CareKitServer } = require('parse-server-carekit'); 508 | let shouldAudit = true; 509 | if (process.env.PARSE_SERVER_USING_PARSECAREKIT_AUDIT === 'false') { 510 | shouldAudit = false; 511 | } 512 | if (shouldAudit) { 513 | setAuditClassLevelPermissions(); 514 | } 515 | let careKitServer = new CareKitServer(parseServer, shouldAudit); 516 | await careKitServer.setup(); 517 | } 518 | } 519 | 520 | function setAuditClassLevelPermissions() { 521 | const auditCLP = { 522 | get: { requiresAuthentication: true }, 523 | find: { requiresAuthentication: true }, 524 | create: { }, 525 | update: { requiresAuthentication: true }, 526 | delete: { requiresAuthentication: true }, 527 | addField: { }, 528 | protectedFields: { } 529 | }; 530 | // Don't audit '_Role' as it doesn't work. 531 | const modifiedClasses = ['_User', '_Installation', '_Audience', 'Clock', 'Patient', 'CarePlan', 'Contact', 'Task', 'HealthKitTask', 'Outcome', 'HealthKitOutcome', 'RevisionRecord']; 532 | const accessedClasses = ['_User', '_Installation', '_Audience', 'Clock', 'Patient', 'CarePlan', 'Contact', 'Task', 'HealthKitTask', 'Outcome', 'HealthKitOutcome', 'RevisionRecord']; 533 | ParseAuditor(modifiedClasses, accessedClasses, { classPostfix: '_Audit', useMasterKey: true, clp: auditCLP }); 534 | }; 535 | 536 | function setupDashboard() { 537 | if (enableDashboard) { 538 | const fs = require('fs'); 539 | const ParseDashboard = require('parse-dashboard'); 540 | 541 | const allowInsecureHTTP = process.env.PARSE_DASHBOARD_ALLOW_INSECURE_HTTP; 542 | const cookieSessionSecret = process.env.PARSE_DASHBOARD_COOKIE_SESSION_SECRET; 543 | const trustProxy = process.env.PARSE_DASHBOARD_TRUST_PROXY; 544 | 545 | if (trustProxy && allowInsecureHTTP) { 546 | console.log('Set only trustProxy *or* allowInsecureHTTP, not both. Only one is needed to handle being behind a proxy.'); 547 | process.exit(1); 548 | } 549 | 550 | let configFile = null; 551 | let configFromCLI = null; 552 | const configServerURL = process.env.PARSE_DASHBOARD_SERVER_URL || serverURL; 553 | const configGraphQLServerURL = process.env.PARSE_DASHBOARD_GRAPHQL_SERVER_URL || graphURL.href; 554 | const configPrimaryKey = process.env.PARSE_DASHBOARD_PRIMARY_KEY || primaryKey; 555 | const configAppId = process.env.PARSE_DASHBOARD_APP_ID || applicationId; 556 | const configAppName = process.env.PARSE_DASHBOARD_APP_NAME || appName; 557 | let configUsernames = process.env.PARSE_DASHBOARD_USERNAMES; 558 | let configUserPasswords = process.env.PARSE_DASHBOARD_USER_PASSWORDS; 559 | let configUserPasswordEncrypted = true; 560 | if (process.env.PARSE_DASHBOARD_USER_PASSWORD_ENCRYPTED == 'false') { 561 | configUserPasswordEncrypted = false; 562 | } 563 | 564 | if (!process.env.PARSE_DASHBOARD_CONFIG) { 565 | if (configServerURL && configPrimaryKey && configAppId) { 566 | configFromCLI = { 567 | data: { 568 | apps: [ 569 | { 570 | appId: configAppId, 571 | serverURL: configServerURL, 572 | masterKey: configPrimaryKey, 573 | appName: configAppName, 574 | }, 575 | ] 576 | } 577 | }; 578 | if (configGraphQLServerURL) { 579 | configFromCLI.data.apps[0].graphQLServerURL = configGraphQLServerURL; 580 | } 581 | if (configUsernames && configUserPasswords) { 582 | configUsernames = configUsernames.split(", "); 583 | configUserPasswords = configUserPasswords.split(", "); 584 | if (configUsernames.length == configUserPasswords.length) { 585 | let users = []; 586 | configUsernames.forEach((username, index) => { 587 | users.push({ 588 | user: username, 589 | pass: configUserPasswords[index], 590 | }); 591 | }); 592 | configFromCLI.data.users = users; 593 | configFromCLI.data.useEncryptedPasswords = configUserPasswordEncrypted; 594 | } else { 595 | console.log('Dashboard usernames(' + configUsernames.length + ') ' + 'and passwords(' + configUserPasswords.length + ') must be the same size.'); 596 | process.exit(1); 597 | } 598 | } 599 | } else if (!configServerURL && !configPrimaryKey && !configAppName) { 600 | configFile = path.join(__dirname, 'parse-dashboard-config.json'); 601 | } 602 | } else { 603 | configFromCLI = { 604 | data: JSON.parse(process.env.PARSE_DASHBOARD_CONFIG) 605 | }; 606 | } 607 | 608 | let config = null; 609 | let configFilePath = null; 610 | if (configFile) { 611 | try { 612 | config = { 613 | data: JSON.parse(fs.readFileSync(configFile, 'utf8')) 614 | }; 615 | configFilePath = path.dirname(configFile); 616 | } catch (error) { 617 | if (error instanceof SyntaxError) { 618 | console.log('Your config file contains invalid JSON. Exiting.'); 619 | process.exit(1); 620 | } else if (error.code === 'ENOENT') { 621 | console.log('You must provide either a config file or required CLI options (app ID, Primary Key, and server URL); not both.'); 622 | process.exit(3); 623 | } else { 624 | console.log('There was a problem with your config. Exiting.'); 625 | process.exit(1); 626 | } 627 | } 628 | } else if (configFromCLI) { 629 | config = configFromCLI; 630 | } else { 631 | //Failed to load default config file. 632 | console.log('You must provide either a config file or an app ID, Primary Key, and server URL. See parse-dashboard --help for details.'); 633 | process.exit(4); 634 | } 635 | 636 | config.data.apps.forEach(app => { 637 | if (!app.appName) { 638 | app.appName = app.appId; 639 | } 640 | }); 641 | 642 | if (config.data.iconsFolder && configFilePath) { 643 | config.data.iconsFolder = path.join(configFilePath, config.data.iconsFolder); 644 | } 645 | 646 | if (enableParseServer == false) { 647 | if (allowInsecureHTTP || trustProxy) app.enable('trust proxy', trustProxy); 648 | config.data.trustProxy = trustProxy; 649 | } else { 650 | config.data.trustProxy = configuration.trustProxy; 651 | } 652 | 653 | const dashboardOptions = { allowInsecureHTTP, cookieSessionSecret }; 654 | const dashboard = new ParseDashboard(config.data, dashboardOptions); 655 | app.use(dashboardMountPath, dashboard); 656 | } 657 | } 658 | 659 | if (enableParseServer) { 660 | setupParseServer(); 661 | } else { 662 | setupDashboard(); 663 | const httpServer = require('http').createServer(app); 664 | 665 | if (startLiveQueryServerNoParse == true) { 666 | let liveQueryConfig = { 667 | appId: applicationId, 668 | masterKey: primaryKey, 669 | serverURL: serverURL, 670 | websocketTimeout: websocketTimeout, 671 | cacheTimeout: cacheTimeout, 672 | verbose: verbose, 673 | logLevel: logLevel, 674 | } 675 | 676 | if (("PARSE_SERVER_REDIS_URL" in process.env) || ("REDIS_TLS_URL" in process.env) || ("REDIS_URL" in process.env)) { 677 | liveQueryConfig.redisURL = redisURL; 678 | } 679 | 680 | // This will enable the Live Query real-time server 681 | ParseServer.createLiveQueryServer(httpServer, liveQueryConfig, configuration); 682 | } 683 | 684 | httpServer.listen(port, host, function() { 685 | 686 | if (startLiveQueryServerNoParse) 687 | console.log(`LiveQuery server is now available at ${url.href}`); 688 | 689 | if (enableDashboard) 690 | console.log(`Dashboard is now available at ${dashboardURL.href}`); 691 | }); 692 | } 693 | -------------------------------------------------------------------------------- /parse/parse-dashboard-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "serverURL": "http://localhost:1337/parse", 5 | "graphQLServerURL": "http://localhost:1337/graphql", 6 | "appId": "E036A0C5-6829-4B40-9B3B-3E05F6DF32B2", 7 | "masterKey": "E2466756-93CF-4C05-BA44-FF5D9C34E99F", 8 | "readOnlyMasterKey": "367F7395-2E3A-46B1-ABA3-963A25D533C3", 9 | "appName": "Parse HIPAA", 10 | "supportedPushLocales": ["en"] 11 | }], 12 | "iconsFolder": "icons", 13 | "users": [ 14 | { 15 | "user":"parse", 16 | "pass": "$2a$12$mw0Bulf8PzAw8u.Zb.l0dueKGSV7z8q9bw8857av2e3yTTlC4hRca" 17 | },{ 18 | "user":"parseRead", 19 | "pass": "$2a$12$mw0Bulf8PzAw8u.Zb.l0dueKGSV7z8q9bw8857av2e3yTTlC4hRca", 20 | "readOnly": true 21 | }], 22 | "useEncryptedPasswords": true 23 | } 24 | -------------------------------------------------------------------------------- /parse/process.yml: -------------------------------------------------------------------------------- 1 | apps: 2 | - name : 'parse-hipaa' 3 | script : './index.js' 4 | watch : true 5 | ignore_watch : ['node_modules', 'logs', '.pm2'] 6 | merge_logs : true 7 | cwd : '/parse-server' 8 | exec_mode : 'cluster' 9 | instances : '4' 10 | -------------------------------------------------------------------------------- /parse/scripts/parse_idempotency_delete_expired_records.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | psql -v ON_ERROR_STOP=1 "$DB_URL"?sslmode=require <<-EOSQL 5 | SELECT idempotency_delete_expired_records(); 6 | EOSQL 7 | 8 | exec "$@" 9 | -------------------------------------------------------------------------------- /parse/scripts/setup-dbs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | psql -v ON_ERROR_STOP=1 "$DB_URL"?sslmode=require <<-EOSQL 5 | CREATE EXTENSION postgis; 6 | CREATE EXTENSION postgis_topology; 7 | EOSQL 8 | 9 | exec "$@" 10 | -------------------------------------------------------------------------------- /parse/scripts/setup-parse-index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | psql -v ON_ERROR_STOP=1 "$DB_URL"?sslmode=require <<-EOSQL 6 | CREATE INDEX IF NOT EXISTS "Patient_deletedDate_is_null" ON "Patient" (("deletedDate" IS NULL)) WHERE ("deletedDate" IS NULL); 7 | CREATE INDEX IF NOT EXISTS "Patient_previousVersionUUIDs_array" ON "Patient" USING GIN ("previousVersionUUIDs"); 8 | CREATE INDEX IF NOT EXISTS "Patient_nextVersionUUIDs_array" ON "Patient" USING GIN ("nextVersionUUIDs"); 9 | CREATE INDEX IF NOT EXISTS "CarePlan_deletedDate_is_null" ON "CarePlan" (("deletedDate" IS NULL)) WHERE ("deletedDate" IS NULL); 10 | CREATE INDEX IF NOT EXISTS "CarePlan_previousVersionUUIDs_array" ON "CarePlan" USING GIN ("previousVersionUUIDs"); 11 | CREATE INDEX IF NOT EXISTS "CarePlan_nextVersionUUIDs_array" ON "CarePlan" USING GIN ("nextVersionUUIDs"); 12 | CREATE INDEX IF NOT EXISTS "Contact_deletedDate_is_null" ON "Contact" (("deletedDate" IS NULL)) WHERE ("deletedDate" IS NULL); 13 | CREATE INDEX IF NOT EXISTS "Contact_previousVersionUUIDs_array" ON "Contact" USING GIN ("previousVersionUUIDs"); 14 | CREATE INDEX IF NOT EXISTS "Contact_nextVersionUUIDs_array" ON "Contact" USING GIN ("nextVersionUUIDs"); 15 | CREATE INDEX IF NOT EXISTS "Task_deletedDate_is_null" ON "Task" (("deletedDate" IS NULL)) WHERE ("deletedDate" IS NULL); 16 | CREATE INDEX IF NOT EXISTS "Task_previousVersionUUIDs_array" ON "Task" USING GIN ("previousVersionUUIDs"); 17 | CREATE INDEX IF NOT EXISTS "Task_nextVersionUUIDs_array" ON "Task" USING GIN ("nextVersionUUIDs"); 18 | CREATE INDEX IF NOT EXISTS "HealthKitTask_deletedDate_is_null" ON "HealthKitTask" (("deletedDate" IS NULL)) WHERE ("deletedDate" IS NULL); 19 | CREATE INDEX IF NOT EXISTS "HealthKitTask_previousVersionUUIDs_array" ON "HealthKitTask" USING GIN ("previousVersionUUIDs"); 20 | CREATE INDEX IF NOT EXISTS "HealthKitTask_nextVersionUUIDs_array" ON "HealthKitTask" USING GIN ("nextVersionUUIDs"); 21 | CREATE INDEX IF NOT EXISTS "Outcome_deletedDate_is_null" ON "Outcome" (("deletedDate" IS NULL)) WHERE ("deletedDate" IS NULL); 22 | CREATE INDEX IF NOT EXISTS "Outcome_previousVersionUUIDs_array" ON "Outcome" USING GIN ("previousVersionUUIDs"); 23 | CREATE INDEX IF NOT EXISTS "Outcome_nextVersionUUIDs_array" ON "Outcome" USING GIN ("nextVersionUUIDs"); 24 | EOSQL 25 | 26 | exec "$@" 27 | -------------------------------------------------------------------------------- /parse/scripts/setup-pgaudit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | psql -v ON_ERROR_STOP=1 "$DB_URL"?sslmode=require <<-EOSQL 5 | CREATE EXTENSION pgaudit; 6 | ALTER SYSTEM SET pgaudit.log_catalog = off; 7 | ALTER SYSTEM SET pgaudit.log = 'all, -misc'; 8 | ALTER SYSTEM SET pgaudit.log_relation = 'on'; 9 | ALTER SYSTEM SET pgaudit.log_parameter = 'on'; 10 | EOSQL 11 | 12 | exec "$@" 13 | -------------------------------------------------------------------------------- /parse/scripts/wait-for-postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # wait-for-postgres.sh 3 | 4 | set -e 5 | 6 | cmd="$@" 7 | 8 | until psql "$DB_URL"?sslmode=require -c '\q'; do 9 | >&2 echo "Postgres is unavailable - parse-hipaa is sleeping" 10 | sleep 1 11 | done 12 | 13 | >&2 echo "Postgres is up - executing command" 14 | exec $cmd 15 | -------------------------------------------------------------------------------- /scripts/wait-for-postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # wait-for-postgres.sh 3 | 4 | set -eo pipefail 5 | 6 | host="$1" 7 | shift 8 | cmd="$@" 9 | timeout=60 10 | start_time=$(date +%s) 11 | 12 | until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$host" -U "$POSTGRES_USER" -c '\q'; do 13 | >&2 echo "Postgres is unavailable on ${host} - parse-server is sleeping" 14 | sleep 1 15 | 16 | current_time=$(date +%s) 17 | elapsed_time=$((current_time - start_time)) 18 | 19 | if [ "$elapsed_time" -gt "$timeout" ]; then 20 | >&2 echo "Timed out while waiting for Postgres to become available on ${host}" 21 | exit 1 22 | fi 23 | done 24 | 25 | >&2 echo "Postgres is up - executing command: $cmd" 26 | exec $cmd 27 | -------------------------------------------------------------------------------- /singularity-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.0" 2 | 3 | instances: 4 | parse: 5 | image: oras://ghcr.io/netreconlab/parse-hipaa:latest 6 | ports: 7 | - 1337:1337 8 | volumes: 9 | - ./scripts/wait-for-postgres.sh:/parse-server/wait-for-postgres.sh 10 | - ./parse/index.js:/parse-server/index.js 11 | - ./parse/cloud:/parse-server/cloud 12 | - ./files:/parse-server/files # All files uploaded from users are stored to an ecrypted drive locally for HIPAA compliance 13 | start: 14 | - fakeroot 15 | exec: 16 | options: 17 | - "env-file=general.env" 18 | command: ./wait-for-postgres.sh db node index.js 19 | depends_on: 20 | - db 21 | db: 22 | image: oras://ghcr.io/netreconlab/hipaa-postgres:latest 23 | ports: 24 | - 5432:5432 25 | start: 26 | - fakeroot 27 | exec: 28 | options: 29 | - "env-file=general.env" 30 | command: postgres -c shared_preload_libraries=pgaudit 31 | # Uncomment volumes below to persist postgres data. Make sure to change directory to store data locally 32 | # volumes: 33 | # - /My/Encrypted/Drive/data:/var/lib/postgresql/data #Mount your own drive 34 | # - /My/Encrypted/Drive/archivedir:/var/lib/postgresql/archivedir #Mount your own drive 35 | #dashboard: 36 | # image: oras://ghcr.io/parseplatform/parse-hipaa-dashboard:latest 37 | # start: 38 | # - fakeroot 39 | # exec: 40 | # options: 41 | # - "env-file=dashboard.env" 42 | # command: node /parse-hipaa-dashboard/index.js 43 | # volumes: 44 | # - ./dashboard/parse-dashboard-config.json:/parse-hipaa-dashboard/lib/parse-dashboard-config.json 45 | # ports: 46 | # - 4040:4040 47 | # depends_on: 48 | # - parse 49 | --------------------------------------------------------------------------------