├── .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://hub.docker.com/r/netreconlab/parse-hipaa)
4 | [](https://github.com/netreconlab/parse-hipaa/actions/workflows/build.yml)
5 | [](https://github.com/netreconlab/parse-hipaa/actions/workflows/build-dashboard.yml)
6 | [](https://github.com/netreconlab/parse-hipaa/actions/workflows/release.yml)
7 | [](https://github.com/netreconlab/parse-hipaa/actions/workflows/release-dashboard.yml)
8 |
9 | ---
10 |
11 | 
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 | [](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 |
--------------------------------------------------------------------------------