mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of github.com:Human-Connection/Human-Connection into helm
This commit is contained in:
commit
aa799e6f6b
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
backend/snapshots/* linguist-generated=true
|
||||
|
||||
90
.gitbook/assets/browserstack-logo.svg
Normal file
90
.gitbook/assets/browserstack-logo.svg
Normal file
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 490.1 105.6" style="enable-background:new 0 0 490.1 105.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#F4B960;}
|
||||
.st1{fill:#E66F32;}
|
||||
.st2{fill:#E43C41;}
|
||||
.st3{fill:#BDD041;}
|
||||
.st4{fill:#6DB54C;}
|
||||
.st5{fill:#AEDAE6;}
|
||||
.st6{fill:#56B8DE;}
|
||||
.st7{fill:#00B1D5;}
|
||||
.st8{fill:url(#SVGID_1_);}
|
||||
.st9{fill:#221F1F;}
|
||||
.st10{fill:#FFFFFF;}
|
||||
.st11{fill:#000111;}
|
||||
</style>
|
||||
<title>Browserstack-logo-white</title>
|
||||
<circle class="st0" cx="52.8" cy="52.8" r="52.8"/>
|
||||
<circle class="st1" cx="47.5" cy="47.5" r="47.5"/>
|
||||
<circle class="st2" cx="53.8" cy="41.1" r="41.1"/>
|
||||
<circle class="st3" cx="57.1" cy="44.4" r="37.8"/>
|
||||
<circle class="st4" cx="54.3" cy="47.2" r="35.1"/>
|
||||
<circle class="st5" cx="48.8" cy="41.7" r="29.5"/>
|
||||
<circle class="st6" cx="53.6" cy="36.8" r="24.7"/>
|
||||
<circle class="st7" cx="56.6" cy="39.9" r="21.7"/>
|
||||
<radialGradient id="SVGID_1_" cx="53.45" cy="63.02" r="18.57" gradientTransform="matrix(1 0 0 -1 0 106)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#797979"/>
|
||||
<stop offset="1" style="stop-color:#4C4C4C"/>
|
||||
</radialGradient>
|
||||
<circle class="st8" cx="53.5" cy="43" r="18.6"/>
|
||||
<circle class="st9" cx="53.5" cy="43" r="18.6"/>
|
||||
<ellipse transform="matrix(0.4094 -0.9123 0.9123 0.4094 2.8913 76.9251)" class="st10" cx="60.9" cy="36.2" rx="5.7" ry="3.7"/>
|
||||
<path class="st11" d="M122.5,32.6c0-0.3,0.3-0.6,0.6-0.6c0,0,0,0,0.1,0h16.6c9.5,0,13.9,4.4,13.9,11c0.2,3.7-1.8,7.2-5.2,8.8v0.1
|
||||
c3.7,1.5,6.1,5.2,6,9.3c0,8.2-5.6,12.2-15.4,12.2h-16c-0.3,0-0.6-0.2-0.7-0.5c0,0,0,0,0-0.1L122.5,32.6L122.5,32.6z M139.6,49.1
|
||||
c3.9,0,6.4-2.2,6.4-5.4s-2.4-5.5-6.4-5.5h-8.9c-0.2,0-0.4,0.1-0.4,0.3c0,0,0,0,0,0.1v10.2c0,0.2,0.1,0.3,0.3,0.4c0,0,0,0,0.1,0
|
||||
H139.6L139.6,49.1z M130.6,66.9h9.3c4.3,0,6.8-2.3,6.8-5.8s-2.4-5.7-6.7-5.7h-9.3c-0.2,0-0.4,0.1-0.4,0.3c0,0,0,0,0,0.1v10.7
|
||||
C130.3,66.8,130.4,66.9,130.6,66.9C130.6,66.9,130.6,66.9,130.6,66.9L130.6,66.9z"/>
|
||||
<path class="st11" d="M159.9,73.3c-0.3,0-0.6-0.2-0.7-0.5c0,0,0,0,0-0.1V44.6c0-0.3,0.3-0.6,0.6-0.6c0,0,0,0,0.1,0h6
|
||||
c0.3,0,0.6,0.2,0.7,0.5c0,0,0,0,0,0.1v2.5h0.1c1.5-2.2,4.2-3.8,8.2-3.8c2.4,0,4.8,0.8,6.6,2.4c0.3,0.3,0.4,0.5,0.1,0.8l-3.5,4.1
|
||||
c-0.2,0.3-0.6,0.4-0.9,0.2c0,0,0,0-0.1,0c-1.4-0.9-3-1.4-4.7-1.4c-4.1,0-6,2.7-6,7.4v15.9c0,0.3-0.3,0.6-0.6,0.6c0,0,0,0-0.1,0
|
||||
H159.9L159.9,73.3z"/>
|
||||
<path class="st11" d="M182.9,65.8c-0.8-2.3-1.1-4.8-1.1-7.2c-0.1-2.5,0.3-4.9,1.1-7.2c1.8-5.1,6.6-8.1,13.1-8.1s11.2,3,13,8.1
|
||||
c0.8,2.3,1.1,4.8,1.1,7.2c0.1,2.5-0.3,4.9-1.1,7.2c-1.8,5.1-6.6,8.1-13,8.1S184.7,71,182.9,65.8z M201.9,64c0.5-1.7,0.8-3.6,0.7-5.4
|
||||
c0.1-1.8-0.1-3.7-0.7-5.4c-0.9-2.5-3.3-4-5.9-3.8c-2.6-0.2-5.1,1.4-6,3.8c-0.5,1.8-0.8,3.6-0.7,5.4c-0.1,1.8,0.1,3.7,0.7,5.4
|
||||
c0.9,2.5,3.4,4,6,3.8C198.6,68,201,66.5,201.9,64L201.9,64z"/>
|
||||
<path class="st11" d="M241.9,73.3c-0.4,0-0.7-0.3-0.8-0.6L235,53.9h-0.1l-6.2,18.7c-0.1,0.4-0.4,0.6-0.8,0.6h-5.4
|
||||
c-0.4,0-0.7-0.3-0.8-0.6l-10-28.1c-0.1-0.2,0-0.5,0.2-0.6c0.1,0,0.2-0.1,0.3,0h6.3c0.4,0,0.8,0.2,0.9,0.6l6.1,19.3h0.1l6-19.3
|
||||
c0.1-0.4,0.5-0.6,0.9-0.6h4.7c0.4,0,0.7,0.2,0.9,0.6l6.4,19.3h0.1l5.8-19.3c0.1-0.4,0.5-0.7,0.9-0.6h6.3c0.2-0.1,0.5,0.1,0.5,0.3
|
||||
c0,0.1,0,0.2,0,0.3l-10,28.1c-0.1,0.4-0.4,0.6-0.8,0.6L241.9,73.3L241.9,73.3z"/>
|
||||
<path class="st11" d="M259.3,69.3c-0.2-0.2-0.3-0.6-0.1-0.8c0,0,0,0,0.1-0.1l3.7-3.6c0.3-0.2,0.7-0.2,0.9,0c2.6,2.1,5.9,3.3,9.3,3.3
|
||||
c3.9,0,5.9-1.5,5.9-3.5c0-1.8-1.1-2.9-5.2-3.2l-3.4-0.3c-6.4-0.6-9.7-3.6-9.7-8.6c0-5.7,4.4-9.2,12.3-9.2c4.2-0.1,8.4,1.2,11.9,3.6
|
||||
c0.3,0.2,0.3,0.5,0.2,0.8c0,0,0,0,0,0.1l-3.2,3.6c-0.2,0.3-0.6,0.3-0.9,0.1c-2.5-1.5-5.4-2.4-8.3-2.4c-3.1,0-4.8,1.3-4.8,3
|
||||
s1.1,2.7,5.2,3.1l3.4,0.3c6.6,0.6,9.8,3.8,9.8,8.6c0,5.8-4.6,9.9-13.3,9.9C268,74,263.2,72.4,259.3,69.3z"/>
|
||||
<path class="st11" d="M291.2,65.8c-0.8-2.3-1.2-4.7-1.1-7.2c-0.1-2.5,0.3-4.9,1-7.2c1.8-5.1,6.6-8.1,12.9-8.1c6.5,0,11.2,3.1,13,8.1
|
||||
c0.7,2.1,1,4.1,1,8.8c0,0.3-0.3,0.6-0.6,0.6c0,0-0.1,0-0.1,0h-19.5c-0.2,0-0.4,0.1-0.4,0.3c0,0,0,0,0,0.1c0,0.8,0.2,1.5,0.5,2.2
|
||||
c1,2.9,3.5,4.4,7.1,4.4c2.7,0.1,5.4-0.9,7.4-2.8c0.2-0.3,0.7-0.4,1-0.1c0,0,0,0,0,0l3.9,3.2c0.2,0.1,0.3,0.5,0.2,0.7
|
||||
c0,0.1-0.1,0.1-0.1,0.1c-2.7,2.9-7.2,5-13,5C297.8,73.9,293,70.9,291.2,65.8z M310.4,52.8c-0.9-2.4-3.2-3.8-6.2-3.8
|
||||
s-5.4,1.4-6.2,3.8c-0.3,0.8-0.4,1.6-0.4,2.5c0,0.2,0.1,0.3,0.3,0.4c0,0,0,0,0.1,0h12.4c0.2,0,0.4-0.1,0.4-0.3c0,0,0,0,0-0.1
|
||||
C310.8,54.5,310.6,53.6,310.4,52.8L310.4,52.8z"/>
|
||||
<path class="st11" d="M323.6,73.3c-0.3,0-0.6-0.2-0.7-0.5c0,0,0,0,0-0.1V44.6c0-0.3,0.3-0.6,0.6-0.6c0,0,0,0,0.1,0h6
|
||||
c0.3,0,0.6,0.2,0.7,0.5c0,0,0,0,0,0.1v2.5h0.1c1.5-2.2,4.2-3.8,8.2-3.8c2.4,0,4.8,0.8,6.6,2.4c0.3,0.3,0.4,0.5,0.1,0.8l-3.5,4.1
|
||||
c-0.2,0.3-0.6,0.4-0.9,0.2c0,0,0,0-0.1,0c-1.4-0.9-3-1.4-4.7-1.4c-4.1,0-6,2.7-6,7.4v15.9c0,0.3-0.3,0.6-0.6,0.6c0,0,0,0-0.1,0
|
||||
H323.6L323.6,73.3z"/>
|
||||
<path class="st11" d="M346.5,68.5c-0.3-0.2-0.4-0.6-0.2-0.9c0,0,0,0,0,0l4.1-4.4c0.2-0.3,0.6-0.3,0.9-0.1c0,0,0,0,0,0
|
||||
c3.5,2.7,7.7,4.2,12.1,4.4c5.3,0,8.4-2.5,8.4-6c0-3-2-4.9-8.1-5.7l-2.4-0.3c-8.6-1.1-13.5-4.9-13.5-11.8c0-7.5,5.9-12.4,15.1-12.4
|
||||
c5.1-0.1,10.1,1.4,14.5,4.2c0.3,0.1,0.4,0.4,0.2,0.7c0,0.1-0.1,0.1-0.1,0.2l-3.1,4.5c-0.2,0.3-0.6,0.4-0.9,0.2
|
||||
c-3.2-2.1-6.9-3.2-10.7-3.2c-4.5,0-7,2.3-7,5.5c0,2.9,2.2,4.8,8.2,5.6l2.4,0.3c8.6,1.1,13.3,4.9,13.3,12c0,7.3-5.7,12.8-16.8,12.8
|
||||
C356.3,73.9,350,71.5,346.5,68.5z"/>
|
||||
<path class="st11" d="M393.3,73.8c-6.4,0-8.8-2.9-8.8-8.6V49.8c0-0.2-0.1-0.3-0.3-0.4c0,0,0,0-0.1,0H382c-0.3,0-0.6-0.2-0.7-0.5
|
||||
c0,0,0,0,0-0.1v-4.1c0-0.3,0.3-0.6,0.6-0.6c0,0,0,0,0.1,0h2.1c0.2,0,0.4-0.1,0.4-0.3c0,0,0,0,0-0.1v-8c0-0.3,0.3-0.6,0.6-0.6
|
||||
c0,0,0,0,0.1,0h6c0.3,0,0.6,0.2,0.7,0.5c0,0,0,0,0,0.1v8c0,0.2,0.1,0.3,0.3,0.4c0,0,0,0,0.1,0h4.2c0.3,0,0.6,0.2,0.7,0.5
|
||||
c0,0,0,0,0,0.1v4.1c0,0.3-0.3,0.6-0.6,0.6c0,0,0,0-0.1,0h-4.2c-0.2,0-0.4,0.1-0.4,0.3c0,0,0,0,0,0.1V65c0,2.1,0.9,2.7,3,2.7h1.6
|
||||
c0.3,0,0.6,0.2,0.7,0.5c0,0,0,0,0,0.1v4.9c0,0.3-0.3,0.6-0.6,0.6c0,0,0,0-0.1,0L393.3,73.8L393.3,73.8z"/>
|
||||
<path class="st11" d="M421.2,73.3c-0.3,0-0.6-0.2-0.7-0.5c0,0,0,0,0-0.1v-2.1h0c-1.5,2-4.5,3.4-8.9,3.4c-5.8,0-10.6-2.8-10.6-8.9
|
||||
c0-6.4,4.9-9.3,12.7-9.3h6.4c0.2,0,0.4-0.1,0.4-0.3c0,0,0,0,0-0.1v-1.4c0-3.3-1.7-4.9-7-4.9c-2.6-0.1-5.1,0.6-7.2,2
|
||||
c-0.3,0.2-0.7,0.2-0.9-0.1c0,0,0,0,0-0.1l-2.4-4c-0.2-0.2-0.1-0.6,0.1-0.8c0,0,0,0,0,0c2.6-1.7,6-2.9,11.2-2.9
|
||||
c9.6,0,13.2,3,13.2,10.2v19.1c0,0.3-0.3,0.6-0.6,0.6c0,0,0,0-0.1,0H421.2L421.2,73.3z M420.4,63.4v-2.2c0-0.2-0.1-0.3-0.3-0.4
|
||||
c0,0,0,0-0.1,0h-5.2c-4.7,0-6.8,1.2-6.8,3.9c0,2.4,1.9,3.6,5.5,3.6C417.9,68.4,420.4,66.8,420.4,63.4L420.4,63.4z"/>
|
||||
<path class="st11" d="M433.1,65.8c-0.7-2.3-1.1-4.8-1-7.2c-0.1-2.4,0.3-4.9,1-7.2c1.8-5.2,6.7-8.1,13.1-8.1c4.2-0.2,8.2,1.5,11,4.6
|
||||
c0.2,0.2,0.2,0.6,0,0.8c0,0,0,0-0.1,0.1l-4.1,3.3c-0.3,0.2-0.7,0.2-0.9-0.1c0,0,0,0,0-0.1c-1.5-1.7-3.6-2.6-5.9-2.5
|
||||
c-2.8,0-5,1.3-5.9,3.8c-0.5,1.8-0.8,3.6-0.7,5.4c-0.1,1.8,0.1,3.7,0.7,5.5c0.9,2.5,3.1,3.8,5.9,3.8c2.2,0.1,4.4-0.9,5.9-2.6
|
||||
c0.2-0.3,0.6-0.3,0.9-0.1c0,0,0,0,0,0l4.1,3.3c0.3,0.2,0.3,0.5,0.1,0.8c0,0,0,0-0.1,0.1c-2.9,3-6.9,4.6-11,4.5
|
||||
C439.8,73.9,435,71.1,433.1,65.8z"/>
|
||||
<path class="st11" d="M482.8,73.3c-0.4,0-0.8-0.2-1-0.6l-8-12.3l-4.3,4.6v7.7c0,0.3-0.3,0.6-0.6,0.6c0,0,0,0-0.1,0h-6
|
||||
c-0.3,0-0.6-0.2-0.7-0.5c0,0,0,0,0-0.1V32.6c0-0.3,0.3-0.6,0.6-0.6c0,0,0,0,0.1,0h6c0.3,0,0.6,0.2,0.7,0.5c0,0,0,0,0,0.1v23.8
|
||||
l10.8-11.8c0.3-0.4,0.8-0.6,1.2-0.6h6.7c0.2,0,0.4,0.1,0.4,0.3c0,0.1,0,0.3-0.1,0.3l-10.1,10.7L490,72.7c0.1,0.2,0.1,0.4,0,0.5
|
||||
c-0.1,0.1-0.2,0.1-0.3,0.1H482.8L482.8,73.3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
BIN
.gitbook/assets/storybook-output.png
Normal file
BIN
.gitbook/assets/storybook-output.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -6,11 +6,19 @@ title: 🚀 [Feature]
|
||||
---
|
||||
|
||||
## :rocket: Feature
|
||||
<!-- Describe the Feature. Use Screenshots if possible. -->
|
||||
<!-- Give a short summary of the Feature. Use Screenshots if you want. -->
|
||||
|
||||
### User Problem
|
||||
<!-- Which problem is this solving? Why do you think this is important? Who will benefit from it and how? -->
|
||||
|
||||
### Implementation
|
||||
<!-- How do you think this feature should be implemented? How will it be used? Where in the network should it be located? Which steps and screens are involved? -->
|
||||
|
||||
### Design & Layout
|
||||
<!-- Attach Screenshots and Drawings. -->
|
||||
<!-- Attach Screenshots and Sketches to illustrate your idea. -->
|
||||
|
||||
### Validation
|
||||
<!-- How can we make sure that this feature indeed solves the above problem? How do we know if it has been accepted by the users of the network, once released? -->
|
||||
|
||||
### Additional context
|
||||
<!-- Add any other context or screenshots about the feature request here.-->
|
||||
<!-- Add other context or background about the feature request here.-->
|
||||
|
||||
18
.github/stale.yml
vendored
Normal file
18
.github/stale.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 30
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- bounty
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,3 +18,4 @@ cypress.env.json
|
||||
**/coverage
|
||||
|
||||
release/
|
||||
*~
|
||||
25
.travis.yml
25
.travis.yml
@ -6,20 +6,19 @@ addons:
|
||||
- libgconf-2-4
|
||||
snaps:
|
||||
- docker
|
||||
- chromium
|
||||
|
||||
before_install:
|
||||
install:
|
||||
- yarn global add wait-on
|
||||
# Install Codecov
|
||||
- yarn install
|
||||
- cp cypress.env.template.json cypress.env.json
|
||||
- cp backend/.env.template backend/.env
|
||||
|
||||
install:
|
||||
before_script:
|
||||
- docker-compose -f docker-compose.yml build --parallel
|
||||
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml build # just tagging, just be quite fast
|
||||
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml up -d
|
||||
- wait-on http://localhost:7474
|
||||
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml exec neo4j db_setup
|
||||
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml exec backend yarn run db:migrate init
|
||||
|
||||
script:
|
||||
- export CYPRESS_RETRIES=1
|
||||
@ -27,22 +26,18 @@ script:
|
||||
- echo "TRAVIS_BRANCH=$TRAVIS_BRANCH, PR=$PR, BRANCH=$BRANCH"
|
||||
# Backend
|
||||
- docker-compose exec backend yarn run lint
|
||||
- docker-compose exec backend yarn run test:jest --ci --verbose=false --coverage
|
||||
- docker-compose exec backend yarn run test --ci --verbose=false --coverage
|
||||
- docker-compose exec backend yarn run db:seed
|
||||
- docker-compose exec backend yarn run db:reset
|
||||
# ActivityPub cucumber testing temporarily disabled because it's too buggy
|
||||
# - docker-compose exec backend yarn run test:cucumber --tags "not @wip"
|
||||
# - docker-compose exec backend yarn run db:reset
|
||||
# - docker-compose exec backend yarn run db:seed
|
||||
# Frontend
|
||||
- docker-compose exec webapp yarn run lint
|
||||
- docker-compose exec webapp yarn run test --ci --verbose=false --coverage
|
||||
- docker-compose exec -d backend yarn run test:before:seeder
|
||||
# Fullstack
|
||||
- docker-compose down
|
||||
- docker-compose -f docker-compose.yml up -d
|
||||
- wait-on http://localhost:7474
|
||||
- yarn run cypress:run
|
||||
- yarn run cypress:run --record
|
||||
- yarn run cucumber
|
||||
# Coverage
|
||||
- yarn run codecov
|
||||
|
||||
@ -67,14 +62,14 @@ before_deploy:
|
||||
|
||||
deploy:
|
||||
- provider: script
|
||||
script: scripts/docker_push.sh
|
||||
script: bash scripts/docker_push.sh
|
||||
on:
|
||||
branch: master
|
||||
- provider: script
|
||||
script: scripts/deploy.sh
|
||||
script: bash scripts/deploy.sh
|
||||
on:
|
||||
branch: master
|
||||
- provider: script
|
||||
script: scripts/github_release.sh
|
||||
script: bash scripts/github_release.sh
|
||||
on:
|
||||
branch: master
|
||||
|
||||
7
.versionrc.json
Normal file
7
.versionrc.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"bumpFiles": [
|
||||
"package.json",
|
||||
"backend/package.json",
|
||||
"webapp/package.json"
|
||||
]
|
||||
}
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -7,6 +7,5 @@
|
||||
"autoFix": true
|
||||
}
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.autoFixOnSave": true
|
||||
"editor.formatOnSave": false,
|
||||
}
|
||||
|
||||
2068
CHANGELOG.md
Normal file
2068
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
142
CONTRIBUTING.md
142
CONTRIBUTING.md
@ -1,79 +1,101 @@
|
||||
# CONTRIBUTING
|
||||
|
||||
Thanks so much for thinking of contributing to the Human Connection project, we really appreciate it! :-\)
|
||||
Thank you so much for thinking of contributing to the Human Connection project! It's awesome you're here, we really appreciate it. :-\)
|
||||
|
||||
## Getting Set Up
|
||||
|
||||
Instructions for how to install all the necessary software can be found in our [documentation](https://docs.human-connection.org/human-connection/).
|
||||
Instructions for how to install all the necessary software and some code guidelines can be found in our [documentation](https://docs.human-connection.org/human-connection/).
|
||||
|
||||
We recommend that new folks should ideally work together with an existing developer. Please join our [discord](https://discord.gg/6ub73U3) instance to chat with developers or just ask them in tickets in [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f/boards?repos=152252353):
|
||||
To get you started we recommend that you join forces with a regular contributor. Please join [our discord instance](https://human-connection.org/discord) to chat with developers or just get in touch directly on an issue on either [Github](https://github.com/Human-Connection/Human-Connection/issues) or [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f/boards?repos=152252353):
|
||||
|
||||

|
||||
|
||||
Here are some general notes on our development flow:
|
||||
We also have regular pair programming sessions that you are very welcome to join! We feel this is often the best way to get to know both the project and the team. Most developers are also available for spontaneous sessions if the times listed below don't work for you – just ping us on discord.
|
||||
|
||||
## Development
|
||||
## Development Flow
|
||||
|
||||
* Currently operating in two week sprints
|
||||
* We are using ZenHub to coordinate
|
||||
* estimating time per issue is the crucial feature of [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f) that Github does not have
|
||||
* "up-for-grabs" links to [Github project](https://github.com/Human-Connection/Human-Connection/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||
* ordering on ZenHub not necessarily reflected on github projects
|
||||
* AgileVentures run open pairing sessions at 10:30am UTC each week on Tuesdays and Thursdays
|
||||
* Core team
|
||||
* all the people who are hired by HC non-profit corporation
|
||||
* you can Meet-the-team [every two weeks in German](https://human-connection.org/veranstaltungen/) and [every month in English](https://human-connection.org/en/events/).
|
||||
* 9 people
|
||||
* 2 core developers \(Robert [@roschaefer](https://github.com/roschaefer) and Greg [@appinteractive](https://github.com/appinteractive)\)
|
||||
* 3 marketeers Jasi, Dennis and Sensi
|
||||
* Hardy doing business development
|
||||
* Martin head of IT and previously data protection officer
|
||||
* Victor doing accounting and controlling
|
||||
* Nicolas is the community manager \(reviews content in the network\) reflects community opinion back to the core team
|
||||
* when can folks pair with Robert
|
||||
* 10am UTC until 5pm UTC every working day
|
||||
We operate in two week sprints that are planned, estimated and prioritised on [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f). All issues are also linked to and synced with [Github](https://github.com/Human-Connection/Human-Connection/issues). Look for the `good first issue` label if you're not sure where to start!
|
||||
|
||||
We try to discuss all questions directly related to a feature or bug in the respective issue, in order to preserve it for the future and for other developers. We use discord for real-time communication.
|
||||
|
||||
This is how we solve bugs and implement features, step by step:
|
||||
1. We find an issue we want to work on, usually during the sprint planning but as an open source contributor this can happen at any time.
|
||||
2. We communicate with the team to see if the issue is still available. (When you comment on an issue but don't get an answer there within 1-2 days try to mention @Human-Connection/hc-dev-team to make sure we check in.)
|
||||
3. We make sure we understand the issue in detail – what problem is it solving and how should it be implemented?
|
||||
4. We assign ourselves to the issue and move it to `In Progress` on [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f).
|
||||
5. We start working on it in a `new branch` and open a `pull request` prefixed with `[WIP]` (work in progress) to which we regularly push our changes.
|
||||
6. When questions come up we clarify them with the team (directly in the issue on Github).
|
||||
7. When we are happy with our work and our PR is passing all tests we remove the `[WIP]` from the PR description and ask for reviews (if you're not sure who to ask there is @Human-Connection/hc-dev-team which pings all core developers).
|
||||
8. We then incorporate the suggestions from the reviews into our work and once it has been approved it can be merged into master!
|
||||
|
||||
Every pull request needs to:
|
||||
* fix an issue (if there is something you want to work on but there is no issue for it, create one first and discuss it with the team)
|
||||
* include tests for the code that is added or changed
|
||||
* pass all tests (linter, backend, frontend, end-to-end)
|
||||
* be approved by at least 1 developer who is not the owner of the PR (when more than 10 files were changed it needs 2 approvals)
|
||||
|
||||
## The Team
|
||||
|
||||
There are many volunteers all around the world helping us build this network and without their contributions we wouldn't be where we are today. Big thank you to all of you!
|
||||
|
||||
You can see the core team behind Human Connection [on our website](https://human-connection.org/en/the-team/). On Github you will mostly run into our developers:
|
||||
* Robert (@roschaefer)
|
||||
* Matt (@mattwr18)
|
||||
* Wolle (@Tirokk)
|
||||
* Alex (@ogerly)
|
||||
* Alina (@alina-beck)
|
||||
* Martin (@datenbrei), our head of IT
|
||||
* and sometimes Dennis (@DennisHack), the founder of Human Connection
|
||||
|
||||
## Meetings and Pair Programming Sessions
|
||||
|
||||
Times below refer to **German Time** – that's CET (GMT+1) in winter and CEST (GMT+2) in summer – because most Human Connection core team members are living in Germany.
|
||||
|
||||
Daily standup
|
||||
* every Monday–Friday 11:30
|
||||
* in the discord `Conference Room`
|
||||
* all contributors welcome!
|
||||
* everybody shares what they are working on and asks for help if they are blocked
|
||||
|
||||
Regular pair programming sessions
|
||||
* every Monday, Wednesday and Thursday 15:00
|
||||
* the link will be posted in the [discord chat](https://discord.gg/6ub73U3) and on the [Agile Ventures website](https://www.agileventures.org/events?utf8=%E2%9C%93&project_id=220&commit=Filter+by+Project)
|
||||
* all contributors welcome!
|
||||
* we team up and work on an issue together (often using Visual Studio live sharing sessions)
|
||||
|
||||
Open-Source Community Meeting
|
||||
* every Thursday 13:00
|
||||
* the link will be posted in the [discord chat](https://discord.gg/6ub73U3) and on the [Agile Ventures website](https://www.agileventures.org/events?utf8=%E2%9C%93&project_id=220&commit=Filter+by+Project)
|
||||
* all contributors welcome!
|
||||
|
||||
Meet the team
|
||||
* every Monday 21:00 (at the moment only in German)
|
||||
* details here https://human-connection.org/veranstaltungen/
|
||||
* via this [zoom link](https://zoom.us/j/936943532)
|
||||
* all contributors and users of the network welcome!
|
||||
* users of the network chat with the Human Connection team and discuss current questions and issues
|
||||
|
||||
Sprint planning
|
||||
* bi-weekly on Tuesday 13:00
|
||||
* via this [zoom link](https://zoom.us/j/7743582385)
|
||||
* all contributors welcome (recommended for those who want to work on an issue in this sprint)
|
||||
* we select and prioritise the issues we will work on in the following two weeks
|
||||
|
||||
Sprint retrospective
|
||||
* bi-weekly on Monday 13:00
|
||||
* via this [zoom link](https://zoom.us/j/7743582385)
|
||||
* all contributors welcome (most interesting for those who participated in the sprint)
|
||||
* we review the past sprint and talk about what went well and what we could improve
|
||||
|
||||
## Philosophy
|
||||
|
||||
We practise [collective code ownership](http://www.extremeprogramming.org/rules/collective.html) rather than strong code ownership, which means that:
|
||||
|
||||
* anyone can start working on anyone elses code
|
||||
* we avoid blocking because someone else isn't working on something
|
||||
* however it's sometimes good to leave something in order to create successful education experience
|
||||
* developers can make contributions to other people's PRs (after checking in with them)
|
||||
* we avoid blocking because someone else isn't working, so we sometimes take over PRs from other developers
|
||||
* everyone should always push their code to branches so others can see it
|
||||
|
||||
Everyone feel free to request merges or answers to issues from the project managers
|
||||
We believe in open source contributions as a learning experience – everyone is welcome to join our team of volunteers and to contribute to the project, no matter their background or level of experience.
|
||||
|
||||
But what do we do when waiting for merge into master \(wanting to keep PRs small\) --> Robert recommends creating a pull request for each step
|
||||
We use pair programming sessions as a tool for knowledge sharing. We can learn a lot from each other and only by sharing what we know and overcoming challenges together can we grow as a team and truly own this project collectively.
|
||||
|
||||
* programming is also about thinking about other people - empathy for your co-workers
|
||||
* but what about when you are waiting for merge?
|
||||
* solutions
|
||||
* 1\) put 2nd PR into branch that the first PR is hitting - but requires update after merging
|
||||
* 2\) prefer to leave exiting PR until it can be reviewed, and instead go and work on some other part of the codebase that is not impacted by the first PR
|
||||
|
||||
### Code Review
|
||||
* Github setting in place - at least one review is required to merge
|
||||
- in principle anyone (who is not the PR owner) can review
|
||||
- but often it will be the core developers (Robert, Ulf, Greg, Wolfgang?)
|
||||
- once there is a review, and presuming no requested changes, PR opener can merge
|
||||
|
||||
* CI/tests
|
||||
- the CI needs to pass
|
||||
- linting <-- autofix?
|
||||
- tests (unit, feature) (backend, frontend)
|
||||
- codecoverage
|
||||
|
||||
## Notes
|
||||
|
||||
question: when you want to pick a task - \(find out priority\) - is it in discord? is it in AV slack? --> Robert says you can always ask in discord - group channels are the best
|
||||
|
||||
Robert shares: [Zenhub board](https://app.zenhub.com/workspaces/nitro-embed-5c0154ecc699f60fc92cf11f/boards?repos=112590397,152252353,152252578,157710732,163305928) Robert says the order of tickets are preserved in ZenHub and reflect their priority \(most important at the top\) and so check out the current milestones
|
||||
|
||||
Matt - question about who can work on [ticket 100](https://app.zenhub.com/workspaces/nitro-embed-5c0154ecc699f60fc92cf11f/issues/human-connection/human-connection/100) --> Robert - in rare occasions it might be exclusive to someone with admin permissions Robert: notes greg just pushed this today: [https://github.com/Human-Connection/Nitro-Deployment](https://github.com/Human-Connection/Nitro-Deployment)
|
||||
|
||||
Matt makes point that new stories will have to be taken off the "New Issues" and Robert says that's fine, if you don't like the first one, then you can take the next one. Volunteeers have no commitment except their own self development and their awesomeness by contributing to free and open-source software projects.
|
||||
|
||||
Robert notes that everyone is invited to join the kickoff meetings
|
||||
|
||||
Robert - difference between "important" \(creates a lot of value\) and "beginner friendly" \(easy to implement\)
|
||||
As a volunteeer you have no commitment except your own self development and your awesomeness by contributing to this free and open-source software project. Cheers to you!
|
||||
|
||||
11
README.md
11
README.md
@ -4,6 +4,7 @@
|
||||
[](https://codecov.io/gh/Human-Connection/Human-Connection/)
|
||||
[](https://github.com/Human-Connection/Nitro-Backend/blob/backend/LICENSE.md)
|
||||
[](https://discordapp.com/invite/DFSjPaX)
|
||||
[](https://www.codetriage.com/human-connection/human-connection)
|
||||
|
||||
Human Connection is a nonprofit social, action and knowledge network that connects information to action and promotes positive local and global change in all areas of life.
|
||||
|
||||
@ -24,7 +25,7 @@ Human Connection is a nonprofit social, action and knowledge network that connec
|
||||
|
||||
## Live demo
|
||||
|
||||
Try out our deployed [staging environment](https://nitro-staging.human-connection.org/).
|
||||
Try out our deployed [development environment](https://develop.human-connection.org/).
|
||||
|
||||
Logins:
|
||||
|
||||
@ -49,10 +50,16 @@ Join our friendly open-source community on [Discord](https://discordapp.com/invi
|
||||
Just introduce yourself at `#introduce-yourself` and mention `@@Mentor` to get you onboard :neckbeard:
|
||||
Check out the [contribution guideline](./CONTRIBUTING.md), too!
|
||||
|
||||
[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/0)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/1)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/2)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/3)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/4)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/5)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/6)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/7)
|
||||
|
||||
|
||||
## Attributions
|
||||
|
||||
Locale Icons made by [Freepik](http://www.freepik.com/) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/)
|
||||
Locale Icons made by [Freepik](http://www.freepik.com/) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/).
|
||||
|
||||
Browser compatibility testing with [BrowserStack](https://www.browserstack.com/).
|
||||
|
||||
<img alt="BrowserStack Logo" src=".gitbook/assets/browserstack-logo.svg" width="256">
|
||||
|
||||
## License
|
||||
See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT).
|
||||
|
||||
16
SUMMARY.md
16
SUMMARY.md
@ -6,16 +6,12 @@
|
||||
* [Neo4J](neo4j/README.md)
|
||||
* [Backend](backend/README.md)
|
||||
* [GraphQL](backend/graphql.md)
|
||||
* [neo4j-graphql-js](backend/neo4j-graphql-js.md)
|
||||
* [Webapp](webapp/README.md)
|
||||
* [COMPONENTS](webapp/components.md)
|
||||
* [PLUGINS](webapp/plugins.md)
|
||||
* [STORE](webapp/store.md)
|
||||
* [PAGES](webapp/pages.md)
|
||||
* [ASSETS](webapp/assets.md)
|
||||
* [LAYOUTS](webapp/layouts.md)
|
||||
* [Styleguide](webapp/styleguide.md)
|
||||
* [STATIC](webapp/static.md)
|
||||
* [MIDDLEWARE](webapp/middleware.md)
|
||||
* [Components](webapp/components.md)
|
||||
* [HTML](webapp/html.md)
|
||||
* [SCSS](webapp/scss.md)
|
||||
* [Vue](webapp/vue.md)
|
||||
* [Testing Guide](testing.md)
|
||||
* [End-to-end tests](cypress/README.md)
|
||||
* [Frontend tests](webapp/testing.md)
|
||||
@ -32,9 +28,11 @@
|
||||
* [Maintenance](deployment/human-connection/maintenance/README.md)
|
||||
* [Volumes](deployment/volumes/README.md)
|
||||
* [Neo4J Offline-Backups](deployment/volumes/neo4j-offline-backup/README.md)
|
||||
* [Neo4J Online-Backups](deployment/volumes/neo4j-online-backup/README.md)
|
||||
* [Volume Snapshots](deployment/volumes/volume-snapshots/README.md)
|
||||
* [Reclaim Policy](deployment/volumes/reclaim-policy/README.md)
|
||||
* [Velero](deployment/volumes/velero/README.md)
|
||||
* [Metrics](deployment/monitoring/README.md)
|
||||
* [Legacy Migration](deployment/legacy-migration/README.md)
|
||||
* [Feature Specification](cypress/features.md)
|
||||
* [Code of conduct](CODE_OF_CONDUCT.md)
|
||||
|
||||
12
babel.config.json
Normal file
12
babel.config.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": "10"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
NEO4J_URI=bolt://localhost:7687
|
||||
NEO4J_USERNAME=neo4j
|
||||
NEO4J_PASSWORD=letmein
|
||||
GRAPHQL_PORT=4000
|
||||
GRAPHQL_URI=http://localhost:4000
|
||||
CLIENT_URI=http://localhost:3000
|
||||
SMTP_HOST=
|
||||
@ -17,3 +16,4 @@ PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78"
|
||||
|
||||
SENTRY_DSN_BACKEND=
|
||||
COMMIT=
|
||||
PUBLIC_REGISTRATION=false
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM node:12.10.0-alpine as base
|
||||
FROM node:lts-alpine as base
|
||||
LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
|
||||
|
||||
EXPOSE 4000
|
||||
@ -24,4 +24,5 @@ FROM base as production
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=build-and-test /nitro-backend/dist ./dist
|
||||
COPY ./public/img/ ./public/img/
|
||||
COPY ./public/providers.json ./public/providers.json
|
||||
RUN yarn install --production=true --frozen-lockfile --non-interactive --no-cache
|
||||
|
||||
@ -53,6 +53,27 @@ can issue GraphQL requests or access GraphQL Playground in the browser.
|
||||
|
||||

|
||||
|
||||
### Database Indices and Constraints
|
||||
|
||||
Database indices and constraints need to be created when the database and the
|
||||
backend is running:
|
||||
|
||||
{% tabs %}
|
||||
{% tab title="Docker" %}
|
||||
```bash
|
||||
docker-compose exec backend yarn run db:migrate init
|
||||
```
|
||||
{% endtab %}
|
||||
|
||||
{% tab title="Without Docker" %}
|
||||
```bash
|
||||
# in folder backend/
|
||||
# make sure your database is running on http://localhost:7474/browser/
|
||||
yarn run db:migrate init
|
||||
```
|
||||
{% endtab %}
|
||||
{% endtabs %}
|
||||
|
||||
|
||||
#### Seed Database
|
||||
|
||||
@ -72,6 +93,8 @@ To reset the database run:
|
||||
$ docker-compose exec backend yarn run db:reset
|
||||
# you could also wipe out your neo4j database and delete all volumes with:
|
||||
$ docker-compose down -v
|
||||
# if container is not running, run this command to set up your database indeces and contstraints
|
||||
$ docker-compose run backend yarn run db:migrate init
|
||||
```
|
||||
{% endtab %}
|
||||
|
||||
@ -88,6 +111,37 @@ $ yarn run db:reset
|
||||
{% endtab %}
|
||||
{% endtabs %}
|
||||
|
||||
### Data migrations
|
||||
|
||||
Although Neo4J is schema-less,you might find yourself in a situation in which
|
||||
you have to migrate your data e.g. because your data modeling has changed.
|
||||
|
||||
{% tabs %}
|
||||
{% tab title="Docker" %}
|
||||
Generate a data migration file:
|
||||
```bash
|
||||
$ docker-compose exec backend yarn run db:migrate:create your_data_migration
|
||||
# Edit the file in ./src/db/migrations/
|
||||
```
|
||||
|
||||
To run the migration:
|
||||
```bash
|
||||
$ docker-compose exec backend yarn run db:migrate up
|
||||
```
|
||||
{% endtab %}
|
||||
{% tab title="Without Docker" %}
|
||||
Generate a data migration file:
|
||||
```bash
|
||||
$ yarn run db:migrate:create your_data_migration
|
||||
# Edit the file in ./src/db/migrations/
|
||||
```
|
||||
|
||||
To run the migration:
|
||||
```bash
|
||||
$ yarn run db:migrate up
|
||||
```
|
||||
{% endtab %}
|
||||
{% endtabs %}
|
||||
|
||||
# Testing
|
||||
|
||||
|
||||
16
backend/neo4j-graphql-js.md
Normal file
16
backend/neo4j-graphql-js.md
Normal file
@ -0,0 +1,16 @@
|
||||
# neo4j-graphql.js
|
||||
|
||||
We use an npm package called `neo4j-graphql-js` as a cypher query builder. This
|
||||
library also generates resolvers for graphql queries, unless we implement them
|
||||
ourselves.
|
||||
|
||||
|
||||
## Debugging
|
||||
|
||||
As you can see in their [documentation](https://github.com/neo4j-graphql/neo4j-graphql-js)
|
||||
it is possible to log out the generated cypher statements. To do so, run the
|
||||
backend like this:
|
||||
|
||||
```sh
|
||||
DEBUG=neo4j-graphql-js yarn run dev
|
||||
```
|
||||
@ -1,26 +1,22 @@
|
||||
{
|
||||
"name": "human-connection-backend",
|
||||
"version": "0.0.1",
|
||||
"version": "0.2.2",
|
||||
"description": "GraphQL Backend for Human Connection",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"build": "babel src/ -d dist/ --copy-files",
|
||||
"__migrate": "migrate --compiler 'js:@babel/register' --migrations-dir ./src/db/migrations",
|
||||
"prod:migrate": "migrate --migrations-dir ./dist/db/migrations --store ./dist/db/migrate/store.js",
|
||||
"start": "node dist/",
|
||||
"build": "babel src/ -d dist/ --copy-files",
|
||||
"dev": "nodemon --exec babel-node src/ -e js,gql",
|
||||
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,gql",
|
||||
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql",
|
||||
"lint": "eslint src --config .eslintrc.js",
|
||||
"jest": "jest --forceExit --detectOpenHandles --runInBand",
|
||||
"test": "run-s test:jest test:cucumber",
|
||||
"test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 yarn run dev 2> /dev/null",
|
||||
"test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions,activityPub yarn run dev 2> /dev/null",
|
||||
"test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand",
|
||||
"test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/",
|
||||
"test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand",
|
||||
"test:jest": "run-p --race test:before:* \"test:jest:cmd {@}\" --",
|
||||
"test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --",
|
||||
"test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --",
|
||||
"db:reset": "babel-node src/seed/reset-db.js",
|
||||
"db:seed": "babel-node src/seed/seed-db.js"
|
||||
"test": "jest --forceExit --detectOpenHandles --runInBand",
|
||||
"db:clean": "babel-node src/db/clean.js",
|
||||
"db:reset": "yarn run db:clean",
|
||||
"db:seed": "babel-node src/db/seed.js",
|
||||
"db:migrate": "yarn run __migrate --store ./src/db/migrate/store.js",
|
||||
"db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.js --date-format 'yyyymmddHHmmss' create"
|
||||
},
|
||||
"author": "Human Connection gGmbH",
|
||||
"license": "MIT",
|
||||
@ -41,94 +37,96 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/joi": "^16.0.1",
|
||||
"@sentry/node": "^5.6.2",
|
||||
"activitystrea.ms": "~2.1.3",
|
||||
"apollo-cache-inmemory": "~1.6.3",
|
||||
"apollo-client": "~2.6.4",
|
||||
"@hapi/joi": "^17.1.0",
|
||||
"@sentry/node": "^5.11.1",
|
||||
"apollo-cache-inmemory": "~1.6.5",
|
||||
"apollo-client": "~2.6.8",
|
||||
"apollo-link-context": "~1.0.19",
|
||||
"apollo-link-http": "~1.5.16",
|
||||
"apollo-server": "~2.9.3",
|
||||
"apollo-server-express": "^2.9.0",
|
||||
"apollo-server": "~2.9.16",
|
||||
"apollo-server-express": "^2.9.16",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"cheerio": "~1.0.0-rc.3",
|
||||
"cors": "~2.8.5",
|
||||
"cross-env": "~5.2.1",
|
||||
"date-fns": "2.1.0",
|
||||
"cross-env": "~7.0.0",
|
||||
"date-fns": "2.9.0",
|
||||
"debug": "~4.1.1",
|
||||
"dotenv": "~8.1.0",
|
||||
"dotenv": "~8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"faker": "Marak/faker.js#master",
|
||||
"graphql": "^14.5.4",
|
||||
"graphql": "^14.6.0",
|
||||
"graphql-custom-directives": "~0.2.14",
|
||||
"graphql-iso-date": "~3.6.1",
|
||||
"graphql-middleware": "~3.0.5",
|
||||
"graphql-middleware-sentry": "^3.2.0",
|
||||
"graphql-shield": "~6.1.0",
|
||||
"graphql-middleware": "~4.0.2",
|
||||
"graphql-middleware-sentry": "^3.2.1",
|
||||
"graphql-shield": "~7.0.8",
|
||||
"graphql-tag": "~2.10.1",
|
||||
"helmet": "~3.21.0",
|
||||
"helmet": "~3.21.2",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"linkifyjs": "~2.1.8",
|
||||
"lodash": "~4.17.14",
|
||||
"merge-graphql-schemas": "^1.7.0",
|
||||
"metascraper": "^4.10.3",
|
||||
"metascraper-audio": "^5.6.5",
|
||||
"metascraper-author": "^5.6.5",
|
||||
"merge-graphql-schemas": "^1.7.6",
|
||||
"metascraper": "^5.10.6",
|
||||
"metascraper-audio": "^5.10.6",
|
||||
"metascraper-author": "^5.10.6",
|
||||
"metascraper-clearbit-logo": "^5.3.0",
|
||||
"metascraper-date": "^5.7.0",
|
||||
"metascraper-description": "^5.7.4",
|
||||
"metascraper-image": "^5.6.5",
|
||||
"metascraper-lang": "^5.6.5",
|
||||
"metascraper-lang-detector": "^4.8.5",
|
||||
"metascraper-logo": "^5.7.4",
|
||||
"metascraper-publisher": "^5.6.5",
|
||||
"metascraper-soundcloud": "^5.6.7",
|
||||
"metascraper-title": "^5.7.0",
|
||||
"metascraper-url": "^5.7.4",
|
||||
"metascraper-video": "^5.6.5",
|
||||
"metascraper-youtube": "^5.7.4",
|
||||
"metascraper-date": "^5.10.6",
|
||||
"metascraper-description": "^5.10.6",
|
||||
"metascraper-image": "^5.10.6",
|
||||
"metascraper-lang": "^5.10.6",
|
||||
"metascraper-lang-detector": "^4.10.2",
|
||||
"metascraper-logo": "^5.10.6",
|
||||
"metascraper-publisher": "^5.10.6",
|
||||
"metascraper-soundcloud": "^5.10.6",
|
||||
"metascraper-title": "^5.10.6",
|
||||
"metascraper-url": "^5.10.6",
|
||||
"metascraper-video": "^5.10.6",
|
||||
"metascraper-youtube": "^5.10.6",
|
||||
"migrate": "^1.6.2",
|
||||
"minimatch": "^3.0.4",
|
||||
"neo4j-driver": "~1.7.6",
|
||||
"neo4j-graphql-js": "^2.7.2",
|
||||
"neode": "^0.3.3",
|
||||
"mustache": "^4.0.0",
|
||||
"neo4j-driver": "^4.0.1",
|
||||
"neo4j-graphql-js": "^2.11.5",
|
||||
"neode": "^0.3.7",
|
||||
"node-fetch": "~2.6.0",
|
||||
"nodemailer": "^6.3.0",
|
||||
"nodemailer": "^6.4.2",
|
||||
"nodemailer-html-to-text": "^3.1.0",
|
||||
"npm-run-all": "~4.1.5",
|
||||
"request": "~2.88.0",
|
||||
"sanitize-html": "~1.20.1",
|
||||
"slug": "~1.1.0",
|
||||
"sanitize-html": "~1.21.1",
|
||||
"slug": "~2.1.1",
|
||||
"trunc-html": "~1.1.2",
|
||||
"uuid": "~3.3.3",
|
||||
"xregexp": "^4.2.4",
|
||||
"wait-on": "~3.3.0"
|
||||
"uuid": "~3.4.0",
|
||||
"validator": "^12.2.0",
|
||||
"wait-on": "~4.0.0",
|
||||
"xregexp": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "~7.6.0",
|
||||
"@babel/core": "~7.6.0",
|
||||
"@babel/node": "~7.6.1",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
||||
"@babel/preset-env": "~7.6.0",
|
||||
"@babel/register": "~7.6.0",
|
||||
"apollo-server-testing": "~2.9.3",
|
||||
"@babel/cli": "~7.8.3",
|
||||
"@babel/core": "~7.8.3",
|
||||
"@babel/node": "~7.8.3",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
|
||||
"@babel/preset-env": "~7.8.3",
|
||||
"@babel/register": "^7.8.3",
|
||||
"apollo-server-testing": "~2.9.16",
|
||||
"babel-core": "~7.0.0-0",
|
||||
"babel-eslint": "~10.0.3",
|
||||
"babel-jest": "~24.9.0",
|
||||
"babel-jest": "~25.1.0",
|
||||
"chai": "~4.2.0",
|
||||
"cucumber": "~5.1.0",
|
||||
"eslint": "~6.3.0",
|
||||
"eslint-config-prettier": "~6.3.0",
|
||||
"cucumber": "~6.0.5",
|
||||
"eslint": "~6.8.0",
|
||||
"eslint-config-prettier": "~6.9.0",
|
||||
"eslint-config-standard": "~14.1.0",
|
||||
"eslint-plugin-import": "~2.18.2",
|
||||
"eslint-plugin-jest": "~22.17.0",
|
||||
"eslint-plugin-node": "~10.0.0",
|
||||
"eslint-plugin-prettier": "~3.1.0",
|
||||
"eslint-plugin-import": "~2.20.0",
|
||||
"eslint-plugin-jest": "~23.6.0",
|
||||
"eslint-plugin-node": "~11.0.0",
|
||||
"eslint-plugin-prettier": "~3.1.2",
|
||||
"eslint-plugin-promise": "~4.2.1",
|
||||
"eslint-plugin-standard": "~4.0.1",
|
||||
"graphql-request": "~1.8.2",
|
||||
"jest": "~24.9.0",
|
||||
"nodemon": "~1.19.2",
|
||||
"prettier": "~1.18.2",
|
||||
"jest": "~25.1.0",
|
||||
"nodemon": "~2.0.2",
|
||||
"prettier": "~1.19.1",
|
||||
"supertest": "~4.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
257
backend/public/providers.json
Normal file
257
backend/public/providers.json
Normal file
@ -0,0 +1,257 @@
|
||||
[
|
||||
{
|
||||
"provider_name": "Codepen",
|
||||
"provider_url": "https:\/\/codepen.io",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"http:\/\/codepen.io\/*",
|
||||
"https:\/\/codepen.io\/*"
|
||||
],
|
||||
"url": "http:\/\/codepen.io\/api\/oembed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "DTube",
|
||||
"provider_url": "https:\/\/d.tube\/",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"https:\/\/d.tube\/v\/*"
|
||||
],
|
||||
"url": "https:\/\/api.d.tube\/oembed",
|
||||
"discovery": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "Facebook (Post)",
|
||||
"provider_url": "https:\/\/www.facebook.com\/",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"https:\/\/www.facebook.com\/*\/posts\/*",
|
||||
"https:\/\/www.facebook.com\/photos\/*",
|
||||
"https:\/\/www.facebook.com\/*\/photos\/*",
|
||||
"https:\/\/www.facebook.com\/photo.php*",
|
||||
"https:\/\/www.facebook.com\/photo.php",
|
||||
"https:\/\/www.facebook.com\/*\/activity\/*",
|
||||
"https:\/\/www.facebook.com\/permalink.php",
|
||||
"https:\/\/www.facebook.com\/media\/set?set=*",
|
||||
"https:\/\/www.facebook.com\/questions\/*",
|
||||
"https:\/\/www.facebook.com\/notes\/*\/*\/*"
|
||||
],
|
||||
"url": "https:\/\/www.facebook.com\/plugins\/post\/oembed.json",
|
||||
"discovery": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "Facebook (Video)",
|
||||
"provider_url": "https:\/\/www.facebook.com\/",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"https:\/\/www.facebook.com\/*\/videos\/*",
|
||||
"https:\/\/www.facebook.com\/video.php"
|
||||
],
|
||||
"url": "https:\/\/www.facebook.com\/plugins\/video\/oembed.json",
|
||||
"discovery": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "Flickr",
|
||||
"provider_url": "https:\/\/www.flickr.com\/",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"http:\/\/*.flickr.com\/photos\/*",
|
||||
"http:\/\/flic.kr\/p\/*",
|
||||
"https:\/\/*.flickr.com\/photos\/*",
|
||||
"https:\/\/flic.kr\/p\/*"
|
||||
],
|
||||
"url": "https:\/\/www.flickr.com\/services\/oembed\/",
|
||||
"discovery": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "GIPHY",
|
||||
"provider_url": "https:\/\/giphy.com",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"https:\/\/giphy.com\/gifs\/*",
|
||||
"http:\/\/gph.is\/*",
|
||||
"https:\/\/media.giphy.com\/media\/*\/giphy.gif"
|
||||
],
|
||||
"url": "https:\/\/giphy.com\/services\/oembed",
|
||||
"discovery": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "Instagram",
|
||||
"provider_url": "https:\/\/instagram.com",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"http:\/\/instagram.com\/p\/*",
|
||||
"http:\/\/instagr.am\/p\/*",
|
||||
"http:\/\/www.instagram.com\/p\/*",
|
||||
"http:\/\/www.instagr.am\/p\/*",
|
||||
"https:\/\/instagram.com\/p\/*",
|
||||
"https:\/\/instagr.am\/p\/*",
|
||||
"https:\/\/www.instagram.com\/p\/*",
|
||||
"https:\/\/www.instagr.am\/p\/*"
|
||||
],
|
||||
"url": "https:\/\/api.instagram.com\/oembed",
|
||||
"formats": [
|
||||
"json"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "Meetup",
|
||||
"provider_url": "http:\/\/www.meetup.com",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"http:\/\/meetup.com\/*",
|
||||
"https:\/\/www.meetup.com\/*",
|
||||
"https:\/\/meetup.com\/*",
|
||||
"http:\/\/meetu.ps\/*"
|
||||
],
|
||||
"url": "https:\/\/api.meetup.com\/oembed",
|
||||
"formats": [
|
||||
"json"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "MixCloud",
|
||||
"provider_url": "https:\/\/mixcloud.com\/",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"http:\/\/www.mixcloud.com\/*\/*\/",
|
||||
"https:\/\/www.mixcloud.com\/*\/*\/"
|
||||
],
|
||||
"url": "https:\/\/www.mixcloud.com\/oembed\/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "Reddit",
|
||||
"provider_url": "https:\/\/reddit.com\/",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"https:\/\/reddit.com\/r\/*\/comments\/*\/*",
|
||||
"https:\/\/www.reddit.com\/r\/*\/comments\/*\/*"
|
||||
],
|
||||
"url": "https:\/\/www.reddit.com\/oembed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "SlideShare",
|
||||
"provider_url": "http:\/\/www.slideshare.net\/",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"http:\/\/www.slideshare.net\/*\/*",
|
||||
"http:\/\/fr.slideshare.net\/*\/*",
|
||||
"http:\/\/de.slideshare.net\/*\/*",
|
||||
"http:\/\/es.slideshare.net\/*\/*",
|
||||
"http:\/\/pt.slideshare.net\/*\/*"
|
||||
],
|
||||
"url": "http:\/\/www.slideshare.net\/api\/oembed\/2",
|
||||
"discovery": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "SoundCloud",
|
||||
"provider_url": "http:\/\/soundcloud.com\/",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"http:\/\/soundcloud.com\/*",
|
||||
"https:\/\/soundcloud.com\/*"
|
||||
],
|
||||
"url": "https:\/\/soundcloud.com\/oembed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "Twitch",
|
||||
"provider_url": "https:\/\/www.twitch.tv",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"http:\/\/clips.twitch.tv\/*",
|
||||
"https:\/\/clips.twitch.tv\/*",
|
||||
"http:\/\/www.twitch.tv\/*",
|
||||
"https:\/\/www.twitch.tv\/*",
|
||||
"http:\/\/twitch.tv\/*",
|
||||
"https:\/\/twitch.tv\/*"
|
||||
],
|
||||
"url": "https:\/\/api.twitch.tv\/v4\/oembed",
|
||||
"formats": [
|
||||
"json"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "Twitter",
|
||||
"provider_url": "http:\/\/www.twitter.com\/",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"https:\/\/twitter.com\/*\/status\/*",
|
||||
"https:\/\/*.twitter.com\/*\/status\/*"
|
||||
],
|
||||
"url": "https:\/\/publish.twitter.com\/oembed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "Vimeo",
|
||||
"provider_url": "https:\/\/vimeo.com\/",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"https:\/\/vimeo.com\/*",
|
||||
"https:\/\/vimeo.com\/album\/*\/video\/*",
|
||||
"https:\/\/vimeo.com\/channels\/*\/*",
|
||||
"https:\/\/vimeo.com\/groups\/*\/videos\/*",
|
||||
"https:\/\/vimeo.com\/ondemand\/*\/*",
|
||||
"https:\/\/player.vimeo.com\/video\/*"
|
||||
],
|
||||
"url": "https:\/\/vimeo.com\/api\/oembed.{format}",
|
||||
"discovery": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider_name": "YouTube",
|
||||
"provider_url": "https:\/\/www.youtube.com\/",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"https:\/\/*.youtube.com\/watch*",
|
||||
"https:\/\/*.youtube.com\/v\/*",
|
||||
"https:\/\/youtu.be\/*"
|
||||
],
|
||||
"url": "https:\/\/www.youtube.com\/oembed",
|
||||
"discovery": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
2667
backend/snapshots/embeds/HumanConnectionOrg.html
Normal file
2667
backend/snapshots/embeds/HumanConnectionOrg.html
Normal file
File diff suppressed because it is too large
Load Diff
1783
backend/snapshots/embeds/babyLovesCat.html
Normal file
1783
backend/snapshots/embeds/babyLovesCat.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,9 @@
|
||||
import { extractNameFromId, extractDomainFromUrl, signAndSend } from './utils'
|
||||
import { isPublicAddressed, sendAcceptActivity, sendRejectActivity } from './utils/activity'
|
||||
// import { extractDomainFromUrl, signAndSend } from './utils'
|
||||
import { extractNameFromId, signAndSend } from './utils'
|
||||
import { isPublicAddressed } from './utils/activity'
|
||||
// import { isPublicAddressed, sendAcceptActivity, sendRejectActivity } from './utils/activity'
|
||||
import request from 'request'
|
||||
import as from 'activitystrea.ms'
|
||||
// import as from 'activitystrea.ms'
|
||||
import NitroDataSource from './NitroDataSource'
|
||||
import router from './routes'
|
||||
import Collections from './Collections'
|
||||
@ -33,71 +35,71 @@ export default class ActivityPub {
|
||||
}
|
||||
}
|
||||
|
||||
handleFollowActivity(activity) {
|
||||
debug(`inside FOLLOW ${activity.actor}`)
|
||||
const toActorName = extractNameFromId(activity.object)
|
||||
const fromDomain = extractDomainFromUrl(activity.actor)
|
||||
const dataSource = this.dataSource
|
||||
// handleFollowActivity(activity) {
|
||||
// debug(`inside FOLLOW ${activity.actor}`)
|
||||
// const toActorName = extractNameFromId(activity.object)
|
||||
// const fromDomain = extractDomainFromUrl(activity.actor)
|
||||
// const dataSource = this.dataSource
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request(
|
||||
{
|
||||
url: activity.actor,
|
||||
headers: {
|
||||
Accept: 'application/activity+json',
|
||||
},
|
||||
},
|
||||
async (err, response, toActorObject) => {
|
||||
if (err) return reject(err)
|
||||
// save shared inbox
|
||||
toActorObject = JSON.parse(toActorObject)
|
||||
await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
|
||||
// return new Promise((resolve, reject) => {
|
||||
// request(
|
||||
// {
|
||||
// url: activity.actor,
|
||||
// headers: {
|
||||
// Accept: 'application/activity+json',
|
||||
// },
|
||||
// },
|
||||
// async (err, response, toActorObject) => {
|
||||
// if (err) return reject(err)
|
||||
// // save shared inbox
|
||||
// toActorObject = JSON.parse(toActorObject)
|
||||
// await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
|
||||
|
||||
const followersCollectionPage = await this.dataSource.getFollowersCollectionPage(
|
||||
activity.object,
|
||||
)
|
||||
// const followersCollectionPage = await this.dataSource.getFollowersCollectionPage(
|
||||
// activity.object,
|
||||
// )
|
||||
|
||||
const followActivity = as
|
||||
.follow()
|
||||
.id(activity.id)
|
||||
.actor(activity.actor)
|
||||
.object(activity.object)
|
||||
// const followActivity = as
|
||||
// .follow()
|
||||
// .id(activity.id)
|
||||
// .actor(activity.actor)
|
||||
// .object(activity.object)
|
||||
|
||||
// add follower if not already in collection
|
||||
if (followersCollectionPage.orderedItems.includes(activity.actor)) {
|
||||
debug('follower already in collection!')
|
||||
debug(`inbox = ${toActorObject.inbox}`)
|
||||
resolve(
|
||||
sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
||||
)
|
||||
} else {
|
||||
followersCollectionPage.orderedItems.push(activity.actor)
|
||||
}
|
||||
debug(`toActorObject = ${toActorObject}`)
|
||||
toActorObject =
|
||||
typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
|
||||
debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
|
||||
debug(`inbox = ${toActorObject.inbox}`)
|
||||
debug(`outbox = ${toActorObject.outbox}`)
|
||||
debug(`followers = ${toActorObject.followers}`)
|
||||
debug(`following = ${toActorObject.following}`)
|
||||
// // add follower if not already in collection
|
||||
// if (followersCollectionPage.orderedItems.includes(activity.actor)) {
|
||||
// debug('follower already in collection!')
|
||||
// debug(`inbox = ${toActorObject.inbox}`)
|
||||
// resolve(
|
||||
// sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
||||
// )
|
||||
// } else {
|
||||
// followersCollectionPage.orderedItems.push(activity.actor)
|
||||
// }
|
||||
// debug(`toActorObject = ${toActorObject}`)
|
||||
// toActorObject =
|
||||
// typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
|
||||
// debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
|
||||
// debug(`inbox = ${toActorObject.inbox}`)
|
||||
// debug(`outbox = ${toActorObject.outbox}`)
|
||||
// debug(`followers = ${toActorObject.followers}`)
|
||||
// debug(`following = ${toActorObject.following}`)
|
||||
|
||||
try {
|
||||
await dataSource.saveFollowersCollectionPage(followersCollectionPage)
|
||||
debug('follow activity saved')
|
||||
resolve(
|
||||
sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
||||
)
|
||||
} catch (e) {
|
||||
debug('followers update error!', e)
|
||||
resolve(
|
||||
sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
// try {
|
||||
// await dataSource.saveFollowersCollectionPage(followersCollectionPage)
|
||||
// debug('follow activity saved')
|
||||
// resolve(
|
||||
// sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
||||
// )
|
||||
// } catch (e) {
|
||||
// debug('followers update error!', e)
|
||||
// resolve(
|
||||
// sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
||||
// )
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
// })
|
||||
// }
|
||||
|
||||
handleUndoActivity(activity) {
|
||||
debug('inside UNDO')
|
||||
|
||||
@ -18,9 +18,9 @@ router.post('/', async function(req, res, next) {
|
||||
case 'Undo':
|
||||
await activityPub.handleUndoActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Follow':
|
||||
await activityPub.handleFollowActivity(req.body).catch(next)
|
||||
break
|
||||
// case 'Follow':
|
||||
// await activityPub.handleFollowActivity(req.body).catch(next)
|
||||
// break
|
||||
case 'Delete':
|
||||
await activityPub.handleDeleteActivity(req.body).catch(next)
|
||||
break
|
||||
|
||||
@ -1,27 +1,29 @@
|
||||
import user from './user'
|
||||
import inbox from './inbox'
|
||||
import webFinger from './webFinger'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import verify from './verify'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/.well-known/webFinger', cors(), express.urlencoded({ extended: true }), webFinger)
|
||||
router.use(
|
||||
'/activitypub/users',
|
||||
cors(),
|
||||
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
|
||||
express.urlencoded({ extended: true }),
|
||||
user,
|
||||
)
|
||||
router.use(
|
||||
'/activitypub/inbox',
|
||||
cors(),
|
||||
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
|
||||
express.urlencoded({ extended: true }),
|
||||
verify,
|
||||
inbox,
|
||||
)
|
||||
|
||||
export default router
|
||||
export default function() {
|
||||
const router = express.Router()
|
||||
router.use(
|
||||
'/activitypub/users',
|
||||
cors(),
|
||||
express.json({
|
||||
type: ['application/activity+json', 'application/ld+json', 'application/json'],
|
||||
}),
|
||||
express.urlencoded({ extended: true }),
|
||||
user,
|
||||
)
|
||||
router.use(
|
||||
'/activitypub/inbox',
|
||||
cors(),
|
||||
express.json({
|
||||
type: ['application/activity+json', 'application/ld+json', 'application/json'],
|
||||
}),
|
||||
express.urlencoded({ extended: true }),
|
||||
verify,
|
||||
inbox,
|
||||
)
|
||||
return router
|
||||
}
|
||||
|
||||
@ -56,9 +56,9 @@ router.post('/:name/inbox', verify, async function(req, res, next) {
|
||||
case 'Undo':
|
||||
await activityPub.handleUndoActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Follow':
|
||||
await activityPub.handleFollowActivity(req.body).catch(next)
|
||||
break
|
||||
// case 'Follow':
|
||||
// await activityPub.handleFollowActivity(req.body).catch(next)
|
||||
// break
|
||||
case 'Delete':
|
||||
await activityPub.handleDeleteActivity(req.body).catch(next)
|
||||
break
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
import express from 'express'
|
||||
import { createWebFinger } from '../utils/actor'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', async function(req, res) {
|
||||
const resource = req.query.resource
|
||||
if (!resource || !resource.includes('acct:')) {
|
||||
return res
|
||||
.status(400)
|
||||
.send(
|
||||
'Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.',
|
||||
)
|
||||
} else {
|
||||
const nameAndDomain = resource.replace('acct:', '')
|
||||
const name = nameAndDomain.split('@')[0]
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await req.app.get('ap').dataSource.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${name}") {
|
||||
slug
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error })
|
||||
}
|
||||
|
||||
if (result.data && result.data.User.length > 0) {
|
||||
const webFinger = createWebFinger(name)
|
||||
return res.contentType('application/jrd+json').json(webFinger)
|
||||
} else {
|
||||
return res.status(404).json({ error: `No record found for ${nameAndDomain}.` })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
59
backend/src/activitypub/routes/webfinger.js
Normal file
59
backend/src/activitypub/routes/webfinger.js
Normal file
@ -0,0 +1,59 @@
|
||||
import express from 'express'
|
||||
import CONFIG from '../../config/'
|
||||
import cors from 'cors'
|
||||
|
||||
const debug = require('debug')('ea:webfinger')
|
||||
const regex = /acct:([a-z0-9_-]*)@([a-z0-9_-]*)/
|
||||
|
||||
const createWebFinger = name => {
|
||||
const { host } = new URL(CONFIG.CLIENT_URI)
|
||||
return {
|
||||
subject: `acct:${name}@${host}`,
|
||||
links: [
|
||||
{
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
href: `${CONFIG.CLIENT_URI}/activitypub/users/${name}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export async function handler(req, res) {
|
||||
const { resource = '' } = req.query
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [_, name, domain] = resource.match(regex) || []
|
||||
if (!(name && domain))
|
||||
return res.status(400).json({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
|
||||
const session = req.app.get('driver').session()
|
||||
try {
|
||||
const [slug] = await session.readTransaction(async t => {
|
||||
const result = await t.run('MATCH (u:User {slug: $slug}) RETURN u.slug AS slug', {
|
||||
slug: name,
|
||||
})
|
||||
return result.records.map(record => record.get('slug'))
|
||||
})
|
||||
if (!slug)
|
||||
return res.status(404).json({
|
||||
error: `No record found for "${name}@${domain}".`,
|
||||
})
|
||||
const webFinger = createWebFinger(name)
|
||||
return res.contentType('application/jrd+json').json(webFinger)
|
||||
} catch (error) {
|
||||
debug(error)
|
||||
return res.status(500).json({
|
||||
error: 'Something went terribly wrong. Please contact support@human-connection.org',
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export default function() {
|
||||
const router = express.Router()
|
||||
router.use('/webfinger', cors(), express.urlencoded({ extended: true }), handler)
|
||||
return router
|
||||
}
|
||||
113
backend/src/activitypub/routes/webfinger.spec.js
Normal file
113
backend/src/activitypub/routes/webfinger.spec.js
Normal file
@ -0,0 +1,113 @@
|
||||
import { handler } from './webfinger'
|
||||
import Factory from '../../factories'
|
||||
import { getDriver } from '../../db/neo4j'
|
||||
|
||||
let resource, res, json, status, contentType
|
||||
|
||||
const factory = Factory()
|
||||
const driver = getDriver()
|
||||
|
||||
const request = () => {
|
||||
json = jest.fn()
|
||||
status = jest.fn(() => ({ json }))
|
||||
contentType = jest.fn(() => ({ json }))
|
||||
res = { status, contentType }
|
||||
const req = {
|
||||
app: {
|
||||
get: key => {
|
||||
return {
|
||||
driver,
|
||||
}[key]
|
||||
},
|
||||
},
|
||||
query: {
|
||||
resource,
|
||||
},
|
||||
}
|
||||
return handler(req, res)
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('webfinger', () => {
|
||||
describe('no ressource', () => {
|
||||
beforeEach(() => {
|
||||
resource = undefined
|
||||
})
|
||||
|
||||
it('sends HTTP 400', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(400)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('?resource query param', () => {
|
||||
describe('is missing acct:', () => {
|
||||
beforeEach(() => {
|
||||
resource = 'some-user@domain'
|
||||
})
|
||||
|
||||
it('sends HTTP 400', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(400)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('has no domain', () => {
|
||||
beforeEach(() => {
|
||||
resource = 'acct:some-user@'
|
||||
})
|
||||
|
||||
it('sends HTTP 400', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(400)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with acct:', () => {
|
||||
beforeEach(() => {
|
||||
resource = 'acct:some-user@domain'
|
||||
})
|
||||
|
||||
it('returns error as json', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(404)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'No record found for "some-user@domain".',
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a user for acct', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', { slug: 'some-user' })
|
||||
})
|
||||
|
||||
it('returns user object', async () => {
|
||||
await request()
|
||||
expect(contentType).toHaveBeenCalledWith('application/jrd+json')
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
links: [
|
||||
{
|
||||
href: 'http://localhost:3000/activitypub/users/some-user',
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
},
|
||||
],
|
||||
subject: 'acct:some-user@localhost:3000',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,10 +1,11 @@
|
||||
import { activityPub } from '../ActivityPub'
|
||||
import { signAndSend, throwErrorIfApolloErrorOccurred } from './index'
|
||||
import { throwErrorIfApolloErrorOccurred } from './index'
|
||||
// import { signAndSend, throwErrorIfApolloErrorOccurred } from './index'
|
||||
|
||||
import crypto from 'crypto'
|
||||
import as from 'activitystrea.ms'
|
||||
// import as from 'activitystrea.ms'
|
||||
import gql from 'graphql-tag'
|
||||
const debug = require('debug')('ea:utils:activity')
|
||||
// const debug = require('debug')('ea:utils:activity')
|
||||
|
||||
export function createNoteObject(text, name, id, published) {
|
||||
const createUuid = crypto.randomBytes(16).toString('hex')
|
||||
@ -62,41 +63,41 @@ export async function getActorId(name) {
|
||||
}
|
||||
}
|
||||
|
||||
export function sendAcceptActivity(theBody, name, targetDomain, url) {
|
||||
as.accept()
|
||||
.id(
|
||||
`${activityPub.endpoint}/activitypub/users/${name}/status/` +
|
||||
crypto.randomBytes(16).toString('hex'),
|
||||
)
|
||||
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
|
||||
.object(theBody)
|
||||
.prettyWrite((err, doc) => {
|
||||
if (!err) {
|
||||
return signAndSend(doc, name, targetDomain, url)
|
||||
} else {
|
||||
debug(`error serializing Accept object: ${err}`)
|
||||
throw new Error('error serializing Accept object')
|
||||
}
|
||||
})
|
||||
}
|
||||
// export function sendAcceptActivity(theBody, name, targetDomain, url) {
|
||||
// as.accept()
|
||||
// .id(
|
||||
// `${activityPub.endpoint}/activitypub/users/${name}/status/` +
|
||||
// crypto.randomBytes(16).toString('hex'),
|
||||
// )
|
||||
// .actor(`${activityPub.endpoint}/activitypub/users/${name}`)
|
||||
// .object(theBody)
|
||||
// .prettyWrite((err, doc) => {
|
||||
// if (!err) {
|
||||
// return signAndSend(doc, name, targetDomain, url)
|
||||
// } else {
|
||||
// debug(`error serializing Accept object: ${err}`)
|
||||
// throw new Error('error serializing Accept object')
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
export function sendRejectActivity(theBody, name, targetDomain, url) {
|
||||
as.reject()
|
||||
.id(
|
||||
`${activityPub.endpoint}/activitypub/users/${name}/status/` +
|
||||
crypto.randomBytes(16).toString('hex'),
|
||||
)
|
||||
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
|
||||
.object(theBody)
|
||||
.prettyWrite((err, doc) => {
|
||||
if (!err) {
|
||||
return signAndSend(doc, name, targetDomain, url)
|
||||
} else {
|
||||
debug(`error serializing Accept object: ${err}`)
|
||||
throw new Error('error serializing Accept object')
|
||||
}
|
||||
})
|
||||
}
|
||||
// export function sendRejectActivity(theBody, name, targetDomain, url) {
|
||||
// as.reject()
|
||||
// .id(
|
||||
// `${activityPub.endpoint}/activitypub/users/${name}/status/` +
|
||||
// crypto.randomBytes(16).toString('hex'),
|
||||
// )
|
||||
// .actor(`${activityPub.endpoint}/activitypub/users/${name}`)
|
||||
// .object(theBody)
|
||||
// .prettyWrite((err, doc) => {
|
||||
// if (!err) {
|
||||
// return signAndSend(doc, name, targetDomain, url)
|
||||
// } else {
|
||||
// debug(`error serializing Accept object: ${err}`)
|
||||
// throw new Error('error serializing Accept object')
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
export function isPublicAddressed(postObject) {
|
||||
if (typeof postObject.to === 'string') {
|
||||
|
||||
@ -22,17 +22,3 @@ export function createActor(name, pubkey) {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function createWebFinger(name) {
|
||||
const { host } = new URL(activityPub.endpoint)
|
||||
return {
|
||||
subject: `acct:${name}@${host}`,
|
||||
links: [
|
||||
{
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
href: `${activityPub.endpoint}/activitypub/users/${name}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import {
|
||||
GraphQLLowerCaseDirective,
|
||||
GraphQLTrimDirective,
|
||||
GraphQLDefaultToDirective,
|
||||
} from 'graphql-custom-directives'
|
||||
|
||||
export default function applyDirectives(augmentedSchema) {
|
||||
const directives = [GraphQLLowerCaseDirective, GraphQLTrimDirective, GraphQLDefaultToDirective]
|
||||
augmentedSchema._directives.push.apply(augmentedSchema._directives, directives)
|
||||
|
||||
return augmentedSchema
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
import { v1 as neo4j } from 'neo4j-driver'
|
||||
import CONFIG from './../config'
|
||||
import setupNeode from './neode'
|
||||
|
||||
let driver
|
||||
|
||||
export function getDriver(options = {}) {
|
||||
const {
|
||||
uri = CONFIG.NEO4J_URI,
|
||||
username = CONFIG.NEO4J_USERNAME,
|
||||
password = CONFIG.NEO4J_PASSWORD,
|
||||
} = options
|
||||
if (!driver) {
|
||||
driver = neo4j.driver(uri, neo4j.auth.basic(username, password))
|
||||
}
|
||||
return driver
|
||||
}
|
||||
|
||||
let neodeInstance
|
||||
export function neode() {
|
||||
if (!neodeInstance) {
|
||||
const { NEO4J_URI: uri, NEO4J_USERNAME: username, NEO4J_PASSWORD: password } = CONFIG
|
||||
neodeInstance = setupNeode({ uri, username, password })
|
||||
}
|
||||
return neodeInstance
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import Neode from 'neode'
|
||||
import models from '../models'
|
||||
|
||||
export default function setupNeode(options) {
|
||||
const { uri, username, password } = options
|
||||
const neodeInstance = new Neode(uri, username, password)
|
||||
neodeInstance.with(models)
|
||||
return neodeInstance
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import { GraphQLDate, GraphQLTime, GraphQLDateTime } from 'graphql-iso-date'
|
||||
|
||||
export default function applyScalars(augmentedSchema) {
|
||||
augmentedSchema._typeMap.Date = GraphQLDate
|
||||
augmentedSchema._typeMap.Time = GraphQLTime
|
||||
augmentedSchema._typeMap.DateTime = GraphQLDateTime
|
||||
|
||||
return augmentedSchema
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
if (require.resolve) {
|
||||
// are we in a nodejs environment?
|
||||
dotenv.config({ path: require.resolve('../../.env') })
|
||||
}
|
||||
|
||||
const {
|
||||
MAPBOX_TOKEN,
|
||||
@ -16,12 +18,25 @@ const {
|
||||
NEO4J_URI = 'bolt://localhost:7687',
|
||||
NEO4J_USERNAME = 'neo4j',
|
||||
NEO4J_PASSWORD = 'neo4j',
|
||||
GRAPHQL_PORT = 4000,
|
||||
CLIENT_URI = 'http://localhost:3000',
|
||||
GRAPHQL_URI = 'http://localhost:4000',
|
||||
} = process.env
|
||||
|
||||
export const requiredConfigs = { MAPBOX_TOKEN, JWT_SECRET, PRIVATE_KEY_PASSPHRASE }
|
||||
export const requiredConfigs = {
|
||||
MAPBOX_TOKEN,
|
||||
JWT_SECRET,
|
||||
PRIVATE_KEY_PASSPHRASE,
|
||||
}
|
||||
|
||||
if (require.resolve) {
|
||||
// are we in a nodejs environment?
|
||||
Object.entries(requiredConfigs).map(entry => {
|
||||
if (!entry[1]) {
|
||||
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const smtpConfigs = {
|
||||
SMTP_HOST,
|
||||
SMTP_PORT,
|
||||
@ -30,7 +45,11 @@ export const smtpConfigs = {
|
||||
SMTP_PASSWORD,
|
||||
}
|
||||
export const neo4jConfigs = { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD }
|
||||
export const serverConfigs = { GRAPHQL_PORT, CLIENT_URI, GRAPHQL_URI }
|
||||
export const serverConfigs = {
|
||||
CLIENT_URI,
|
||||
GRAPHQL_URI,
|
||||
PUBLIC_REGISTRATION: process.env.PUBLIC_REGISTRATION === 'true',
|
||||
}
|
||||
|
||||
export const developmentConfigs = {
|
||||
DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { cleanDatabase } from './factories'
|
||||
import { cleanDatabase } from '../factories'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
throw new Error(`You cannot clean the database in production environment!`)
|
||||
97
backend/src/db/migrate/store.js
Normal file
97
backend/src/db/migrate/store.js
Normal file
@ -0,0 +1,97 @@
|
||||
import { getDriver, getNeode } from '../../db/neo4j'
|
||||
|
||||
class Store {
|
||||
async init(next) {
|
||||
const neode = getNeode()
|
||||
const { driver } = neode
|
||||
const session = driver.session()
|
||||
// eslint-disable-next-line no-console
|
||||
const writeTxResultPromise = session.writeTransaction(async txc => {
|
||||
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices
|
||||
return Promise.all(
|
||||
[
|
||||
'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])',
|
||||
'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])',
|
||||
].map(statement => txc.run(statement)),
|
||||
)
|
||||
})
|
||||
try {
|
||||
await writeTxResultPromise
|
||||
await getNeode().schema.install()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Successfully created database indices and constraints!')
|
||||
next()
|
||||
} catch (error) {
|
||||
console.log(error) // eslint-disable-line no-console
|
||||
next(error, null)
|
||||
} finally {
|
||||
session.close()
|
||||
driver.close()
|
||||
}
|
||||
}
|
||||
|
||||
async load(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const readTxResultPromise = session.readTransaction(async txc => {
|
||||
const result = await txc.run(
|
||||
'MATCH (migration:Migration) RETURN migration {.*} ORDER BY migration.timestamp DESC',
|
||||
)
|
||||
return result.records.map(r => r.get('migration'))
|
||||
})
|
||||
try {
|
||||
const migrations = await readTxResultPromise
|
||||
if (migrations.length <= 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
"No migrations found in database. If it's the first time you run migrations, then this is normal.",
|
||||
)
|
||||
return next(null, {})
|
||||
}
|
||||
const [{ title: lastRun }] = migrations
|
||||
next(null, { lastRun, migrations })
|
||||
} catch (error) {
|
||||
console.log(error) // eslint-disable-line no-console
|
||||
next(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
async save(set, next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const { migrations } = set
|
||||
const writeTxResultPromise = session.writeTransaction(txc => {
|
||||
return Promise.all(
|
||||
migrations.map(async migration => {
|
||||
const { title, description, timestamp } = migration
|
||||
const properties = { title, description, timestamp }
|
||||
const migrationResult = await txc.run(
|
||||
`
|
||||
MERGE (migration:Migration { title: $properties.title })
|
||||
ON MATCH SET
|
||||
migration += $properties
|
||||
ON CREATE SET
|
||||
migration += $properties,
|
||||
migration.migratedAt = toString(datetime())
|
||||
`,
|
||||
{ properties },
|
||||
)
|
||||
return migrationResult
|
||||
}),
|
||||
)
|
||||
})
|
||||
try {
|
||||
await writeTxResultPromise
|
||||
next()
|
||||
} catch (error) {
|
||||
console.log(error) // eslint-disable-line no-console
|
||||
next(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Store
|
||||
31
backend/src/db/migrate/template.js
Normal file
31
backend/src/db/migrate/template.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { getDriver } from '../../db/neo4j'
|
||||
|
||||
export const description = ''
|
||||
|
||||
export function up(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
try {
|
||||
// Implement your migration here.
|
||||
next()
|
||||
} catch (err) {
|
||||
next(err)
|
||||
} finally {
|
||||
session.close()
|
||||
driver.close()
|
||||
}
|
||||
}
|
||||
|
||||
export function down(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
try {
|
||||
// Rollback your migration here.
|
||||
next()
|
||||
} catch (err) {
|
||||
next(err)
|
||||
} finally {
|
||||
session.close()
|
||||
driver.close()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
import { throwError, concat } from 'rxjs'
|
||||
import { flatMap, mergeMap, map, catchError, filter } from 'rxjs/operators'
|
||||
import { getDriver } from '../neo4j'
|
||||
import normalizeEmail from '../../schema/resolvers//helpers/normalizeEmail'
|
||||
|
||||
export const description = `
|
||||
This migration merges duplicate :User and :EmailAddress nodes. It became
|
||||
necessary after we implemented the email normalization but forgot to migrate
|
||||
the existing data. Some (40) users decided to just register with a new account
|
||||
but the same email address. On signup our backend would normalize the email,
|
||||
which is good, but would also keep the existing unnormalized email address.
|
||||
|
||||
This led to about 40 duplicate user and email address nodes in our database.
|
||||
`
|
||||
export function up(next) {
|
||||
const driver = getDriver()
|
||||
const rxSession = driver.rxSession()
|
||||
rxSession
|
||||
.beginTransaction()
|
||||
.pipe(
|
||||
flatMap(txc =>
|
||||
concat(
|
||||
txc
|
||||
.run('MATCH (email:EmailAddress) RETURN email {.email}')
|
||||
.records()
|
||||
.pipe(
|
||||
map(record => {
|
||||
const { email } = record.get('email')
|
||||
const normalizedEmail = normalizeEmail(email)
|
||||
return { email, normalizedEmail }
|
||||
}),
|
||||
filter(({ email, normalizedEmail }) => email !== normalizedEmail),
|
||||
mergeMap(({ email, normalizedEmail }) => {
|
||||
return txc
|
||||
.run(
|
||||
`
|
||||
MATCH (oldUser:User)-[:PRIMARY_EMAIL]->(oldEmail:EmailAddress {email: $email}), (oldUser)-[previousRelationship]-(oldEmail)
|
||||
MATCH (user:User)-[:PRIMARY_EMAIL]->(email:EmailAddress {email: $normalizedEmail})
|
||||
DELETE previousRelationship
|
||||
WITH oldUser, oldEmail, user, email
|
||||
CALL apoc.refactor.mergeNodes([user, oldUser], { properties: 'discard', mergeRels: true }) YIELD node as mergedUser
|
||||
CALL apoc.refactor.mergeNodes([email, oldEmail], { properties: 'discard', mergeRels: true }) YIELD node as mergedEmail
|
||||
RETURN user {.*}, email {.*}
|
||||
`,
|
||||
{ email, normalizedEmail },
|
||||
)
|
||||
.records()
|
||||
.pipe(
|
||||
map(r => ({
|
||||
oldEmail: email,
|
||||
email: r.get('email'),
|
||||
user: r.get('user'),
|
||||
})),
|
||||
)
|
||||
}),
|
||||
),
|
||||
txc.commit(),
|
||||
).pipe(catchError(err => txc.rollback().pipe(throwError(err)))),
|
||||
),
|
||||
)
|
||||
.subscribe({
|
||||
next: ({ user, email, oldUser, oldEmail }) =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`
|
||||
Merged:
|
||||
=============================
|
||||
userId: ${user.id}
|
||||
email: ${oldEmail} => ${email.email}
|
||||
=============================
|
||||
`),
|
||||
complete: () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Merging of duplicate users completed')
|
||||
next()
|
||||
},
|
||||
error: error => {
|
||||
next(new Error(error), null)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function down(next) {
|
||||
next(new Error('Irreversible migration'))
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import { throwError, concat } from 'rxjs'
|
||||
import { flatMap, mergeMap, map, catchError } from 'rxjs/operators'
|
||||
import { getDriver } from '../neo4j'
|
||||
|
||||
export const description = `
|
||||
This migration merges duplicate :Location nodes. It became
|
||||
necessary after we realized that we had not set up constraints for Location.id in production.
|
||||
`
|
||||
export function up(next) {
|
||||
const driver = getDriver()
|
||||
const rxSession = driver.rxSession()
|
||||
rxSession
|
||||
.beginTransaction()
|
||||
.pipe(
|
||||
flatMap(transaction =>
|
||||
concat(
|
||||
transaction
|
||||
.run(
|
||||
`
|
||||
MATCH (location:Location)
|
||||
RETURN location {.id}
|
||||
`,
|
||||
)
|
||||
.records()
|
||||
.pipe(
|
||||
map(record => {
|
||||
const { id: locationId } = record.get('location')
|
||||
return { locationId }
|
||||
}),
|
||||
mergeMap(({ locationId }) => {
|
||||
return transaction
|
||||
.run(
|
||||
`
|
||||
MATCH(location:Location {id: $locationId}), (location2:Location {id: $locationId})
|
||||
WHERE location.id = location2.id AND id(location) < id(location2)
|
||||
CALL apoc.refactor.mergeNodes([location, location2], { properties: 'combine', mergeRels: true }) YIELD node as updatedLocation
|
||||
RETURN location {.*},updatedLocation {.*}
|
||||
`,
|
||||
{ locationId },
|
||||
)
|
||||
.records()
|
||||
.pipe(
|
||||
map(record => ({
|
||||
location: record.get('location'),
|
||||
updatedLocation: record.get('updatedLocation'),
|
||||
})),
|
||||
)
|
||||
}),
|
||||
),
|
||||
transaction.commit(),
|
||||
).pipe(catchError(error => transaction.rollback().pipe(throwError(error)))),
|
||||
),
|
||||
)
|
||||
.subscribe({
|
||||
next: ({ updatedLocation, location }) =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`
|
||||
Merged:
|
||||
=============================
|
||||
locationId: ${location.id}
|
||||
updatedLocation: ${location.id} => ${updatedLocation.id}
|
||||
=============================
|
||||
`),
|
||||
complete: () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Merging of duplicate locations completed')
|
||||
next()
|
||||
},
|
||||
error: error => {
|
||||
next(new Error(error), null)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function down(next) {
|
||||
next(new Error('Irreversible migration'))
|
||||
}
|
||||
29
backend/src/db/neo4j.js
Normal file
29
backend/src/db/neo4j.js
Normal file
@ -0,0 +1,29 @@
|
||||
import neo4j from 'neo4j-driver'
|
||||
import CONFIG from './../config'
|
||||
import Neode from 'neode'
|
||||
import models from '../models'
|
||||
|
||||
let driver
|
||||
const defaultOptions = {
|
||||
uri: CONFIG.NEO4J_URI,
|
||||
username: CONFIG.NEO4J_USERNAME,
|
||||
password: CONFIG.NEO4J_PASSWORD,
|
||||
}
|
||||
|
||||
export function getDriver(options = {}) {
|
||||
const { uri, username, password } = { ...defaultOptions, ...options }
|
||||
if (!driver) {
|
||||
driver = neo4j.driver(uri, neo4j.auth.basic(username, password))
|
||||
}
|
||||
return driver
|
||||
}
|
||||
|
||||
let neodeInstance
|
||||
export function getNeode(options = {}) {
|
||||
if (!neodeInstance) {
|
||||
const { uri, username, password } = { ...defaultOptions, ...options }
|
||||
neodeInstance = new Neode(uri, username, password).with(models)
|
||||
return neodeInstance
|
||||
}
|
||||
return neodeInstance
|
||||
}
|
||||
@ -1,9 +1,12 @@
|
||||
import faker from 'faker'
|
||||
import sample from 'lodash/sample'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer from '../server'
|
||||
import Factory from './factories'
|
||||
import { neode as getNeode, getDriver } from '../bootstrap/neo4j'
|
||||
import { gql } from '../jest/helpers'
|
||||
import Factory from '../factories'
|
||||
import { getNeode, getDriver } from '../db/neo4j'
|
||||
import { gql } from '../helpers/jest'
|
||||
|
||||
const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
|
||||
/* eslint-disable no-multi-spaces */
|
||||
;(async function() {
|
||||
@ -24,10 +27,8 @@ import { gql } from '../jest/helpers'
|
||||
})
|
||||
const { mutate } = createTestClient(server)
|
||||
|
||||
const f = Factory()
|
||||
|
||||
const [Hamburg, Berlin, Germany, Paris, France] = await Promise.all([
|
||||
f.create('Location', {
|
||||
factory.create('Location', {
|
||||
id: 'region.5127278006398860',
|
||||
name: 'Hamburg',
|
||||
type: 'region',
|
||||
@ -41,8 +42,9 @@ import { gql } from '../jest/helpers'
|
||||
nameDE: 'Hamburg',
|
||||
nameNL: 'Hamburg',
|
||||
namePL: 'Hamburg',
|
||||
nameRU: 'Гамбург',
|
||||
}),
|
||||
f.create('Location', {
|
||||
factory.create('Location', {
|
||||
id: 'region.14880313158564380',
|
||||
type: 'region',
|
||||
name: 'Berlin',
|
||||
@ -56,8 +58,9 @@ import { gql } from '../jest/helpers'
|
||||
nameDE: 'Berlin',
|
||||
nameNL: 'Berlijn',
|
||||
namePL: 'Berlin',
|
||||
nameRU: 'Берлин',
|
||||
}),
|
||||
f.create('Location', {
|
||||
factory.create('Location', {
|
||||
id: 'country.10743216036480410',
|
||||
name: 'Germany',
|
||||
type: 'country',
|
||||
@ -69,8 +72,9 @@ import { gql } from '../jest/helpers'
|
||||
nameFR: 'Allemagne',
|
||||
nameIT: 'Germania',
|
||||
nameEN: 'Germany',
|
||||
nameRU: 'Германия',
|
||||
}),
|
||||
f.create('Location', {
|
||||
factory.create('Location', {
|
||||
id: 'region.9397217726497330',
|
||||
name: 'Paris',
|
||||
type: 'region',
|
||||
@ -84,8 +88,9 @@ import { gql } from '../jest/helpers'
|
||||
nameDE: 'Paris',
|
||||
nameNL: 'Parijs',
|
||||
namePL: 'Paryż',
|
||||
nameRU: 'Париж',
|
||||
}),
|
||||
f.create('Location', {
|
||||
factory.create('Location', {
|
||||
id: 'country.9759535382641660',
|
||||
name: 'France',
|
||||
type: 'country',
|
||||
@ -97,6 +102,7 @@ import { gql } from '../jest/helpers'
|
||||
nameFR: 'France',
|
||||
nameIT: 'Francia',
|
||||
nameEN: 'France',
|
||||
nameRU: 'Франция',
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
@ -106,27 +112,27 @@ import { gql } from '../jest/helpers'
|
||||
])
|
||||
|
||||
const [racoon, rabbit, wolf, bear, turtle, rhino] = await Promise.all([
|
||||
f.create('Badge', {
|
||||
factory.create('Badge', {
|
||||
id: 'indiegogo_en_racoon',
|
||||
icon: '/img/badges/indiegogo_en_racoon.svg',
|
||||
}),
|
||||
f.create('Badge', {
|
||||
factory.create('Badge', {
|
||||
id: 'indiegogo_en_rabbit',
|
||||
icon: '/img/badges/indiegogo_en_rabbit.svg',
|
||||
}),
|
||||
f.create('Badge', {
|
||||
factory.create('Badge', {
|
||||
id: 'indiegogo_en_wolf',
|
||||
icon: '/img/badges/indiegogo_en_wolf.svg',
|
||||
}),
|
||||
f.create('Badge', {
|
||||
factory.create('Badge', {
|
||||
id: 'indiegogo_en_bear',
|
||||
icon: '/img/badges/indiegogo_en_bear.svg',
|
||||
}),
|
||||
f.create('Badge', {
|
||||
factory.create('Badge', {
|
||||
id: 'indiegogo_en_turtle',
|
||||
icon: '/img/badges/indiegogo_en_turtle.svg',
|
||||
}),
|
||||
f.create('Badge', {
|
||||
factory.create('Badge', {
|
||||
id: 'indiegogo_en_rhino',
|
||||
icon: '/img/badges/indiegogo_en_rhino.svg',
|
||||
}),
|
||||
@ -141,49 +147,49 @@ import { gql } from '../jest/helpers'
|
||||
louie,
|
||||
dagobert,
|
||||
] = await Promise.all([
|
||||
f.create('User', {
|
||||
factory.create('User', {
|
||||
id: 'u1',
|
||||
name: 'Peter Lustig',
|
||||
slug: 'peter-lustig',
|
||||
role: 'admin',
|
||||
email: 'admin@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
factory.create('User', {
|
||||
id: 'u2',
|
||||
name: 'Bob der Baumeister',
|
||||
slug: 'bob-der-baumeister',
|
||||
role: 'moderator',
|
||||
email: 'moderator@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
factory.create('User', {
|
||||
id: 'u3',
|
||||
name: 'Jenny Rostock',
|
||||
slug: 'jenny-rostock',
|
||||
role: 'user',
|
||||
email: 'user@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
factory.create('User', {
|
||||
id: 'u4',
|
||||
name: 'Huey',
|
||||
slug: 'huey',
|
||||
role: 'user',
|
||||
email: 'huey@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
factory.create('User', {
|
||||
id: 'u5',
|
||||
name: 'Dewey',
|
||||
slug: 'dewey',
|
||||
role: 'user',
|
||||
email: 'dewey@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
factory.create('User', {
|
||||
id: 'u6',
|
||||
name: 'Louie',
|
||||
slug: 'louie',
|
||||
role: 'user',
|
||||
email: 'louie@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
factory.create('User', {
|
||||
id: 'u7',
|
||||
name: 'Dagobert',
|
||||
slug: 'dagobert',
|
||||
@ -220,103 +226,107 @@ import { gql } from '../jest/helpers'
|
||||
dewey.relateTo(huey, 'following'),
|
||||
louie.relateTo(jennyRostock, 'following'),
|
||||
|
||||
huey.relateTo(dagobert, 'muted'),
|
||||
dewey.relateTo(dagobert, 'muted'),
|
||||
louie.relateTo(dagobert, 'muted'),
|
||||
|
||||
dagobert.relateTo(huey, 'blocked'),
|
||||
dagobert.relateTo(dewey, 'blocked'),
|
||||
dagobert.relateTo(louie, 'blocked'),
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat1',
|
||||
name: 'Just For Fun',
|
||||
slug: 'just-for-fun',
|
||||
icon: 'smile',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat2',
|
||||
name: 'Happiness & Values',
|
||||
slug: 'happiness-values',
|
||||
icon: 'heart-o',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat3',
|
||||
name: 'Health & Wellbeing',
|
||||
slug: 'health-wellbeing',
|
||||
icon: 'medkit',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat4',
|
||||
name: 'Environment & Nature',
|
||||
slug: 'environment-nature',
|
||||
icon: 'tree',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat5',
|
||||
name: 'Animal Protection',
|
||||
slug: 'animal-protection',
|
||||
icon: 'paw',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat6',
|
||||
name: 'Human Rights & Justice',
|
||||
slug: 'human-rights-justice',
|
||||
icon: 'balance-scale',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat7',
|
||||
name: 'Education & Sciences',
|
||||
slug: 'education-sciences',
|
||||
icon: 'graduation-cap',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat8',
|
||||
name: 'Cooperation & Development',
|
||||
slug: 'cooperation-development',
|
||||
icon: 'users',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
slug: 'democracy-politics',
|
||||
icon: 'university',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat10',
|
||||
name: 'Economy & Finances',
|
||||
slug: 'economy-finances',
|
||||
icon: 'money',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat11',
|
||||
name: 'Energy & Technology',
|
||||
slug: 'energy-technology',
|
||||
icon: 'flash',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat12',
|
||||
name: 'IT, Internet & Data Privacy',
|
||||
slug: 'it-internet-data-privacy',
|
||||
icon: 'mouse-pointer',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat13',
|
||||
name: 'Art, Culture & Sport',
|
||||
slug: 'art-culture-sport',
|
||||
icon: 'paint-brush',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat14',
|
||||
name: 'Freedom of Speech',
|
||||
slug: 'freedom-of-speech',
|
||||
icon: 'bullhorn',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat15',
|
||||
name: 'Consumption & Sustainability',
|
||||
slug: 'consumption-sustainability',
|
||||
icon: 'shopping-cart',
|
||||
}),
|
||||
f.create('Category', {
|
||||
factory.create('Category', {
|
||||
id: 'cat16',
|
||||
name: 'Global Peace & Nonviolence',
|
||||
slug: 'global-peace-nonviolence',
|
||||
@ -325,16 +335,16 @@ import { gql } from '../jest/helpers'
|
||||
])
|
||||
|
||||
const [environment, nature, democracy, freedom] = await Promise.all([
|
||||
f.create('Tag', {
|
||||
factory.create('Tag', {
|
||||
id: 'Environment',
|
||||
}),
|
||||
f.create('Tag', {
|
||||
factory.create('Tag', {
|
||||
id: 'Nature',
|
||||
}),
|
||||
f.create('Tag', {
|
||||
factory.create('Tag', {
|
||||
id: 'Democracy',
|
||||
}),
|
||||
f.create('Tag', {
|
||||
factory.create('Tag', {
|
||||
id: 'Freedom',
|
||||
}),
|
||||
])
|
||||
@ -343,66 +353,84 @@ import { gql } from '../jest/helpers'
|
||||
factory.create('Post', {
|
||||
author: peterLustig,
|
||||
id: 'p0',
|
||||
image: faker.image.unsplash.food(),
|
||||
language: sample(languages),
|
||||
image: faker.image.unsplash.food(300, 169),
|
||||
categoryIds: ['cat16'],
|
||||
imageBlurred: true,
|
||||
imageAspectRatio: 300 / 169,
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: bobDerBaumeister,
|
||||
id: 'p1',
|
||||
image: faker.image.unsplash.technology(),
|
||||
language: sample(languages),
|
||||
image: faker.image.unsplash.technology(300, 1500),
|
||||
categoryIds: ['cat1'],
|
||||
imageAspectRatio: 300 / 1500,
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: huey,
|
||||
id: 'p3',
|
||||
language: sample(languages),
|
||||
categoryIds: ['cat3'],
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: dewey,
|
||||
id: 'p4',
|
||||
language: sample(languages),
|
||||
categoryIds: ['cat4'],
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: louie,
|
||||
id: 'p5',
|
||||
language: sample(languages),
|
||||
categoryIds: ['cat5'],
|
||||
}),
|
||||
factory.create('Post', {
|
||||
authorId: 'u1',
|
||||
id: 'p6',
|
||||
image: faker.image.unsplash.buildings(),
|
||||
language: sample(languages),
|
||||
image: faker.image.unsplash.buildings(300, 857),
|
||||
categoryIds: ['cat6'],
|
||||
imageAspectRatio: 300 / 857,
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: huey,
|
||||
id: 'p9',
|
||||
language: sample(languages),
|
||||
categoryIds: ['cat9'],
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: dewey,
|
||||
id: 'p10',
|
||||
categoryIds: ['cat10'],
|
||||
imageBlurred: true,
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: louie,
|
||||
id: 'p11',
|
||||
image: faker.image.unsplash.people(),
|
||||
language: sample(languages),
|
||||
image: faker.image.unsplash.people(300, 901),
|
||||
categoryIds: ['cat11'],
|
||||
imageAspectRatio: 300 / 901,
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: bobDerBaumeister,
|
||||
id: 'p13',
|
||||
language: sample(languages),
|
||||
categoryIds: ['cat13'],
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: jennyRostock,
|
||||
id: 'p14',
|
||||
image: faker.image.unsplash.objects(),
|
||||
language: sample(languages),
|
||||
image: faker.image.unsplash.objects(300, 200),
|
||||
categoryIds: ['cat14'],
|
||||
imageAspectRatio: 300 / 450,
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: huey,
|
||||
id: 'p15',
|
||||
language: sample(languages),
|
||||
categoryIds: ['cat15'],
|
||||
}),
|
||||
])
|
||||
@ -413,12 +441,26 @@ import { gql } from '../jest/helpers'
|
||||
const mention2 =
|
||||
'Hey <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, here is another notification for you!'
|
||||
const hashtag1 =
|
||||
'See <a class="hashtag" href="/search/hashtag/NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!'
|
||||
'See <a class="hashtag" data-hashtag-id="NaturphilosophieYoga" href="/?hashtag=NaturphilosophieYoga">#NaturphilosophieYoga</a>, it can really help you!'
|
||||
const hashtagAndMention1 =
|
||||
'The new physics of <a class="hashtag" href="/search/hashtag/QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" href="/search/hashtag/QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
|
||||
'The new physics of <a class="hashtag" data-hashtag-id="QuantenFlussTheorie" href="/?hashtag=QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" data-hashtag-id="QuantumGravity" href="/?hashtag=QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
|
||||
const createPostMutation = gql`
|
||||
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
mutation(
|
||||
$id: ID
|
||||
$title: String!
|
||||
$content: String!
|
||||
$categoryIds: [ID]
|
||||
$imageBlurred: Boolean
|
||||
$imageAspectRatio: Float
|
||||
) {
|
||||
CreatePost(
|
||||
id: $id
|
||||
title: $title
|
||||
content: $content
|
||||
categoryIds: $categoryIds
|
||||
imageBlurred: $imageBlurred
|
||||
imageAspectRatio: $imageAspectRatio
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@ -432,6 +474,7 @@ import { gql } from '../jest/helpers'
|
||||
title: `Nature Philosophy Yoga`,
|
||||
content: hashtag1,
|
||||
categoryIds: ['cat2'],
|
||||
imageAspectRatio: 300 / 200,
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
@ -441,6 +484,7 @@ import { gql } from '../jest/helpers'
|
||||
title: 'This is post #7',
|
||||
content: `${mention1} ${faker.lorem.paragraph()}`,
|
||||
categoryIds: ['cat7'],
|
||||
imageAspectRatio: 300 / 180,
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
@ -451,6 +495,7 @@ import { gql } from '../jest/helpers'
|
||||
title: `Quantum Flow Theory explains Quantum Gravity`,
|
||||
content: hashtagAndMention1,
|
||||
categoryIds: ['cat8'],
|
||||
imageAspectRatio: 300 / 900,
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
@ -460,6 +505,7 @@ import { gql } from '../jest/helpers'
|
||||
title: 'This is post #12',
|
||||
content: `${mention2} ${faker.lorem.paragraph()}`,
|
||||
categoryIds: ['cat12'],
|
||||
imageAspectRatio: 300 / 200,
|
||||
},
|
||||
}),
|
||||
])
|
||||
@ -470,9 +516,9 @@ import { gql } from '../jest/helpers'
|
||||
|
||||
authenticatedUser = await dewey.toJson()
|
||||
const mentionInComment1 =
|
||||
'I heard <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, practice it since 3 years now.'
|
||||
'I heard <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a> has practiced it for 3 years now.'
|
||||
const mentionInComment2 =
|
||||
'Did <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> told you?'
|
||||
'Did <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> tell you?'
|
||||
const createCommentMutation = gql`
|
||||
mutation($id: ID, $postId: ID!, $content: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||
@ -507,7 +553,7 @@ import { gql } from '../jest/helpers'
|
||||
])
|
||||
authenticatedUser = null
|
||||
|
||||
await Promise.all([
|
||||
const comments = await Promise.all([
|
||||
factory.create('Comment', {
|
||||
author: jennyRostock,
|
||||
id: 'c1',
|
||||
@ -524,7 +570,7 @@ import { gql } from '../jest/helpers'
|
||||
postId: 'p3',
|
||||
}),
|
||||
factory.create('Comment', {
|
||||
author: bobDerBaumeister,
|
||||
author: jennyRostock,
|
||||
id: 'c5',
|
||||
postId: 'p3',
|
||||
}),
|
||||
@ -564,6 +610,7 @@ import { gql } from '../jest/helpers'
|
||||
postId: 'p15',
|
||||
}),
|
||||
])
|
||||
const trollingComment = comments[0]
|
||||
|
||||
await Promise.all([
|
||||
democracy.relateTo(p3, 'post'),
|
||||
@ -627,67 +674,339 @@ import { gql } from '../jest/helpers'
|
||||
louie.relateTo(p10, 'shouted'),
|
||||
])
|
||||
|
||||
const disableMutation = gql`
|
||||
mutation($id: ID!) {
|
||||
disable(id: $id)
|
||||
}
|
||||
`
|
||||
authenticatedUser = await bobDerBaumeister.toJson()
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: disableMutation,
|
||||
variables: {
|
||||
id: 'p11',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: disableMutation,
|
||||
variables: {
|
||||
id: 'c5',
|
||||
},
|
||||
}),
|
||||
const reports = await Promise.all([
|
||||
factory.create('Report'),
|
||||
factory.create('Report'),
|
||||
factory.create('Report'),
|
||||
factory.create('Report'),
|
||||
])
|
||||
authenticatedUser = null
|
||||
const reportAgainstDagobert = reports[0]
|
||||
const reportAgainstTrollingPost = reports[1]
|
||||
const reportAgainstTrollingComment = reports[2]
|
||||
const reportAgainstDewey = reports[3]
|
||||
|
||||
const reportMutation = gql`
|
||||
mutation($id: ID!, $description: String!) {
|
||||
report(description: $description, id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
authenticatedUser = await huey.toJson()
|
||||
// report resource first time
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: {
|
||||
description: "I don't like this comment",
|
||||
id: 'c1',
|
||||
},
|
||||
reportAgainstDagobert.relateTo(jennyRostock, 'filed', {
|
||||
resourceId: 'u7',
|
||||
reasonCategory: 'discrimination_etc',
|
||||
reasonDescription: 'This user is harassing me with bigoted remarks!',
|
||||
}),
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: {
|
||||
description: "I don't like this post",
|
||||
id: 'p1',
|
||||
},
|
||||
reportAgainstDagobert.relateTo(dagobert, 'belongsTo'),
|
||||
reportAgainstTrollingPost.relateTo(jennyRostock, 'filed', {
|
||||
resourceId: 'p2',
|
||||
reasonCategory: 'doxing',
|
||||
reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!",
|
||||
}),
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: {
|
||||
description: "I don't like this user",
|
||||
id: 'u1',
|
||||
},
|
||||
reportAgainstTrollingPost.relateTo(p2, 'belongsTo'),
|
||||
reportAgainstTrollingComment.relateTo(huey, 'filed', {
|
||||
resourceId: 'c1',
|
||||
reasonCategory: 'other',
|
||||
reasonDescription: 'This comment is bigoted',
|
||||
}),
|
||||
reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'),
|
||||
reportAgainstDewey.relateTo(dagobert, 'filed', {
|
||||
resourceId: 'u5',
|
||||
reasonCategory: 'discrimination_etc',
|
||||
reasonDescription: 'This user is harassing me!',
|
||||
}),
|
||||
reportAgainstDewey.relateTo(dewey, 'belongsTo'),
|
||||
])
|
||||
|
||||
// report resource a second time
|
||||
await Promise.all([
|
||||
reportAgainstDagobert.relateTo(louie, 'filed', {
|
||||
resourceId: 'u7',
|
||||
reasonCategory: 'discrimination_etc',
|
||||
reasonDescription: 'this user is attacking me for who I am!',
|
||||
}),
|
||||
reportAgainstDagobert.relateTo(dagobert, 'belongsTo'),
|
||||
reportAgainstTrollingPost.relateTo(peterLustig, 'filed', {
|
||||
resourceId: 'p2',
|
||||
reasonCategory: 'discrimination_etc',
|
||||
reasonDescription: 'This post is bigoted',
|
||||
}),
|
||||
reportAgainstTrollingPost.relateTo(p2, 'belongsTo'),
|
||||
|
||||
reportAgainstTrollingComment.relateTo(bobDerBaumeister, 'filed', {
|
||||
resourceId: 'c1',
|
||||
reasonCategory: 'pornographic_content_links',
|
||||
reasonDescription: 'This comment is porno!!!',
|
||||
}),
|
||||
reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'),
|
||||
])
|
||||
|
||||
const disableVariables = {
|
||||
resourceId: 'undefined-resource',
|
||||
disable: true,
|
||||
closed: false,
|
||||
}
|
||||
|
||||
// review resource first time
|
||||
await Promise.all([
|
||||
reportAgainstDagobert.relateTo(bobDerBaumeister, 'reviewed', {
|
||||
...disableVariables,
|
||||
resourceId: 'u7',
|
||||
}),
|
||||
dagobert.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||
reportAgainstTrollingPost.relateTo(peterLustig, 'reviewed', {
|
||||
...disableVariables,
|
||||
resourceId: 'p2',
|
||||
}),
|
||||
p2.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||
reportAgainstTrollingComment.relateTo(bobDerBaumeister, 'reviewed', {
|
||||
...disableVariables,
|
||||
resourceId: 'c1',
|
||||
}),
|
||||
trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||
])
|
||||
|
||||
// second review of resource and close report
|
||||
await Promise.all([
|
||||
reportAgainstDagobert.relateTo(peterLustig, 'reviewed', {
|
||||
resourceId: 'u7',
|
||||
disable: false,
|
||||
closed: true,
|
||||
}),
|
||||
dagobert.update({ disabled: false, updatedAt: new Date().toISOString(), closed: true }),
|
||||
reportAgainstTrollingPost.relateTo(bobDerBaumeister, 'reviewed', {
|
||||
resourceId: 'p2',
|
||||
disable: true,
|
||||
closed: true,
|
||||
}),
|
||||
p2.update({ disabled: true, updatedAt: new Date().toISOString(), closed: true }),
|
||||
reportAgainstTrollingComment.relateTo(peterLustig, 'reviewed', {
|
||||
...disableVariables,
|
||||
resourceId: 'c1',
|
||||
disable: true,
|
||||
closed: true,
|
||||
}),
|
||||
trollingComment.update({ disabled: true, updatedAt: new Date().toISOString(), closed: true }),
|
||||
])
|
||||
authenticatedUser = null
|
||||
|
||||
await Promise.all(
|
||||
[...Array(30).keys()].map(i => {
|
||||
return f.create('User')
|
||||
return factory.create('User')
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(30).keys()].map(() => {
|
||||
return factory.create('Post', {
|
||||
author: jennyRostock,
|
||||
image: faker.image.unsplash.objects(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(6).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: jennyRostock,
|
||||
postId: 'p2',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(4).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: jennyRostock,
|
||||
postId: 'p15',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(2).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: jennyRostock,
|
||||
postId: 'p4',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(21).keys()].map(() => {
|
||||
return factory.create('Post', {
|
||||
author: peterLustig,
|
||||
image: faker.image.unsplash.buildings(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(3).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: peterLustig,
|
||||
postId: 'p4',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(5).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: peterLustig,
|
||||
postId: 'p14',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(6).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: peterLustig,
|
||||
postId: 'p0',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(11).keys()].map(() => {
|
||||
return factory.create('Post', {
|
||||
author: dewey,
|
||||
image: faker.image.unsplash.food(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(7).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: dewey,
|
||||
postId: 'p2',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(5).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: dewey,
|
||||
postId: 'p6',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(2).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: dewey,
|
||||
postId: 'p9',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(16).keys()].map(() => {
|
||||
return factory.create('Post', {
|
||||
author: louie,
|
||||
image: faker.image.unsplash.technology(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(4).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: louie,
|
||||
postId: 'p1',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(8).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: louie,
|
||||
postId: 'p10',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(5).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: louie,
|
||||
postId: 'p13',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(45).keys()].map(() => {
|
||||
return factory.create('Post', {
|
||||
author: bobDerBaumeister,
|
||||
image: faker.image.unsplash.people(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(2).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: bobDerBaumeister,
|
||||
postId: 'p2',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(3).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: bobDerBaumeister,
|
||||
postId: 'p12',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(7).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: bobDerBaumeister,
|
||||
postId: 'p13',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(8).keys()].map(() => {
|
||||
return factory.create('Post', {
|
||||
author: huey,
|
||||
image: faker.image.unsplash.nature(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(6).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: huey,
|
||||
postId: 'p0',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(8).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: huey,
|
||||
postId: 'p13',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
[...Array(9).keys()].map(() => {
|
||||
return factory.create('Comment', {
|
||||
author: huey,
|
||||
postId: 'p15',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
await factory.create('Donations')
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.log('Seeded Data...')
|
||||
process.exit(0)
|
||||
@ -1,17 +1,18 @@
|
||||
import faker from 'faker'
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
export default function create() {
|
||||
return {
|
||||
factory: async ({ args, neodeInstance }) => {
|
||||
const defaults = {
|
||||
email: faker.internet.email(),
|
||||
verifiedAt: new Date().toISOString(),
|
||||
id: uuid(),
|
||||
goal: 15000,
|
||||
progress: 0,
|
||||
}
|
||||
args = {
|
||||
...defaults,
|
||||
...args,
|
||||
}
|
||||
return neodeInstance.create('EmailAddress', args)
|
||||
return neodeInstance.create('Donations', args)
|
||||
},
|
||||
}
|
||||
}
|
||||
22
backend/src/factories/emailAddresses.js
Normal file
22
backend/src/factories/emailAddresses.js
Normal file
@ -0,0 +1,22 @@
|
||||
import faker from 'faker'
|
||||
|
||||
export function defaults({ args }) {
|
||||
const defaults = {
|
||||
email: faker.internet.email(),
|
||||
verifiedAt: new Date().toISOString(),
|
||||
}
|
||||
args = {
|
||||
...defaults,
|
||||
...args,
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
export default function create() {
|
||||
return {
|
||||
factory: async ({ args, neodeInstance }) => {
|
||||
args = defaults({ args })
|
||||
return neodeInstance.create('EmailAddress', args)
|
||||
},
|
||||
}
|
||||
}
|
||||
63
backend/src/factories/index.js
Normal file
63
backend/src/factories/index.js
Normal file
@ -0,0 +1,63 @@
|
||||
import { getDriver, getNeode } from '../db/neo4j'
|
||||
|
||||
const factories = {
|
||||
Badge: require('./badges.js').default,
|
||||
User: require('./users.js').default,
|
||||
Post: require('./posts.js').default,
|
||||
Comment: require('./comments.js').default,
|
||||
Category: require('./categories.js').default,
|
||||
Tag: require('./tags.js').default,
|
||||
SocialMedia: require('./socialMedia.js').default,
|
||||
Location: require('./locations.js').default,
|
||||
EmailAddress: require('./emailAddresses.js').default,
|
||||
UnverifiedEmailAddress: require('./unverifiedEmailAddresses.js').default,
|
||||
Donations: require('./donations.js').default,
|
||||
Report: require('./reports.js').default,
|
||||
}
|
||||
|
||||
export const cleanDatabase = async (options = {}) => {
|
||||
const { driver = getDriver() } = options
|
||||
const session = driver.session()
|
||||
try {
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (everything)
|
||||
DETACH DELETE everything
|
||||
`,
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export default function Factory(options = {}) {
|
||||
const { neo4jDriver = getDriver(), neodeInstance = getNeode() } = options
|
||||
|
||||
const result = {
|
||||
neo4jDriver,
|
||||
factories,
|
||||
lastResponse: null,
|
||||
neodeInstance,
|
||||
async create(node, args = {}) {
|
||||
const { factory } = this.factories[node](args)
|
||||
this.lastResponse = await factory({
|
||||
args,
|
||||
neodeInstance,
|
||||
factoryInstance: this,
|
||||
})
|
||||
return this.lastResponse
|
||||
},
|
||||
|
||||
async cleanDatabase() {
|
||||
this.lastResponse = await cleanDatabase({
|
||||
driver: this.neo4jDriver,
|
||||
})
|
||||
return this
|
||||
},
|
||||
}
|
||||
result.create.bind(result)
|
||||
result.cleanDatabase.bind(result)
|
||||
return result
|
||||
}
|
||||
@ -19,11 +19,17 @@ export default function create() {
|
||||
visibility: 'public',
|
||||
deleted: false,
|
||||
categoryIds: [],
|
||||
imageBlurred: false,
|
||||
imageAspectRatio: 1.333,
|
||||
pinned: null,
|
||||
}
|
||||
args = {
|
||||
...defaults,
|
||||
...args,
|
||||
}
|
||||
// Convert false to null
|
||||
args.pinned = args.pinned || null
|
||||
|
||||
args.slug = args.slug || slugify(args.title, { lower: true })
|
||||
args.contentExcerpt = args.contentExcerpt || args.content
|
||||
|
||||
@ -34,7 +40,6 @@ export default function create() {
|
||||
if (categoryIds)
|
||||
categories = await Promise.all(categoryIds.map(id => neodeInstance.find('Category', id)))
|
||||
categories = categories || (await Promise.all([factoryInstance.create('Category')]))
|
||||
|
||||
const { tagIds = [] } = args
|
||||
delete args.tags
|
||||
const tags = await Promise.all(
|
||||
@ -49,9 +54,21 @@ export default function create() {
|
||||
if (author && authorId) throw new Error('You provided both author and authorId')
|
||||
if (authorId) author = await neodeInstance.find('User', authorId)
|
||||
author = author || (await factoryInstance.create('User'))
|
||||
|
||||
const post = await neodeInstance.create('Post', args)
|
||||
await post.relateTo(author, 'author')
|
||||
|
||||
if (args.pinned) {
|
||||
args.pinnedAt = args.pinnedAt || new Date().toISOString()
|
||||
if (!args.pinnedBy) {
|
||||
const admin = await factoryInstance.create('User', {
|
||||
role: 'admin',
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
await admin.relateTo(post, 'pinned')
|
||||
args.pinnedBy = admin
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(categories.map(c => c.relateTo(post, 'post')))
|
||||
await Promise.all(tags.map(t => t.relateTo(post, 'post')))
|
||||
return post
|
||||
7
backend/src/factories/reports.js
Normal file
7
backend/src/factories/reports.js
Normal file
@ -0,0 +1,7 @@
|
||||
export default function create() {
|
||||
return {
|
||||
factory: async ({ args, neodeInstance }) => {
|
||||
return neodeInstance.create('Report', args)
|
||||
},
|
||||
}
|
||||
}
|
||||
10
backend/src/factories/unverifiedEmailAddresses.js
Normal file
10
backend/src/factories/unverifiedEmailAddresses.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { defaults } from './emailAddresses.js'
|
||||
|
||||
export default function create() {
|
||||
return {
|
||||
factory: async ({ args, neodeInstance }) => {
|
||||
args = defaults({ args })
|
||||
return neodeInstance.create('UnverifiedEmailAddress', args)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import faker from 'faker'
|
||||
import uuid from 'uuid/v4'
|
||||
import encryptPassword from '../../helpers/encryptPassword'
|
||||
import encryptPassword from '../helpers/encryptPassword'
|
||||
import slugify from 'slug'
|
||||
|
||||
export default function create() {
|
||||
@ -16,6 +16,9 @@ export default function create() {
|
||||
about: faker.lorem.paragraph(),
|
||||
termsAndConditionsAgreedVersion: '0.0.1',
|
||||
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
|
||||
allowEmbedIframes: false,
|
||||
showShoutsPublicly: false,
|
||||
locale: 'en',
|
||||
}
|
||||
defaults.slug = slugify(defaults.name, { lower: true })
|
||||
args = {
|
||||
@ -24,7 +27,15 @@ export default function create() {
|
||||
}
|
||||
args = await encryptPassword(args)
|
||||
const user = await neodeInstance.create('User', args)
|
||||
const email = await factoryInstance.create('EmailAddress', { email: args.email })
|
||||
|
||||
let email
|
||||
if (typeof args.email === 'object') {
|
||||
// probably a neode node
|
||||
email = args.email
|
||||
} else {
|
||||
email = await factoryInstance.create('EmailAddress', { email: args.email })
|
||||
}
|
||||
|
||||
await user.relateTo(email, 'primaryEmail')
|
||||
await email.relateTo(user, 'belongsTo')
|
||||
return user
|
||||
5
backend/src/helpers/jest.js
Normal file
5
backend/src/helpers/jest.js
Normal file
@ -0,0 +1,5 @@
|
||||
//* This is a fake ES2015 template string, just to benefit of syntax
|
||||
// highlighting of `gql` template strings in certain editors.
|
||||
export function gql(strings) {
|
||||
return strings.join('')
|
||||
}
|
||||
@ -2,7 +2,8 @@ import createServer from './server'
|
||||
import CONFIG from './config'
|
||||
|
||||
const { app } = createServer()
|
||||
app.listen({ port: CONFIG.GRAPHQL_PORT }, () => {
|
||||
const url = new URL(CONFIG.GRAPHQL_URI)
|
||||
app.listen({ port: url.port }, () => {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`)
|
||||
})
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
import { request } from 'graphql-request'
|
||||
|
||||
// this is the to-be-tested server host
|
||||
// not to be confused with the seeder host
|
||||
export const host = 'http://127.0.0.1:4123'
|
||||
|
||||
export async function login(variables) {
|
||||
const mutation = `
|
||||
mutation($email: String!, $password: String!) {
|
||||
login(email: $email, password: $password)
|
||||
}
|
||||
`
|
||||
const response = await request(host, mutation, variables)
|
||||
return {
|
||||
authorization: `Bearer ${response.login}`,
|
||||
}
|
||||
}
|
||||
|
||||
//* This is a fake ES2015 template string, just to benefit of syntax
|
||||
// highlighting of `gql` template strings in certain editors.
|
||||
export function gql(strings) {
|
||||
return strings.join('')
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -12,19 +12,27 @@ export default async (driver, authorizationHeader) => {
|
||||
return null
|
||||
}
|
||||
const session = driver.session()
|
||||
const query = `
|
||||
MATCH (user:User {id: $id, deleted: false, disabled: false })
|
||||
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
||||
LIMIT 1
|
||||
`
|
||||
const result = await session.run(query, { id })
|
||||
session.close()
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const updateUserLastActiveTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $id, deleted: false, disabled: false })
|
||||
SET user.lastActiveAt = toString(datetime())
|
||||
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
||||
LIMIT 1
|
||||
`,
|
||||
{ id },
|
||||
)
|
||||
return updateUserLastActiveTransactionResponse.records.map(record => record.get('user'))
|
||||
})
|
||||
if (!currentUser) return null
|
||||
return {
|
||||
token,
|
||||
...currentUser,
|
||||
try {
|
||||
const [currentUser] = await writeTxResultPromise
|
||||
if (!currentUser) return null
|
||||
return {
|
||||
token,
|
||||
...currentUser,
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import Factory from '../seed/factories/index'
|
||||
import { getDriver } from '../bootstrap/neo4j'
|
||||
import Factory from '../factories/index'
|
||||
import { getDriver, getNeode } from '../db/neo4j'
|
||||
import decode from './decode'
|
||||
|
||||
const factory = Factory()
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
|
||||
// here is the decoded JWT token:
|
||||
// {
|
||||
@ -85,6 +86,33 @@ describe('decode', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('sets `lastActiveAt`', async () => {
|
||||
let user = await neode.first('User', { id: 'u3' })
|
||||
await expect(user.toJson()).resolves.not.toHaveProperty('lastActiveAt')
|
||||
await decode(driver, authorizationHeader)
|
||||
user = await neode.first('User', { id: 'u3' })
|
||||
await expect(user.toJson()).resolves.toMatchObject({
|
||||
lastActiveAt: expect.any(String),
|
||||
})
|
||||
})
|
||||
|
||||
it('updates `lastActiveAt` for every authenticated request', async () => {
|
||||
let user = await neode.first('User', { id: 'u3' })
|
||||
await user.update({
|
||||
updatedAt: new Date().toISOString(),
|
||||
lastActiveAt: '2019-10-03T23:33:08.598Z',
|
||||
})
|
||||
await expect(user.toJson()).resolves.toMatchObject({
|
||||
lastActiveAt: '2019-10-03T23:33:08.598Z',
|
||||
})
|
||||
await decode(driver, authorizationHeader)
|
||||
user = await neode.first('User', { id: 'u3' })
|
||||
await expect(user.toJson()).resolves.toMatchObject({
|
||||
// should be a different time by now ;)
|
||||
lastActiveAt: expect.not.stringContaining('2019-10-03T23:33'),
|
||||
})
|
||||
})
|
||||
|
||||
describe('but user is deleted', () => {
|
||||
beforeEach(async () => {
|
||||
await user.update({ updatedAt: new Date().toISOString(), deleted: true })
|
||||
@ -92,6 +120,7 @@ describe('decode', () => {
|
||||
|
||||
it('returns null', returnsNull)
|
||||
})
|
||||
|
||||
describe('but user is disabled', () => {
|
||||
beforeEach(async () => {
|
||||
await user.update({ updatedAt: new Date().toISOString(), disabled: true })
|
||||
|
||||
@ -1,51 +1,51 @@
|
||||
import { generateRsaKeyPair } from '../activitypub/security'
|
||||
import { activityPub } from '../activitypub/ActivityPub'
|
||||
import as from 'activitystrea.ms'
|
||||
// import as from 'activitystrea.ms'
|
||||
|
||||
const debug = require('debug')('backend:schema')
|
||||
// const debug = require('debug')('backend:schema')
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
args.activityId = activityPub.generateStatusId(context.user.slug)
|
||||
args.objectId = activityPub.generateStatusId(context.user.slug)
|
||||
// CreatePost: async (resolve, root, args, context, info) => {
|
||||
// args.activityId = activityPub.generateStatusId(context.user.slug)
|
||||
// args.objectId = activityPub.generateStatusId(context.user.slug)
|
||||
|
||||
const post = await resolve(root, args, context, info)
|
||||
// const post = await resolve(root, args, context, info)
|
||||
|
||||
const { user: author } = context
|
||||
const actorId = author.actorId
|
||||
debug(`actorId = ${actorId}`)
|
||||
const createActivity = await new Promise((resolve, reject) => {
|
||||
as.create()
|
||||
.id(`${actorId}/status/${args.activityId}`)
|
||||
.actor(`${actorId}`)
|
||||
.object(
|
||||
as
|
||||
.article()
|
||||
.id(`${actorId}/status/${post.id}`)
|
||||
.content(post.content)
|
||||
.to('https://www.w3.org/ns/activitystreams#Public')
|
||||
.publishedNow()
|
||||
.attributedTo(`${actorId}`),
|
||||
)
|
||||
.prettyWrite((err, doc) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
debug(doc)
|
||||
const parsedDoc = JSON.parse(doc)
|
||||
parsedDoc.send = true
|
||||
resolve(JSON.stringify(parsedDoc))
|
||||
}
|
||||
})
|
||||
})
|
||||
try {
|
||||
await activityPub.sendActivity(createActivity)
|
||||
} catch (e) {
|
||||
debug(`error sending post activity\n${e}`)
|
||||
}
|
||||
return post
|
||||
},
|
||||
// const { user: author } = context
|
||||
// const actorId = author.actorId
|
||||
// debug(`actorId = ${actorId}`)
|
||||
// const createActivity = await new Promise((resolve, reject) => {
|
||||
// as.create()
|
||||
// .id(`${actorId}/status/${args.activityId}`)
|
||||
// .actor(`${actorId}`)
|
||||
// .object(
|
||||
// as
|
||||
// .article()
|
||||
// .id(`${actorId}/status/${post.id}`)
|
||||
// .content(post.content)
|
||||
// .to('https://www.w3.org/ns/activitystreams#Public')
|
||||
// .publishedNow()
|
||||
// .attributedTo(`${actorId}`),
|
||||
// )
|
||||
// .prettyWrite((err, doc) => {
|
||||
// if (err) {
|
||||
// reject(err)
|
||||
// } else {
|
||||
// debug(doc)
|
||||
// const parsedDoc = JSON.parse(doc)
|
||||
// parsedDoc.send = true
|
||||
// resolve(JSON.stringify(parsedDoc))
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// try {
|
||||
// await activityPub.sendActivity(createActivity)
|
||||
// } catch (e) {
|
||||
// debug(`error sending post activity\n${e}`)
|
||||
// }
|
||||
// return post
|
||||
// },
|
||||
SignupVerification: async (resolve, root, args, context, info) => {
|
||||
const keys = generateRsaKeyPair()
|
||||
Object.assign(args, keys)
|
||||
|
||||
@ -1,36 +1,45 @@
|
||||
import CONFIG from '../../config'
|
||||
import nodemailer from 'nodemailer'
|
||||
import { resetPasswordMail, wrongAccountMail } from './templates/passwordReset'
|
||||
import { signupTemplate } from './templates/signup'
|
||||
import { htmlToText } from 'nodemailer-html-to-text'
|
||||
import {
|
||||
signupTemplate,
|
||||
resetPasswordTemplate,
|
||||
wrongAccountTemplate,
|
||||
emailVerificationTemplate,
|
||||
} from './templateBuilder'
|
||||
|
||||
let sendMail
|
||||
if (CONFIG.SMTP_HOST && CONFIG.SMTP_PORT) {
|
||||
sendMail = async templateArgs => {
|
||||
await transporter().sendMail({
|
||||
from: '"Human Connection" <info@human-connection.org>',
|
||||
...templateArgs,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
sendMail = () => {}
|
||||
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
|
||||
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
|
||||
|
||||
let sendMail = () => {}
|
||||
if (!hasEmailConfig) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Warning: Email middleware will not try to send mails.')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sendMail = async templateArgs => {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: CONFIG.SMTP_HOST,
|
||||
port: CONFIG.SMTP_PORT,
|
||||
ignoreTLS: CONFIG.SMTP_IGNORE_TLS === 'true',
|
||||
secure: false, // true for 465, false for other ports
|
||||
auth: hasAuthData && {
|
||||
user: CONFIG.SMTP_USERNAME,
|
||||
pass: CONFIG.SMTP_PASSWORD,
|
||||
},
|
||||
})
|
||||
|
||||
const transporter = () => {
|
||||
const configs = {
|
||||
host: CONFIG.SMTP_HOST,
|
||||
port: CONFIG.SMTP_PORT,
|
||||
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
|
||||
secure: false, // true for 465, false for other ports
|
||||
transporter.use(
|
||||
'compile',
|
||||
htmlToText({
|
||||
ignoreImage: true,
|
||||
wordwrap: false,
|
||||
}),
|
||||
)
|
||||
|
||||
await transporter.sendMail(templateArgs)
|
||||
}
|
||||
const { SMTP_USERNAME: user, SMTP_PASSWORD: pass } = CONFIG
|
||||
if (user && pass) {
|
||||
configs.auth = { user, pass }
|
||||
}
|
||||
return nodemailer.createTransport(configs)
|
||||
}
|
||||
|
||||
const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
|
||||
@ -41,15 +50,26 @@ const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
|
||||
return response
|
||||
}
|
||||
|
||||
const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo) => {
|
||||
const { email } = args
|
||||
const { email: userFound, nonce, name } = await resolve(root, args, context, resolveInfo)
|
||||
const template = userFound ? resetPasswordTemplate : wrongAccountTemplate
|
||||
await sendMail(template({ email, nonce, name }))
|
||||
return true
|
||||
}
|
||||
|
||||
const sendEmailVerificationMail = async (resolve, root, args, context, resolveInfo) => {
|
||||
const response = await resolve(root, args, context, resolveInfo)
|
||||
const { email, nonce, name } = response
|
||||
await sendMail(emailVerificationTemplate({ email, nonce, name }))
|
||||
delete response.nonce
|
||||
return response
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
requestPasswordReset: async (resolve, root, args, context, resolveInfo) => {
|
||||
const { email } = args
|
||||
const { email: emailFound, nonce, name } = await resolve(root, args, context, resolveInfo)
|
||||
const mailTemplate = emailFound ? resetPasswordMail : wrongAccountMail
|
||||
await sendMail(mailTemplate({ email, nonce, name }))
|
||||
return true
|
||||
},
|
||||
AddEmailAddress: sendEmailVerificationMail,
|
||||
requestPasswordReset: sendPasswordResetMail,
|
||||
Signup: sendSignupMail,
|
||||
SignupByInvitation: sendSignupMail,
|
||||
},
|
||||
|
||||
77
backend/src/middleware/email/templateBuilder.js
Normal file
77
backend/src/middleware/email/templateBuilder.js
Normal file
@ -0,0 +1,77 @@
|
||||
import mustache from 'mustache'
|
||||
import CONFIG from '../../config'
|
||||
|
||||
import * as templates from './templates'
|
||||
|
||||
const from = '"Human Connection" <info@human-connection.org>'
|
||||
const supportUrl = 'https://human-connection.org/en/contact'
|
||||
|
||||
export const signupTemplate = ({ email, nonce }) => {
|
||||
const subject = 'Willkommen, Bienvenue, Welcome to Human Connection!'
|
||||
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
|
||||
actionUrl.searchParams.set('nonce', nonce)
|
||||
actionUrl.searchParams.set('email', email)
|
||||
|
||||
return {
|
||||
from,
|
||||
to: email,
|
||||
subject,
|
||||
html: mustache.render(
|
||||
templates.layout,
|
||||
{ actionUrl, nonce, supportUrl, subject },
|
||||
{ content: templates.signup },
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export const emailVerificationTemplate = ({ email, nonce, name }) => {
|
||||
const subject = 'Neue E-Mail Adresse | New E-Mail Address'
|
||||
const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI)
|
||||
actionUrl.searchParams.set('nonce', nonce)
|
||||
actionUrl.searchParams.set('email', email)
|
||||
|
||||
return {
|
||||
from,
|
||||
to: email,
|
||||
subject,
|
||||
html: mustache.render(
|
||||
templates.layout,
|
||||
{ actionUrl, name, nonce, supportUrl, subject },
|
||||
{ content: templates.emailVerification },
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export const resetPasswordTemplate = ({ email, nonce, name }) => {
|
||||
const subject = 'Neues Passwort | Reset Password'
|
||||
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
|
||||
actionUrl.searchParams.set('nonce', nonce)
|
||||
actionUrl.searchParams.set('email', email)
|
||||
|
||||
return {
|
||||
from,
|
||||
to: email,
|
||||
subject,
|
||||
html: mustache.render(
|
||||
templates.layout,
|
||||
{ actionUrl, name, nonce, supportUrl, subject },
|
||||
{ content: templates.passwordReset },
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export const wrongAccountTemplate = ({ email }) => {
|
||||
const subject = 'Falsche Mailadresse? | Wrong E-mail?'
|
||||
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
|
||||
|
||||
return {
|
||||
from,
|
||||
to: email,
|
||||
subject,
|
||||
html: mustache.render(
|
||||
templates.layout,
|
||||
{ actionUrl, supportUrl },
|
||||
{ content: templates.wrongAccount },
|
||||
),
|
||||
}
|
||||
}
|
||||
186
backend/src/middleware/email/templates/emailVerification.html
Normal file
186
backend/src/middleware/email/templates/emailVerification.html
Normal file
@ -0,0 +1,186 @@
|
||||
<!-- Email Body German : BEGIN -->
|
||||
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||
style="margin: auto;">
|
||||
|
||||
<!-- Hero Image, Flush : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff;">
|
||||
<img
|
||||
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
|
||||
width="600" height="" alt="Human Connection community logo" border="0"
|
||||
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
|
||||
class="g-img">
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Hero Image, Flush : END -->
|
||||
|
||||
<!-- 1 Column Text + Button : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<h1
|
||||
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
|
||||
Hallo {{ name }}!</h1>
|
||||
<p style="margin: 0;">Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button
|
||||
kannst Du Deine neue E-Mail Adresse bestätigen:</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0 20px;">
|
||||
<!-- Button : BEGIN -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
|
||||
<tr>
|
||||
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
|
||||
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
|
||||
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">E-Mail
|
||||
Adresse
|
||||
bestätigen</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Button : END -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text + Button : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-bottom: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht
|
||||
einfach ignorieren. Melde Dich gerne <a href="{{{ supportUrl }}}" style="color: #17b53e;">bei
|
||||
unserem Support Team</a>, wenn du noch Fragen hast!</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
|
||||
Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
|
||||
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
|
||||
style="color: #17b53e;">Human Connection</a>!</p>
|
||||
<p style="margin: 0; margin-bottom: 10px;">– Dein Human Connection Team</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="display: none;">
|
||||
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
</table>
|
||||
<!-- Email Body German : END -->
|
||||
|
||||
<!-- Email Body English : BEGIN -->
|
||||
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||
style="margin: auto;">
|
||||
<tr>
|
||||
<td style="padding: 20px 0; text-align: center">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Hero Image, Flush : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff;">
|
||||
<img
|
||||
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
|
||||
width="600" height="" alt="Human Connection community logo" border="0"
|
||||
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
|
||||
class="g-img">
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Hero Image, Flush : END -->
|
||||
|
||||
<!-- 1 Column Text + Button : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<h1
|
||||
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
|
||||
Hello {{ name }}!</h1>
|
||||
<p style="margin: 0;">So, you want to change your e-mail? No problem! Just click the button below to verify
|
||||
your new address:</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0 20px;">
|
||||
<!-- Button : BEGIN -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
|
||||
<tr>
|
||||
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
|
||||
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
|
||||
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Verify
|
||||
e-mail address</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Button : END -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text + Button : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-bottom: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">If you don't want to change your e-mail address feel free to ignore this message. You
|
||||
can
|
||||
also <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our
|
||||
support team</a> if you have any questions!</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">If the above button doesn't work you can also copy the following code into your
|
||||
browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
|
||||
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org"
|
||||
style="color: #17b53e;">Human Connection</a>!</p>
|
||||
<p style="margin: 0; margin-bottom: 10px;">– The Human Connection Team</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
</table>
|
||||
<!-- Email Body English : END -->
|
||||
11
backend/src/middleware/email/templates/index.js
Normal file
11
backend/src/middleware/email/templates/index.js
Normal file
@ -0,0 +1,11 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const readFile = fileName => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')
|
||||
|
||||
export const signup = readFile('./signup.html')
|
||||
export const passwordReset = readFile('./resetPassword.html')
|
||||
export const wrongAccount = readFile('./wrongAccount.html')
|
||||
export const emailVerification = readFile('./emailVerification.html')
|
||||
|
||||
export const layout = readFile('./layout.html')
|
||||
196
backend/src/middleware/email/templates/layout.html
Normal file
196
backend/src/middleware/email/templates/layout.html
Normal file
@ -0,0 +1,196 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
|
||||
<title>{{ subject }}</title>
|
||||
|
||||
<!--[if mso]>
|
||||
<style>
|
||||
* {
|
||||
font-family: sans-serif !important;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
|
||||
<!--[if !mso]><!-->
|
||||
<link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
|
||||
<!--<![endif]-->
|
||||
|
||||
<!-- CSS RESETS -->
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
* {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
div[style*="margin: 16px 0"] {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
mso-table-lspace: 0pt !important;
|
||||
mso-table-rspace: 0pt !important;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0 !important;
|
||||
border-collapse: collapse !important;
|
||||
table-layout: fixed !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a[x-apple-data-detectors],
|
||||
.unstyle-auto-detected-links a,
|
||||
.aBn {
|
||||
border-bottom: 0 !important;
|
||||
cursor: default !important;
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
|
||||
.a6S {
|
||||
display: none !important;
|
||||
opacity: 0.01 !important;
|
||||
}
|
||||
|
||||
.im {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
img.g-img+div {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
|
||||
u~div .email-container {
|
||||
min-width: 320px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPhone 6, 6S, 7, 8, and X */
|
||||
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
|
||||
u~div .email-container {
|
||||
min-width: 375px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPhone 6+, 7+, and 8+ */
|
||||
@media only screen and (min-device-width: 414px) {
|
||||
u~div .email-container {
|
||||
min-width: 414px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--[if gte mso 9]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
|
||||
<!-- PROGRESSIVE ENHANCEMENTS -->
|
||||
<style>
|
||||
.button-td,
|
||||
.button-a {
|
||||
transition: all 100ms ease-in;
|
||||
}
|
||||
|
||||
.button-td-primary:hover,
|
||||
.button-a-primary:hover {
|
||||
background: #19c243 !important;
|
||||
border-color: #555555 !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.email-container p {
|
||||
font-size: 17px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f5f4f6;">
|
||||
<center style="width: 100%; background-color: #f5f4f6;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f5f4f6;">
|
||||
<tr>
|
||||
<td>
|
||||
<![endif]-->
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;" class="email-container">
|
||||
<!--[if mso]>
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
|
||||
<tr>
|
||||
<td>
|
||||
<![endif]-->
|
||||
|
||||
<p style="color:#19c243; font-style: italic; font-family: Lato, sans-serif; font-size: 16px; padding-top: 20px;">English version below!</p>
|
||||
|
||||
{{> content}}
|
||||
|
||||
<!-- Email Footer : BEGIN -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||
style="margin: auto;">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
|
||||
<br><br>
|
||||
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
|
||||
Teck<br>Germany</span>
|
||||
<br><br>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Email Footer : END -->
|
||||
|
||||
<!--[if mso]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</center>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,83 +0,0 @@
|
||||
import CONFIG from '../../../config'
|
||||
|
||||
export const resetPasswordMail = options => {
|
||||
const {
|
||||
name,
|
||||
email,
|
||||
nonce,
|
||||
subject = 'Use this link to reset your password. The link is only valid for 24 hours.',
|
||||
supportUrl = 'https://human-connection.org/en/contact/',
|
||||
} = options
|
||||
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
|
||||
actionUrl.searchParams.set('nonce', nonce)
|
||||
actionUrl.searchParams.set('email', email)
|
||||
|
||||
return {
|
||||
to: email,
|
||||
subject,
|
||||
text: `
|
||||
Hi ${name}!
|
||||
|
||||
You recently requested to reset your password for your Human Connection account.
|
||||
Use the link below to reset it. This password reset is only valid for the next
|
||||
24 hours.
|
||||
|
||||
${actionUrl}
|
||||
|
||||
If you did not request a password reset, please ignore this email or contact
|
||||
support if you have questions:
|
||||
|
||||
${supportUrl}
|
||||
|
||||
Thanks,
|
||||
The Human Connection Team
|
||||
|
||||
If you're having trouble with the link above, you can manually copy and
|
||||
paste the following code into your browser window:
|
||||
|
||||
${nonce}
|
||||
|
||||
Human Connection gemeinnützige GmbH
|
||||
Bahnhofstr. 11
|
||||
73235 Weilheim / Teck
|
||||
Deutschland
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
export const wrongAccountMail = options => {
|
||||
const {
|
||||
email,
|
||||
subject = `We received a request to reset your password with this email address (${email})`,
|
||||
supportUrl = 'https://human-connection.org/en/contact/',
|
||||
} = options
|
||||
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
|
||||
return {
|
||||
to: email,
|
||||
subject,
|
||||
text: `
|
||||
We received a request to reset the password to access Human Connection with your
|
||||
email address, but we were unable to find an account associated with this
|
||||
address.
|
||||
|
||||
If you use Human Connection and were expecting this email, consider trying to
|
||||
request a password reset using the email address associated with your account.
|
||||
Try a different email:
|
||||
|
||||
${actionUrl}
|
||||
|
||||
If you do not use Human Connection or did not request a password reset, please
|
||||
ignore this email. Feel free to contact support if you have further questions:
|
||||
|
||||
${supportUrl}
|
||||
|
||||
Thanks,
|
||||
The Human Connection Team
|
||||
|
||||
Human Connection gemeinnützige GmbH
|
||||
Bahnhofstr. 11
|
||||
73235 Weilheim / Teck
|
||||
Deutschland
|
||||
`,
|
||||
}
|
||||
}
|
||||
185
backend/src/middleware/email/templates/resetPassword.html
Normal file
185
backend/src/middleware/email/templates/resetPassword.html
Normal file
@ -0,0 +1,185 @@
|
||||
<!-- Email Body German : BEGIN -->
|
||||
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||
style="margin: auto;">
|
||||
|
||||
<!-- Hero Image, Flush : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff;">
|
||||
<img
|
||||
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
|
||||
width="600" height="" alt="Human Connection community logo" border="0"
|
||||
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
|
||||
class="g-img">
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Hero Image, Flush : END -->
|
||||
|
||||
<!-- 1 Column Text + Button : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<h1
|
||||
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
|
||||
Hallo {{ name }}!</h1>
|
||||
<p style="margin: 0;">Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button
|
||||
kannst Du innerhalb der nächsten 24 Stunden Dein Passwort zurücksetzen:</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0 20px;">
|
||||
<!-- Button : BEGIN -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
|
||||
<tr>
|
||||
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
|
||||
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
|
||||
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Passwort
|
||||
zurücksetzen</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Button : END -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text + Button : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-bottom: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">Falls Du kein neues Passwort angefordert hast, kannst Du diese E-Mail einfach
|
||||
ignorieren. Wenn Du noch Fragen hast, melde Dich gerne <a href="{{{ supportUrl }}}"
|
||||
style="color: #17b53e;">bei
|
||||
unserem Support Team</a>!</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
|
||||
Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
|
||||
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
|
||||
style="color: #17b53e;">Human Connection</a>!</p>
|
||||
<p style="margin: 0; margin-bottom: 10px;">– Dein Human Connection Team</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="display: none;">
|
||||
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
</table>
|
||||
<!-- Email Body German : END -->
|
||||
|
||||
<!-- Email Body English : BEGIN -->
|
||||
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||
style="margin: auto;">
|
||||
<tr>
|
||||
<td style="padding: 20px 0; text-align: center">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Hero Image, Flush : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff;">
|
||||
<img
|
||||
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
|
||||
width="600" height="" alt="Human Connection community logo" border="0"
|
||||
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
|
||||
class="g-img">
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Hero Image, Flush : END -->
|
||||
|
||||
<!-- 1 Column Text + Button : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<h1
|
||||
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
|
||||
Hello {{ name }}!</h1>
|
||||
<p style="margin: 0;">So, you forgot your password? No problem! Just click the button below to reset
|
||||
it within the next 24 hours:</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0 20px;">
|
||||
<!-- Button : BEGIN -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
|
||||
<tr>
|
||||
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
|
||||
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
|
||||
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Reset
|
||||
password</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Button : END -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text + Button : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-bottom: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">If you didn't request a new password feel free to ignore this e-mail. You can
|
||||
also <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our
|
||||
support team</a> if you have any questions!</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">If the above button doesn't work you can also copy the following code into your
|
||||
browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
|
||||
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org"
|
||||
style="color: #17b53e;">Human Connection</a>!</p>
|
||||
<p style="margin: 0; margin-bottom: 10px;">– The Human Connection Team</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
</table>
|
||||
<!-- Email Body English : END -->
|
||||
214
backend/src/middleware/email/templates/signup.html
Normal file
214
backend/src/middleware/email/templates/signup.html
Normal file
@ -0,0 +1,214 @@
|
||||
<!-- Email Body German : BEGIN -->
|
||||
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||
style="margin: auto;">
|
||||
|
||||
<!-- Hero Image, Flush : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff;">
|
||||
<img
|
||||
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
|
||||
width="600" height="" alt="Human Connection community logo" border="0"
|
||||
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
|
||||
class="g-img">
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Hero Image, Flush : END -->
|
||||
|
||||
<!-- 1 Column Text + Button : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<h1
|
||||
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
|
||||
Willkommen bei Human Connection!</h1>
|
||||
<p style="margin: 0;">Danke, dass Du dich angemeldet hast – wir freuen uns, Dich dabei zu haben. Jetzt
|
||||
fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können ... Bitte bestätige
|
||||
Deine E-Mail Adresse:</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0 20px;">
|
||||
<!-- Button : BEGIN -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
|
||||
<tr>
|
||||
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
|
||||
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
|
||||
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Bestätige
|
||||
Deine E-Mail Adresse</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Button : END -->
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: center; color :#17b53e">
|
||||
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text + Button : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
|
||||
<p style="margin: 0;">Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.</p>
|
||||
<p style="margin: 0; margin-top: 10px;">Falls Du Dich nicht selbst bei <a href="https://human-connection.org"
|
||||
style="color: #17b53e;">Human Connection</a> angemeldet hast, schau doch mal vorbei!
|
||||
Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.</p>
|
||||
<p style="margin: 0; margin-top: 10px;">PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese
|
||||
E-Mail einfach ignorieren. ;)</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: center; color :#17b53e">
|
||||
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">Melde Dich gerne <a href="{{{ supportUrl }}}" style="color: #17b53e;">bei
|
||||
unserem Support Team</a>, wenn Du Fragen hast.</p>
|
||||
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
|
||||
style="color: #17b53e;">Human Connection</a>!</p>
|
||||
<p style="margin: 0; margin-bottom: 10px;">– Dein Human Connection Team</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="display: none;">
|
||||
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
</table>
|
||||
<!-- Email Body German : END -->
|
||||
|
||||
<!-- Email Body English : BEGIN -->
|
||||
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||
style="margin: auto;">
|
||||
<tr>
|
||||
<td style="padding: 20px 0; text-align: center">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Hero Image, Flush : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff;">
|
||||
<img
|
||||
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
|
||||
width="600" height="" alt="Human Connection community logo" border="0"
|
||||
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
|
||||
class="g-img">
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Hero Image, Flush : END -->
|
||||
|
||||
<!-- 1 Column Text + Button : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<h1
|
||||
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
|
||||
Welcome to Human Connection!</h1>
|
||||
<p style="margin: 0;">Thank you for joining our cause – it's awesome to have you on board. There's
|
||||
just one tiny step missing before we can start shaping the world together ... Please confirm your
|
||||
e-mail address by clicking the button below:</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0 20px;">
|
||||
<!-- Button : BEGIN -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
|
||||
<tr>
|
||||
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
|
||||
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
|
||||
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Confirm
|
||||
your e-mail address</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Button : END -->
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: center; color :#17b53e">
|
||||
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text + Button : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">If the above button doesn't work, you can also copy the following code into your browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
|
||||
<p style="margin: 0;">However, this only works if you have registered through our website.</p>
|
||||
<p style="margin: 0; margin-top: 10px;">If you didn't sign up for <a href="https://human-connection.org"
|
||||
style="color: #17b53e;">Human Connection</a> we recommend you to check it out!
|
||||
It's a social network from people for people who want to connect and change the world together.</p>
|
||||
<p style="margin: 0; margin-top: 10px;">PS: If you ignore this e-mail we will not create an account
|
||||
for
|
||||
you. ;)</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: center; color :#17b53e">
|
||||
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">Feel free to <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our
|
||||
support team</a> with any
|
||||
questions you have.</p>
|
||||
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org"
|
||||
style="color: #17b53e;">Human Connection</a>!</p>
|
||||
<p style="margin: 0; margin-bottom: 10px;">– The Human Connection Team</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
</table>
|
||||
<!-- Email Body English : END -->
|
||||
@ -1,62 +0,0 @@
|
||||
import CONFIG from '../../../config'
|
||||
|
||||
export const signupTemplate = options => {
|
||||
const {
|
||||
email,
|
||||
nonce,
|
||||
subject = 'Welcome to Human Connection! Here is your signup link.',
|
||||
supportUrl = 'https://human-connection.org/en/contact/',
|
||||
} = options
|
||||
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
|
||||
actionUrl.searchParams.set('nonce', nonce)
|
||||
actionUrl.searchParams.set('email', email)
|
||||
|
||||
return {
|
||||
to: email,
|
||||
subject,
|
||||
text: `
|
||||
Willkommen bei Human Connection! Klick auf diesen Link, um den
|
||||
Registrierungsprozess abzuschließen und um ein Benutzerkonto zu erstellen!
|
||||
|
||||
${actionUrl}
|
||||
|
||||
Alternativ kannst du diesen Code auch kopieren und im Browserfenster einfügen:
|
||||
|
||||
${nonce}
|
||||
|
||||
Bitte ignoriere diese Mail, falls du dich nicht bei Human Connection angemeldet
|
||||
hast. Bei Fragen kontaktiere gerne unseren Support:
|
||||
|
||||
${supportUrl}
|
||||
|
||||
Danke,
|
||||
Das Human Connection Team
|
||||
|
||||
|
||||
English Version
|
||||
===============
|
||||
|
||||
Welcome to Human Connection! Use this link to complete the registration process
|
||||
and create a user account:
|
||||
|
||||
${actionUrl}
|
||||
|
||||
You can also copy+paste this verification nonce in your browser window:
|
||||
|
||||
${nonce}
|
||||
|
||||
If you did not signed up for Human Connection, please ignore this email or
|
||||
contact support if you have questions:
|
||||
|
||||
${supportUrl}
|
||||
|
||||
Thanks,
|
||||
The Human Connection Team
|
||||
|
||||
Human Connection gemeinnützige GmbH
|
||||
Bahnhofstr. 11
|
||||
73235 Weilheim / Teck
|
||||
Deutschland
|
||||
`,
|
||||
}
|
||||
}
|
||||
185
backend/src/middleware/email/templates/wrongAccount.html
Normal file
185
backend/src/middleware/email/templates/wrongAccount.html
Normal file
@ -0,0 +1,185 @@
|
||||
<!-- Email Body German : BEGIN -->
|
||||
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||
style="margin: auto;">
|
||||
|
||||
<!-- Hero Image, Flush : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff;">
|
||||
<img
|
||||
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
|
||||
width="600" height="" alt="Human Connection community logo" border="0"
|
||||
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
|
||||
class="g-img">
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Hero Image, Flush : END -->
|
||||
|
||||
<!-- 1 Column Text + Button : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<h1
|
||||
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
|
||||
Hallo!</h1>
|
||||
<p style="margin: 0;">Du hast bei uns ein neues Passwort angefordert – leider haben wir aber keinen
|
||||
Account mit Deiner E-Mailadresse gefunden. Kann es sein, dass Du mit einer anderen Adresse bei uns
|
||||
angemeldet bist?</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0 20px;">
|
||||
<!-- Button : BEGIN -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
|
||||
<tr>
|
||||
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
|
||||
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
|
||||
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Versuch'
|
||||
es mit einer anderen E-Mail</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Button : END -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text + Button : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">Wenn Du noch keinen Account bei <a href="https://human-connection.org"
|
||||
style="color: #17b53e;">Human Connection</a> hast oder Dein Password gar nicht ändern willst,
|
||||
kannst Du diese E-Mail einfach ignorieren!</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">Ansonsten hilft Dir <a href="{{{ supportUrl }}}" style="color: #17b53e;">unser
|
||||
Support Team</a> gerne weiter.</p>
|
||||
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
|
||||
style="color: #17b53e;">Human Connection</a>!</p>
|
||||
<p style="margin: 0; margin-bottom: 10px;">– Dein Human Connection Team</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="display: none;">
|
||||
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
</table>
|
||||
<!-- Email Body German : END -->
|
||||
|
||||
<!-- Email Body English : BEGIN -->
|
||||
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||
style="margin: auto;">
|
||||
<tr>
|
||||
<td style="padding: 20px 0; text-align: center">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Hero Image, Flush : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff;">
|
||||
<img
|
||||
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
|
||||
width="600" height="" alt="Human Connection community logo" border="0"
|
||||
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
|
||||
class="g-img">
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Hero Image, Flush : END -->
|
||||
|
||||
<!-- 1 Column Text + Button : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<h1
|
||||
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
|
||||
Hello!</h1>
|
||||
<p style="margin: 0;">You requested a password reset but unfortunately we couldn't find an account
|
||||
associated with your e-mail address. Did you maybe use another one when you signed up?</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0 20px;">
|
||||
<!-- Button : BEGIN -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
|
||||
<tr>
|
||||
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
|
||||
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
|
||||
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Try
|
||||
a different e-mail</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Button : END -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text + Button : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">If you don't have an account at <a href="https://human-connection.org"
|
||||
style="color: #17b53e;">Human Connection</a> yet or if you didn't want to reset your password,
|
||||
please ignore this e-mail.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
<!-- 1 Column Text : BEGIN -->
|
||||
<tr>
|
||||
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||
<p style="margin: 0;">Otherwise <a href="{{{ supportUrl }}}" style="color: #17b53e;">our
|
||||
support team</a> will be happy to help you out.</p>
|
||||
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org"
|
||||
style="color: #17b53e;">Human Connection</a>!</p>
|
||||
<p style="margin: 0; margin-bottom: 10px;">– The Human Connection Team</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text : END -->
|
||||
|
||||
</table>
|
||||
<!-- Email Body English : END -->
|
||||
@ -4,23 +4,23 @@ import { exec, build } from 'xregexp/xregexp-all.js'
|
||||
// https://en.wikipedia.org/w/index.php?title=Hashtag&oldid=905141980#Style
|
||||
// here:
|
||||
// 0. Search for whole string.
|
||||
// 1. Hashtag has only all unicode characters and '0-9'.
|
||||
// 2. If it starts with a digit '0-9' than a unicode character has to follow.
|
||||
const regX = build('^/search/hashtag/((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$')
|
||||
// 1. Hashtag has only all unicode letters and '0-9'.
|
||||
// 2. If it starts with a digit '0-9' than a unicode letter has to follow.
|
||||
const regX = build('^((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$')
|
||||
|
||||
export default function(content) {
|
||||
if (!content) return []
|
||||
const $ = cheerio.load(content)
|
||||
// We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware.
|
||||
// But we have to know, which Hashtags are removed from the content as well, so we search for the 'a' html-tag.
|
||||
const urls = $('a')
|
||||
const ids = $('a[data-hashtag-id]')
|
||||
.map((_, el) => {
|
||||
return $(el).attr('href')
|
||||
return $(el).attr('data-hashtag-id')
|
||||
})
|
||||
.get()
|
||||
const hashtags = []
|
||||
urls.forEach(url => {
|
||||
const match = exec(url, regX)
|
||||
ids.forEach(id => {
|
||||
const match = exec(id, regX)
|
||||
if (match != null) {
|
||||
hashtags.push(match[1])
|
||||
}
|
||||
|
||||
@ -8,9 +8,24 @@ describe('extractHashtags', () => {
|
||||
})
|
||||
|
||||
describe('searches through links', () => {
|
||||
it('finds links with and without ".hashtag" class and extracts Hashtag names', () => {
|
||||
const content =
|
||||
'<p><a class="hashtag" href="/search/hashtag/Elections">#Elections</a><a href="/search/hashtag/Democracy">#Democracy</a></p>'
|
||||
it('without `class="hashtag"` but `data-hashtag-id="something"`, and extracts the Hashtag to make a Hashtag link', () => {
|
||||
const content = `
|
||||
<p>
|
||||
<a
|
||||
class="hashtag"
|
||||
data-hashtag-id="Elections"
|
||||
href="/?hashtag=Elections"
|
||||
>
|
||||
#Elections
|
||||
</a>
|
||||
<a
|
||||
data-hashtag-id="Democracy"
|
||||
href="/?hashtag=Democracy"
|
||||
>
|
||||
#Democracy
|
||||
</a>
|
||||
</p>
|
||||
`
|
||||
expect(extractHashtags(content)).toEqual(['Elections', 'Democracy'])
|
||||
})
|
||||
|
||||
@ -20,23 +35,57 @@ describe('extractHashtags', () => {
|
||||
expect(extractHashtags(content)).toEqual([])
|
||||
})
|
||||
|
||||
describe('handles links', () => {
|
||||
it('ignores links with domains', () => {
|
||||
const content =
|
||||
'<p><a class="hashtag" href="http://localhost:3000/search/hashtag/Elections">#Elections</a><a href="/search/hashtag/Democracy">#Democracy</a></p>'
|
||||
expect(extractHashtags(content)).toEqual(['Democracy'])
|
||||
})
|
||||
|
||||
it('ignores Hashtag links with not allowed character combinations', () => {
|
||||
// Allowed are all unicode letters '\pL' and all digits '0-9'. There haveto be at least one letter in it.
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/search/hashtag/AbcDefXyz0123456789!*(),2" class="hashtag" target="_blank">#AbcDefXyz0123456789!*(),2</a>, <a href="/search/hashtag/0123456789" class="hashtag" target="_blank">#0123456789</a>, <a href="/search/hashtag/0123456789a" class="hashtag" target="_blank">#0123456789a</a>, <a href="/search/hashtag/AbcDefXyz0123456789" target="_blank">#AbcDefXyz0123456789</a>, and <a href="/search/hashtag/λαπ" target="_blank">#λαπ</a>.</p>'
|
||||
expect(extractHashtags(content).sort()).toEqual([
|
||||
'0123456789a',
|
||||
'AbcDefXyz0123456789',
|
||||
'λαπ',
|
||||
])
|
||||
})
|
||||
it('ignores hashtag links with unsupported character combinations', () => {
|
||||
// Allowed are all unicode letters '\pL' and all digits '0-9'. There haveto be at least one letter in it.
|
||||
const content = `
|
||||
<p>
|
||||
Something inspirational about
|
||||
<a
|
||||
href="/?hashtag=AbcDefXyz0123456789!*(),2"
|
||||
data-hashtag-id="AbcDefXyz0123456789!*(),2"
|
||||
class="hashtag"
|
||||
target="_blank"
|
||||
>
|
||||
#AbcDefXyz0123456789!*(),2
|
||||
</a>,
|
||||
<a
|
||||
href="/?hashtag=0123456789"
|
||||
data-hashtag-id="0123456789"
|
||||
class="hashtag"
|
||||
target="_blank"
|
||||
>
|
||||
#0123456789
|
||||
</a>,
|
||||
<a href="?hashtag=0123456789a"
|
||||
data-hashtag-id="0123456789a"
|
||||
class="hashtag"
|
||||
target="_blank"
|
||||
>
|
||||
#0123456789a
|
||||
</a>,
|
||||
<a
|
||||
href="/?hashtag=AbcDefXyz0123456789"
|
||||
data-hashtag-id="AbcDefXyz0123456789"
|
||||
class="hashtag"
|
||||
target="_blank"
|
||||
>
|
||||
#AbcDefXyz0123456789
|
||||
</a>, and
|
||||
<a
|
||||
href="/?hashtag=%C4%A7%CF%80%CE%B1%CE%BB"
|
||||
data-hashtag-id="ħπαλ"
|
||||
class="hashtag"
|
||||
target="_blank"
|
||||
>
|
||||
#ħπαλ
|
||||
</a>.
|
||||
</p>
|
||||
`
|
||||
expect(extractHashtags(content).sort()).toEqual([
|
||||
'0123456789a',
|
||||
'AbcDefXyz0123456789',
|
||||
'ħπαλ',
|
||||
])
|
||||
})
|
||||
|
||||
describe('does not crash if', () => {
|
||||
|
||||
@ -2,31 +2,27 @@ import extractHashtags from '../hashtags/extractHashtags'
|
||||
|
||||
const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||
if (!hashtags.length) return
|
||||
|
||||
const session = context.driver.session()
|
||||
// We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement
|
||||
// functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted
|
||||
// and no new Hashtags and relations will be created.
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (p: Post { id: $postId })-[previousRelations: TAGGED]->(t: Tag)
|
||||
DELETE previousRelations
|
||||
RETURN p, t
|
||||
`
|
||||
const cypherCreateNewTagsAndRelations = `
|
||||
MATCH (p: Post { id: $postId})
|
||||
UNWIND $hashtags AS tagName
|
||||
MERGE (t: Tag { id: tagName, disabled: false, deleted: false })
|
||||
MERGE (p)-[:TAGGED]->(t)
|
||||
RETURN p, t
|
||||
`
|
||||
await session.run(cypherDeletePreviousRelations, {
|
||||
postId,
|
||||
})
|
||||
await session.run(cypherCreateNewTagsAndRelations, {
|
||||
postId,
|
||||
hashtags,
|
||||
})
|
||||
session.close()
|
||||
|
||||
try {
|
||||
await session.writeTransaction(txc => {
|
||||
return txc.run(
|
||||
`
|
||||
MATCH (post:Post { id: $postId})
|
||||
OPTIONAL MATCH (post)-[previousRelations:TAGGED]->(tag:Tag)
|
||||
DELETE previousRelations
|
||||
WITH post
|
||||
UNWIND $hashtags AS tagName
|
||||
MERGE (tag:Tag {id: tagName, disabled: false, deleted: false })
|
||||
MERGE (post)-[:TAGGED]->(tag)
|
||||
RETURN post, tag
|
||||
`,
|
||||
{ postId, hashtags },
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { gql } from '../../jest/helpers'
|
||||
import Factory from '../../seed/factories'
|
||||
import { gql } from '../../helpers/jest'
|
||||
import Factory from '../../factories'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import { neode, getDriver } from '../../bootstrap/neo4j'
|
||||
import { getNeode, getDriver } from '../../db/neo4j'
|
||||
import createServer from '../../server'
|
||||
|
||||
let server
|
||||
@ -11,7 +11,7 @@ let hashtagingUser
|
||||
let authenticatedUser
|
||||
const factory = Factory()
|
||||
const driver = getDriver()
|
||||
const instance = neode()
|
||||
const neode = getNeode()
|
||||
const categoryIds = ['cat9']
|
||||
const createPostMutation = gql`
|
||||
mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
|
||||
@ -36,7 +36,7 @@ beforeAll(() => {
|
||||
context: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode: instance,
|
||||
neode,
|
||||
driver,
|
||||
}
|
||||
},
|
||||
@ -48,14 +48,14 @@ beforeAll(() => {
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
hashtagingUser = await instance.create('User', {
|
||||
hashtagingUser = await neode.create('User', {
|
||||
id: 'you',
|
||||
name: 'Al Capone',
|
||||
slug: 'al-capone',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await instance.create('Category', {
|
||||
await neode.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
@ -69,8 +69,27 @@ afterEach(async () => {
|
||||
describe('hashtags', () => {
|
||||
const id = 'p135'
|
||||
const title = 'Two Hashtags'
|
||||
const postContent =
|
||||
'<p>Hey Dude, <a class="hashtag" href="/search/hashtag/Democracy">#Democracy</a> should work equal for everybody!? That seems to be the only way to have equal <a class="hashtag" href="/search/hashtag/Liberty">#Liberty</a> for everyone.</p>'
|
||||
const postContent = `
|
||||
<p>
|
||||
Hey Dude,
|
||||
<a
|
||||
class="hashtag"
|
||||
data-hashtag-id="Democracy"
|
||||
href="/?hashtag=Democracy">
|
||||
#Democracy
|
||||
</a>
|
||||
should work equal for everybody!? That seems to be the only way to have
|
||||
equal
|
||||
<a
|
||||
class="hashtag"
|
||||
data-hashtag-id="Liberty"
|
||||
href="/?hashtag=Liberty"
|
||||
>
|
||||
#Liberty
|
||||
</a>
|
||||
for everyone.
|
||||
</p>
|
||||
`
|
||||
const postWithHastagsQuery = gql`
|
||||
query($id: ID) {
|
||||
Post(id: $id) {
|
||||
@ -129,10 +148,29 @@ describe('hashtags', () => {
|
||||
)
|
||||
})
|
||||
|
||||
describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => {
|
||||
// The already existing Hashtag has no class at this point.
|
||||
const postContent =
|
||||
'<p>Hey Dude, <a class="hashtag" href="/search/hashtag/Elections">#Elections</a> should work equal for everybody!? That seems to be the only way to have equal <a href="/search/hashtag/Liberty">#Liberty</a> for everyone.</p>'
|
||||
describe('updates the Post by removing, keeping and adding one hashtag respectively', () => {
|
||||
// The already existing hashtag has no class at this point.
|
||||
const postContent = `
|
||||
<p>
|
||||
Hey Dude,
|
||||
<a
|
||||
class="hashtag"
|
||||
data-hashtag-id="Elections"
|
||||
href="?hashtag=Elections"
|
||||
>
|
||||
#Elections
|
||||
</a>
|
||||
should work equal for everybody!? That seems to be the only way to
|
||||
have equal
|
||||
<a
|
||||
data-hashtag-id="Liberty"
|
||||
href="?hashtag=Liberty"
|
||||
>
|
||||
#Liberty
|
||||
</a>
|
||||
for everyone.
|
||||
</p>
|
||||
`
|
||||
|
||||
it('only one previous Hashtag and the new Hashtag exists', async () => {
|
||||
await mutate({
|
||||
|
||||
@ -7,7 +7,6 @@ import sluggify from './sluggifyMiddleware'
|
||||
import excerpt from './excerptMiddleware'
|
||||
import xss from './xssMiddleware'
|
||||
import permissions from './permissionsMiddleware'
|
||||
import user from './userMiddleware'
|
||||
import includedFields from './includedFieldsMiddleware'
|
||||
import orderBy from './orderByMiddleware'
|
||||
import validation from './validation/validationMiddleware'
|
||||
@ -18,25 +17,25 @@ import sentry from './sentryMiddleware'
|
||||
|
||||
export default schema => {
|
||||
const middlewares = {
|
||||
permissions,
|
||||
sentry,
|
||||
permissions,
|
||||
xss,
|
||||
activityPub,
|
||||
validation,
|
||||
sluggify,
|
||||
excerpt,
|
||||
email,
|
||||
notifications,
|
||||
hashtags,
|
||||
xss,
|
||||
softDelete,
|
||||
user,
|
||||
includedFields,
|
||||
orderBy,
|
||||
email,
|
||||
}
|
||||
|
||||
let order = [
|
||||
'sentry',
|
||||
'permissions',
|
||||
'xss',
|
||||
// 'activityPub', disabled temporarily
|
||||
'validation',
|
||||
'sluggify',
|
||||
@ -44,9 +43,7 @@ export default schema => {
|
||||
'email',
|
||||
'notifications',
|
||||
'hashtags',
|
||||
'xss',
|
||||
'softDelete',
|
||||
'user',
|
||||
'includedFields',
|
||||
'orderBy',
|
||||
]
|
||||
|
||||
@ -1,135 +1,121 @@
|
||||
import extractMentionedUsers from './mentions/extractMentionedUsers'
|
||||
import { validateNotifyUsers } from '../validation/validationMiddleware'
|
||||
|
||||
const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
||||
if (!idsOfUsers.length) return
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfUsers = extractMentionedUsers(args.content)
|
||||
const post = await resolve(root, args, context, resolveInfo)
|
||||
if (post && idsOfUsers && idsOfUsers.length)
|
||||
await notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context)
|
||||
return post
|
||||
}
|
||||
|
||||
// Checked here, because it does not go through GraphQL checks at all in this file.
|
||||
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post']
|
||||
if (!reasonsAllowed.includes(reason)) {
|
||||
throw new Error('Notification reason is not allowed!')
|
||||
}
|
||||
if (
|
||||
(label === 'Post' && reason !== 'mentioned_in_post') ||
|
||||
(label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason))
|
||||
) {
|
||||
throw new Error('Notification does not fit the reason!')
|
||||
}
|
||||
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
const { content } = args
|
||||
let idsOfUsers = extractMentionedUsers(content)
|
||||
const comment = await resolve(root, args, context, resolveInfo)
|
||||
const [postAuthor] = await postAuthorOfComment(comment.id, { context })
|
||||
idsOfUsers = idsOfUsers.filter(id => id !== postAuthor.id)
|
||||
if (idsOfUsers && idsOfUsers.length)
|
||||
await notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context)
|
||||
if (context.user.id !== postAuthor.id)
|
||||
await notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context)
|
||||
return comment
|
||||
}
|
||||
|
||||
const postAuthorOfComment = async (commentId, { context }) => {
|
||||
const session = context.driver.session()
|
||||
let cypher
|
||||
let postAuthorId
|
||||
try {
|
||||
postAuthorId = await session.readTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (author:User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN author { .id } as authorId
|
||||
`,
|
||||
{ commentId },
|
||||
)
|
||||
})
|
||||
return postAuthorId.records.map(record => record.get('authorId'))
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
|
||||
await validateNotifyUsers(label, reason)
|
||||
let mentionedCypher
|
||||
switch (reason) {
|
||||
case 'mentioned_in_post': {
|
||||
cypher = `
|
||||
mentionedCypher = `
|
||||
MATCH (post: Post { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
`
|
||||
break
|
||||
}
|
||||
case 'mentioned_in_comment': {
|
||||
cypher = `
|
||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (user)<-[:BLOCKED]-(postAuthor)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
`
|
||||
break
|
||||
}
|
||||
case 'commented_on_post': {
|
||||
cypher = `
|
||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (author)<-[:BLOCKED]-(user)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
mentionedCypher = `
|
||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (user)<-[:BLOCKED]-(postAuthor)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
`
|
||||
break
|
||||
}
|
||||
}
|
||||
await session.run(cypher, {
|
||||
id,
|
||||
idsOfUsers,
|
||||
reason,
|
||||
})
|
||||
session.close()
|
||||
}
|
||||
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfUsers = extractMentionedUsers(args.content)
|
||||
|
||||
const post = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
if (post) {
|
||||
await notifyUsers('Post', post.id, idsOfUsers, 'mentioned_in_post', context)
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfUsers = extractMentionedUsers(args.content)
|
||||
|
||||
const comment = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
if (comment) {
|
||||
await notifyUsers('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context)
|
||||
}
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
const handleCreateComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo)
|
||||
|
||||
if (comment) {
|
||||
const session = context.driver.session()
|
||||
const cypherFindUser = `
|
||||
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN user { .id }
|
||||
`
|
||||
const result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
mentionedCypher += `
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
`
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(mentionedCypher, { id, idsOfUsers, reason })
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
const [postAuthor] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
if (context.user.id !== postAuthor.id) {
|
||||
await notifyUsers('Comment', comment.id, [postAuthor.id], 'commented_on_post', context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return comment
|
||||
const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, context) => {
|
||||
await validateNotifyUsers(label, reason)
|
||||
const session = context.driver.session()
|
||||
|
||||
try {
|
||||
await session.writeTransaction(async transaction => {
|
||||
await transaction.run(
|
||||
`
|
||||
MATCH (postAuthor:User {id: $postAuthorId})-[:WROTE]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User)
|
||||
WHERE NOT (postAuthor)-[:BLOCKED]-(commenter)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(postAuthor)
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
`,
|
||||
{ commentId, postAuthorId, reason },
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: handleContentDataOfPost,
|
||||
UpdatePost: handleContentDataOfPost,
|
||||
CreateComment: handleCreateComment,
|
||||
CreateComment: handleContentDataOfComment,
|
||||
UpdateComment: handleContentDataOfComment,
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,17 +1,13 @@
|
||||
import { gql } from '../../jest/helpers'
|
||||
import Factory from '../../seed/factories'
|
||||
import { gql } from '../../helpers/jest'
|
||||
import Factory from '../../factories'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import { neode, getDriver } from '../../bootstrap/neo4j'
|
||||
import { getNeode, getDriver } from '../../db/neo4j'
|
||||
import createServer from '../../server'
|
||||
|
||||
let server
|
||||
let query
|
||||
let mutate
|
||||
let notifiedUser
|
||||
let authenticatedUser
|
||||
let server, query, mutate, notifiedUser, authenticatedUser
|
||||
const factory = Factory()
|
||||
const driver = getDriver()
|
||||
const instance = neode()
|
||||
const neode = getNeode()
|
||||
const categoryIds = ['cat9']
|
||||
const createPostMutation = gql`
|
||||
mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
|
||||
@ -39,12 +35,13 @@ const createCommentMutation = gql`
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await factory.cleanDatabase()
|
||||
const createServerResult = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode: instance,
|
||||
neode: neode,
|
||||
driver,
|
||||
}
|
||||
},
|
||||
@ -56,14 +53,14 @@ beforeAll(() => {
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
notifiedUser = await instance.create('User', {
|
||||
notifiedUser = await neode.create('User', {
|
||||
id: 'you',
|
||||
name: 'Al Capone',
|
||||
slug: 'al-capone',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await instance.create('Category', {
|
||||
await neode.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
@ -105,6 +102,7 @@ describe('notifications', () => {
|
||||
let title
|
||||
let postContent
|
||||
let postAuthor
|
||||
|
||||
const createPostAction = async () => {
|
||||
authenticatedUser = await postAuthor.toJson()
|
||||
await mutate({
|
||||
@ -145,7 +143,7 @@ describe('notifications', () => {
|
||||
describe('commenter is not me', () => {
|
||||
beforeEach(async () => {
|
||||
commentContent = 'Commenters comment.'
|
||||
commentAuthor = await instance.create('User', {
|
||||
commentAuthor = await neode.create('User', {
|
||||
id: 'commentAuthor',
|
||||
name: 'Mrs Comment',
|
||||
slug: 'mrs-comment',
|
||||
@ -172,7 +170,6 @@ describe('notifications', () => {
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -189,7 +186,7 @@ describe('notifications', () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -213,7 +210,7 @@ describe('notifications', () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -227,7 +224,7 @@ describe('notifications', () => {
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
postAuthor = await instance.create('User', {
|
||||
postAuthor = await neode.create('User', {
|
||||
id: 'postAuthor',
|
||||
name: 'Mrs Post',
|
||||
slug: 'mrs-post',
|
||||
@ -239,6 +236,7 @@ describe('notifications', () => {
|
||||
describe('mentions me in a post', () => {
|
||||
beforeEach(async () => {
|
||||
title = 'Mentioning Al Capone'
|
||||
|
||||
postContent =
|
||||
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone">@al-capone</a> how do you do?'
|
||||
})
|
||||
@ -263,7 +261,7 @@ describe('notifications', () => {
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -369,7 +367,7 @@ describe('notifications', () => {
|
||||
expect(readAfter).toEqual(false)
|
||||
})
|
||||
|
||||
it('updates the `createdAt` attribute', async () => {
|
||||
it('does not update the `createdAt` attribute', async () => {
|
||||
await createPostAction()
|
||||
await markAsReadAction()
|
||||
const {
|
||||
@ -407,7 +405,7 @@ describe('notifications', () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -430,7 +428,7 @@ describe('notifications', () => {
|
||||
beforeEach(async () => {
|
||||
commentContent =
|
||||
'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
|
||||
commentAuthor = await instance.create('User', {
|
||||
commentAuthor = await neode.create('User', {
|
||||
id: 'commentAuthor',
|
||||
name: 'Mrs Comment',
|
||||
slug: 'mrs-comment',
|
||||
@ -439,7 +437,15 @@ describe('notifications', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('sends a notification', async () => {
|
||||
it('sends only one notification with reason mentioned_in_comment', async () => {
|
||||
postAuthor = await neode.create('User', {
|
||||
id: 'MrPostAuthor',
|
||||
name: 'Mr Author',
|
||||
slug: 'mr-author',
|
||||
email: 'post-author@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
@ -457,7 +463,41 @@ describe('notifications', () => {
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
title = "Post where I'm the author and I get mentioned in a comment"
|
||||
postContent = 'Content of post where I get mentioned in a comment.'
|
||||
postAuthor = notifiedUser
|
||||
})
|
||||
it('sends only one notification with reason commented_on_post, no notification with reason mentioned_in_comment', async () => {
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
createdAt: expect.any(String),
|
||||
reason: 'commented_on_post',
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
id: 'c47',
|
||||
content: commentContent,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -474,7 +514,7 @@ describe('notifications', () => {
|
||||
await postAuthor.relateTo(notifiedUser, 'blocked')
|
||||
commentContent =
|
||||
'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
|
||||
commentAuthor = await instance.create('User', {
|
||||
commentAuthor = await neode.create('User', {
|
||||
id: 'commentAuthor',
|
||||
name: 'Mrs Comment',
|
||||
slug: 'mrs-comment',
|
||||
@ -488,7 +528,7 @@ describe('notifications', () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import Factory from '../seed/factories'
|
||||
import { gql } from '../jest/helpers'
|
||||
import { neode as getNeode, getDriver } from '../bootstrap/neo4j'
|
||||
import { gql } from '../helpers/jest'
|
||||
import Factory from '../factories'
|
||||
import { getNeode, getDriver } from '../db/neo4j'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer from '../server'
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { rule, shield, deny, allow, and, or, not } from 'graphql-shield'
|
||||
import { neode } from '../bootstrap/neo4j'
|
||||
import { rule, shield, deny, allow, or } from 'graphql-shield'
|
||||
import { getNeode } from '../db/neo4j'
|
||||
import CONFIG from '../config'
|
||||
|
||||
const debug = !!CONFIG.DEBUG
|
||||
const allowExternalErrors = true
|
||||
|
||||
const instance = neode()
|
||||
const neode = getNeode()
|
||||
|
||||
const isAuthenticated = rule({
|
||||
cache: 'contextual',
|
||||
@ -36,67 +36,33 @@ const isMyOwn = rule({
|
||||
const isMySocialMedia = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_, args, { user }) => {
|
||||
let socialMedia = await instance.find('SocialMedia', args.id)
|
||||
let socialMedia = await neode.find('SocialMedia', args.id)
|
||||
socialMedia = await socialMedia.toJson()
|
||||
return socialMedia.ownedBy.node.id === user.id
|
||||
})
|
||||
|
||||
/* TODO: decide if we want to remove this check: the check
|
||||
* `onlyEnabledContent` throws authorization errors only if you have
|
||||
* arguments for `disabled` or `deleted` assuming these are filter
|
||||
* parameters. Soft-delete middleware obfuscates data on its way out
|
||||
* anyways. Furthermore, `neo4j-graphql-js` offers many ways to filter for
|
||||
* data so I believe, this is not a good check anyways.
|
||||
*/
|
||||
const onlyEnabledContent = rule({
|
||||
cache: 'strict',
|
||||
})(async (parent, args, ctx, info) => {
|
||||
const { disabled, deleted } = args
|
||||
return !(disabled || deleted)
|
||||
})
|
||||
|
||||
const invitationLimitReached = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (parent, args, { user, driver }) => {
|
||||
const session = driver.session()
|
||||
try {
|
||||
const result = await session.run(
|
||||
`
|
||||
MATCH (user:User {id:$id})-[:GENERATED]->(i:InvitationCode)
|
||||
RETURN COUNT(i) >= 3 as limitReached
|
||||
`,
|
||||
{ id: user.id },
|
||||
)
|
||||
const [limitReached] = result.records.map(record => {
|
||||
return record.get('limitReached')
|
||||
})
|
||||
return limitReached
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isAuthor = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!user) return false
|
||||
const session = driver.session()
|
||||
const { id: resourceId } = args
|
||||
const result = await session.run(
|
||||
`
|
||||
MATCH (resource {id: $resourceId})<-[:WROTE]-(author)
|
||||
RETURN author
|
||||
`,
|
||||
{
|
||||
resourceId,
|
||||
},
|
||||
)
|
||||
session.close()
|
||||
const [author] = result.records.map(record => {
|
||||
return record.get('author')
|
||||
const session = driver.session()
|
||||
const authorReadTxPromise = session.readTransaction(async transaction => {
|
||||
const authorTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (resource {id: $resourceId})<-[:WROTE]-(author {id: $userId})
|
||||
RETURN author
|
||||
`,
|
||||
{ resourceId, userId: user.id },
|
||||
)
|
||||
return authorTransactionResponse.records.map(record => record.get('author'))
|
||||
})
|
||||
const authorId = author && author.properties && author.properties.id
|
||||
return authorId === user.id
|
||||
try {
|
||||
const [author] = await authorReadTxPromise
|
||||
return !!author
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isDeletingOwnAccount = rule({
|
||||
@ -111,40 +77,45 @@ const noEmailFilter = rule({
|
||||
return !('email' in args)
|
||||
})
|
||||
|
||||
const publicRegistration = rule()(() => !!CONFIG.PUBLIC_REGISTRATION)
|
||||
|
||||
// Permissions
|
||||
const permissions = shield(
|
||||
export default shield(
|
||||
{
|
||||
Query: {
|
||||
'*': deny,
|
||||
findPosts: allow,
|
||||
findUsers: allow,
|
||||
findResources: allow,
|
||||
embed: allow,
|
||||
Category: allow,
|
||||
Tag: allow,
|
||||
Report: isModerator,
|
||||
reports: isModerator,
|
||||
statistics: allow,
|
||||
currentUser: allow,
|
||||
Post: or(onlyEnabledContent, isModerator),
|
||||
Post: allow,
|
||||
profilePagePosts: allow,
|
||||
Comment: allow,
|
||||
User: or(noEmailFilter, isAdmin),
|
||||
isLoggedIn: allow,
|
||||
Badge: allow,
|
||||
PostsEmotionsCountByEmotion: allow,
|
||||
PostsEmotionsByCurrentUser: isAuthenticated,
|
||||
blockedUsers: isAuthenticated,
|
||||
mutedUsers: isAuthenticated,
|
||||
notifications: isAuthenticated,
|
||||
Donations: isAuthenticated,
|
||||
},
|
||||
Mutation: {
|
||||
'*': deny,
|
||||
login: allow,
|
||||
SignupByInvitation: allow,
|
||||
Signup: isAdmin,
|
||||
Signup: or(publicRegistration, isAdmin),
|
||||
SignupVerification: allow,
|
||||
CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)),
|
||||
UpdateUser: onlyYourself,
|
||||
CreatePost: isAuthenticated,
|
||||
UpdatePost: isAuthor,
|
||||
DeletePost: isAuthor,
|
||||
report: isAuthenticated,
|
||||
fileReport: isAuthenticated,
|
||||
CreateSocialMedia: isAuthenticated,
|
||||
UpdateSocialMedia: isMySocialMedia,
|
||||
DeleteSocialMedia: isMySocialMedia,
|
||||
@ -152,13 +123,12 @@ const permissions = shield(
|
||||
// RemoveBadgeRewarded: isAdmin,
|
||||
reward: isAdmin,
|
||||
unreward: isAdmin,
|
||||
follow: isAuthenticated,
|
||||
unfollow: isAuthenticated,
|
||||
followUser: isAuthenticated,
|
||||
unfollowUser: isAuthenticated,
|
||||
shout: isAuthenticated,
|
||||
unshout: isAuthenticated,
|
||||
changePassword: isAuthenticated,
|
||||
enable: isModerator,
|
||||
disable: isModerator,
|
||||
review: isModerator,
|
||||
CreateComment: isAuthenticated,
|
||||
UpdateComment: isAuthor,
|
||||
DeleteComment: isAuthor,
|
||||
@ -167,12 +137,17 @@ const permissions = shield(
|
||||
resetPassword: allow,
|
||||
AddPostEmotions: isAuthenticated,
|
||||
RemovePostEmotions: isAuthenticated,
|
||||
block: isAuthenticated,
|
||||
unblock: isAuthenticated,
|
||||
muteUser: isAuthenticated,
|
||||
unmuteUser: isAuthenticated,
|
||||
markAsRead: isAuthenticated,
|
||||
AddEmailAddress: isAuthenticated,
|
||||
VerifyEmailAddress: isAuthenticated,
|
||||
pinPost: isAdmin,
|
||||
unpinPost: isAdmin,
|
||||
UpdateDonations: isAdmin,
|
||||
},
|
||||
User: {
|
||||
email: isMyOwn,
|
||||
email: or(isMyOwn, isAdmin),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -181,5 +156,3 @@ const permissions = shield(
|
||||
fallbackRule: allow,
|
||||
},
|
||||
)
|
||||
|
||||
export default permissions
|
||||
|
||||
@ -1,22 +1,63 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import Factory from '../seed/factories'
|
||||
import { host, login } from '../jest/helpers'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer from '../server'
|
||||
import Factory from '../factories'
|
||||
import { gql } from '../helpers/jest'
|
||||
import { getDriver, getNeode } from '../db/neo4j'
|
||||
|
||||
const factory = Factory()
|
||||
const instance = getNeode()
|
||||
const driver = getDriver()
|
||||
|
||||
let query, authenticatedUser, owner, anotherRegularUser, administrator, variables, moderator
|
||||
|
||||
const userQuery = gql`
|
||||
query($name: String) {
|
||||
User(name: $name) {
|
||||
email
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('authorization', () => {
|
||||
beforeAll(async () => {
|
||||
await factory.cleanDatabase()
|
||||
const { server } = createServer({
|
||||
context: () => ({
|
||||
driver,
|
||||
instance,
|
||||
user: authenticatedUser,
|
||||
}),
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
})
|
||||
|
||||
describe('given two existing users', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
email: 'owner@example.org',
|
||||
name: 'Owner',
|
||||
password: 'iamtheowner',
|
||||
})
|
||||
await factory.create('User', {
|
||||
email: 'someone@example.org',
|
||||
name: 'Someone else',
|
||||
password: 'else',
|
||||
})
|
||||
;[owner, anotherRegularUser, administrator, moderator] = await Promise.all([
|
||||
factory.create('User', {
|
||||
email: 'owner@example.org',
|
||||
name: 'Owner',
|
||||
password: 'iamtheowner',
|
||||
}),
|
||||
factory.create('User', {
|
||||
email: 'another.regular.user@example.org',
|
||||
name: 'Another Regular User',
|
||||
password: 'else',
|
||||
}),
|
||||
factory.create('User', {
|
||||
email: 'admin@example.org',
|
||||
name: 'Admin',
|
||||
password: 'admin',
|
||||
role: 'admin',
|
||||
}),
|
||||
factory.create('User', {
|
||||
email: 'moderator@example.org',
|
||||
name: 'Moderator',
|
||||
password: 'moderator',
|
||||
role: 'moderator',
|
||||
}),
|
||||
])
|
||||
variables = {}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
@ -24,66 +65,77 @@ describe('authorization', () => {
|
||||
})
|
||||
|
||||
describe('access email address', () => {
|
||||
let headers = {}
|
||||
let loginCredentials = null
|
||||
const action = async () => {
|
||||
if (loginCredentials) {
|
||||
headers = await login(loginCredentials)
|
||||
}
|
||||
const graphQLClient = new GraphQLClient(host, { headers })
|
||||
return graphQLClient.request('{User(name: "Owner") { email } }')
|
||||
}
|
||||
|
||||
describe('not logged in', () => {
|
||||
it('rejects', async () => {
|
||||
await expect(action()).rejects.toThrow('Not Authorised!')
|
||||
})
|
||||
|
||||
it("does not expose the owner's email address", async () => {
|
||||
let response = {}
|
||||
try {
|
||||
await action()
|
||||
} catch (error) {
|
||||
response = error.response.data
|
||||
} finally {
|
||||
expect(response).toEqual({ User: [null] })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('as owner', () => {
|
||||
describe('unauthenticated', () => {
|
||||
beforeEach(() => {
|
||||
loginCredentials = {
|
||||
email: 'owner@example.org',
|
||||
password: 'iamtheowner',
|
||||
}
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
it("exposes the owner's email address", async () => {
|
||||
await expect(action()).resolves.toEqual({ User: [{ email: 'owner@example.org' }] })
|
||||
it("throws an error and does not expose the owner's email address", async () => {
|
||||
await expect(
|
||||
query({ query: userQuery, variables: { name: 'Owner' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { User: [null] },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated as another user', () => {
|
||||
beforeEach(async () => {
|
||||
loginCredentials = {
|
||||
email: 'someone@example.org',
|
||||
password: 'else',
|
||||
}
|
||||
describe('authenticated', () => {
|
||||
describe('as the owner', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await owner.toJson()
|
||||
})
|
||||
|
||||
it("exposes the owner's email address", async () => {
|
||||
variables = { name: 'Owner' }
|
||||
await expect(query({ query: userQuery, variables })).resolves.toMatchObject({
|
||||
data: { User: [{ email: 'owner@example.org' }] },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects', async () => {
|
||||
await expect(action()).rejects.toThrow('Not Authorised!')
|
||||
describe('as another regular user', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await anotherRegularUser.toJson()
|
||||
})
|
||||
|
||||
it("throws an error and does not expose the owner's email address", async () => {
|
||||
await expect(
|
||||
query({ query: userQuery, variables: { name: 'Owner' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { User: [null] },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("does not expose the owner's email address", async () => {
|
||||
let response
|
||||
try {
|
||||
await action()
|
||||
} catch (error) {
|
||||
response = error.response.data
|
||||
}
|
||||
expect(response).toEqual({ User: [null] })
|
||||
describe('as a moderator', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
it("throws an error and does not expose the owner's email address", async () => {
|
||||
await expect(
|
||||
query({ query: userQuery, variables: { name: 'Owner' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { User: [null] },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('administrator', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await administrator.toJson()
|
||||
})
|
||||
|
||||
it("exposes the owner's email address", async () => {
|
||||
variables = { name: 'Owner' }
|
||||
await expect(query({ query: userQuery, variables })).resolves.toMatchObject({
|
||||
data: { User: [{ email: 'owner@example.org' }] },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,11 +3,20 @@ import uniqueSlug from './slugify/uniqueSlug'
|
||||
const isUniqueFor = (context, type) => {
|
||||
return async slug => {
|
||||
const session = context.driver.session()
|
||||
const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, {
|
||||
slug,
|
||||
})
|
||||
session.close()
|
||||
return response.records.length === 0
|
||||
try {
|
||||
const existingSlug = await session.readTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH(p:${type} {slug: $slug })
|
||||
RETURN p.slug
|
||||
`,
|
||||
{ slug },
|
||||
)
|
||||
})
|
||||
return existingSlug.records.length === 0
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,9 +34,5 @@ export default {
|
||||
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreateCategory: async (resolve, root, args, context, info) => {
|
||||
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Category')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import Factory from '../seed/factories'
|
||||
import { gql } from '../jest/helpers'
|
||||
import { neode as getNeode, getDriver } from '../bootstrap/neo4j'
|
||||
import Factory from '../factories'
|
||||
import { gql } from '../helpers/jest'
|
||||
import { getNeode, getDriver } from '../db/neo4j'
|
||||
import createServer from '../server'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
|
||||
@ -3,9 +3,7 @@ const isModerator = ({ user }) => {
|
||||
}
|
||||
|
||||
const setDefaultFilters = (resolve, root, args, context, info) => {
|
||||
if (typeof args.deleted !== 'boolean') {
|
||||
args.deleted = false
|
||||
}
|
||||
args.deleted = false
|
||||
|
||||
if (!isModerator(context)) {
|
||||
args.disabled = false
|
||||
@ -32,6 +30,7 @@ export default {
|
||||
Post: setDefaultFilters,
|
||||
Comment: setDefaultFilters,
|
||||
User: setDefaultFilters,
|
||||
profilePagePosts: setDefaultFilters,
|
||||
},
|
||||
Mutation: async (resolve, root, args, context, info) => {
|
||||
args.disabled = false
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import Factory from '../../seed/factories'
|
||||
import { gql } from '../../jest/helpers'
|
||||
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||
import Factory from '../../factories'
|
||||
import { gql } from '../../helpers/jest'
|
||||
import { getNeode, getDriver } from '../../db/neo4j'
|
||||
import createServer from '../../server'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
@ -8,14 +8,8 @@ const factory = Factory()
|
||||
const neode = getNeode()
|
||||
const driver = getDriver()
|
||||
|
||||
let query
|
||||
let mutate
|
||||
let graphqlQuery
|
||||
const categoryIds = ['cat9']
|
||||
let authenticatedUser
|
||||
let user
|
||||
let moderator
|
||||
let troll
|
||||
let query, graphqlQuery, authenticatedUser, user, moderator, troll
|
||||
|
||||
const action = () => {
|
||||
return query({ query: graphqlQuery })
|
||||
@ -38,18 +32,17 @@ beforeAll(async () => {
|
||||
avatar: '/some/offensive/avatar.jpg',
|
||||
about: 'This self description is very offensive',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
}),
|
||||
])
|
||||
|
||||
user = users[0]
|
||||
moderator = users[1]
|
||||
troll = users[2]
|
||||
|
||||
await neode.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
user.relateTo(troll, 'following'),
|
||||
factory.create('Post', {
|
||||
@ -70,33 +63,32 @@ beforeAll(async () => {
|
||||
}),
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
const resources = await Promise.all([
|
||||
factory.create('Comment', {
|
||||
author: user,
|
||||
id: 'c2',
|
||||
postId: 'p3',
|
||||
content: 'Enabled comment on public post',
|
||||
}),
|
||||
factory.create('Post', {
|
||||
id: 'p2',
|
||||
author: troll,
|
||||
title: 'Disabled post',
|
||||
content: 'This is an offensive post content',
|
||||
contentExcerpt: 'This is an offensive post content',
|
||||
image: '/some/offensive/image.jpg',
|
||||
deleted: false,
|
||||
categoryIds,
|
||||
}),
|
||||
factory.create('Comment', {
|
||||
id: 'c1',
|
||||
author: troll,
|
||||
postId: 'p3',
|
||||
content: 'Disabled comment',
|
||||
contentExcerpt: 'Disabled comment',
|
||||
}),
|
||||
])
|
||||
|
||||
await factory.create('Post', {
|
||||
id: 'p2',
|
||||
author: troll,
|
||||
title: 'Disabled post',
|
||||
content: 'This is an offensive post content',
|
||||
contentExcerpt: 'This is an offensive post content',
|
||||
image: '/some/offensive/image.jpg',
|
||||
deleted: false,
|
||||
categoryIds,
|
||||
})
|
||||
await factory.create('Comment', {
|
||||
id: 'c1',
|
||||
author: troll,
|
||||
postId: 'p3',
|
||||
content: 'Disabled comment',
|
||||
contentExcerpt: 'Disabled comment',
|
||||
})
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
@ -108,20 +100,57 @@ beforeAll(async () => {
|
||||
})
|
||||
const client = createTestClient(server)
|
||||
query = client.query
|
||||
mutate = client.mutate
|
||||
|
||||
authenticatedUser = await moderator.toJson()
|
||||
const disableMutation = gql`
|
||||
mutation($id: ID!) {
|
||||
disable(id: $id)
|
||||
}
|
||||
`
|
||||
await Promise.all([
|
||||
mutate({ mutation: disableMutation, variables: { id: 'c1' } }),
|
||||
mutate({ mutation: disableMutation, variables: { id: 'u2' } }),
|
||||
mutate({ mutation: disableMutation, variables: { id: 'p2' } }),
|
||||
const trollingPost = resources[1]
|
||||
const trollingComment = resources[2]
|
||||
|
||||
const reports = await Promise.all([
|
||||
factory.create('Report'),
|
||||
factory.create('Report'),
|
||||
factory.create('Report'),
|
||||
])
|
||||
const reportAgainstTroll = reports[0]
|
||||
const reportAgainstTrollingPost = reports[1]
|
||||
const reportAgainstTrollingComment = reports[2]
|
||||
|
||||
const reportVariables = {
|
||||
resourceId: 'undefined-resource',
|
||||
reasonCategory: 'discrimination_etc',
|
||||
reasonDescription: 'I am what I am !!!',
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
reportAgainstTroll.relateTo(user, 'filed', { ...reportVariables, resourceId: 'u2' }),
|
||||
reportAgainstTroll.relateTo(troll, 'belongsTo'),
|
||||
reportAgainstTrollingPost.relateTo(user, 'filed', { ...reportVariables, resourceId: 'p2' }),
|
||||
reportAgainstTrollingPost.relateTo(trollingPost, 'belongsTo'),
|
||||
reportAgainstTrollingComment.relateTo(moderator, 'filed', {
|
||||
...reportVariables,
|
||||
resourceId: 'c1',
|
||||
}),
|
||||
reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'),
|
||||
])
|
||||
|
||||
const disableVariables = {
|
||||
resourceId: 'undefined-resource',
|
||||
disable: true,
|
||||
closed: false,
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
reportAgainstTroll.relateTo(moderator, 'reviewed', { ...disableVariables, resourceId: 'u2' }),
|
||||
troll.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||
reportAgainstTrollingPost.relateTo(moderator, 'reviewed', {
|
||||
...disableVariables,
|
||||
resourceId: 'p2',
|
||||
}),
|
||||
trollingPost.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||
reportAgainstTrollingComment.relateTo(moderator, 'reviewed', {
|
||||
...disableVariables,
|
||||
resourceId: 'c1',
|
||||
}),
|
||||
trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }),
|
||||
])
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -341,76 +370,6 @@ describe('softDeleteMiddleware', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter (deleted: true)', () => {
|
||||
beforeEach(() => {
|
||||
graphqlQuery = gql`
|
||||
{
|
||||
Post(deleted: true) {
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
describe('as user', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('throws authorisation error', async () => {
|
||||
const { data, errors } = await action()
|
||||
expect(data).toEqual({ Post: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
it('does not show deleted posts', async () => {
|
||||
const expected = { data: { Post: [{ title: 'UNAVAILABLE' }] } }
|
||||
await expect(action()).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter (disabled: true)', () => {
|
||||
beforeEach(() => {
|
||||
graphqlQuery = gql`
|
||||
{
|
||||
Post(disabled: true) {
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
describe('as user', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('throws authorisation error', async () => {
|
||||
const { data, errors } = await action()
|
||||
expect(data).toEqual({ Post: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
it('shows disabled posts', async () => {
|
||||
const expected = { data: { Post: [{ title: 'Disabled post' }] } }
|
||||
await expect(action()).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
import createOrUpdateLocations from './nodes/locations'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
SignupVerification: async (resolve, root, args, context, info) => {
|
||||
const result = await resolve(root, args, context, info)
|
||||
await createOrUpdateLocations(args.id, args.locationName, context.driver)
|
||||
return result
|
||||
},
|
||||
UpdateUser: async (resolve, root, args, context, info) => {
|
||||
const result = await resolve(root, args, context, info)
|
||||
await createOrUpdateLocations(args.id, args.locationName, context.driver)
|
||||
return result
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -4,8 +4,8 @@ const COMMENT_MIN_LENGTH = 1
|
||||
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
||||
const NO_CATEGORIES_ERR_MESSAGE =
|
||||
'You cannot save a post without at least one category or more than three'
|
||||
|
||||
const validateCommentCreation = async (resolve, root, args, context, info) => {
|
||||
const USERNAME_MIN_LENGTH = 3
|
||||
const validateCreateComment = async (resolve, root, args, context, info) => {
|
||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||
const { postId } = args
|
||||
|
||||
@ -13,28 +13,31 @@ const validateCommentCreation = async (resolve, root, args, context, info) => {
|
||||
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
|
||||
}
|
||||
const session = context.driver.session()
|
||||
const postQueryRes = await session.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
RETURN post`,
|
||||
{
|
||||
postId,
|
||||
},
|
||||
)
|
||||
session.close()
|
||||
const [post] = postQueryRes.records.map(record => {
|
||||
return record.get('post')
|
||||
})
|
||||
try {
|
||||
const postQueryRes = await session.readTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
RETURN post
|
||||
`,
|
||||
{ postId },
|
||||
)
|
||||
})
|
||||
const [post] = postQueryRes.records.map(record => {
|
||||
return record.get('post')
|
||||
})
|
||||
|
||||
if (!post) {
|
||||
throw new UserInputError(NO_POST_ERR_MESSAGE)
|
||||
} else {
|
||||
return resolve(root, args, context, info)
|
||||
if (!post) {
|
||||
throw new UserInputError(NO_POST_ERR_MESSAGE)
|
||||
} else {
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const validateUpdateComment = async (resolve, root, args, context, info) => {
|
||||
const COMMENT_MIN_LENGTH = 1
|
||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||
if (!args.content || content.length < COMMENT_MIN_LENGTH) {
|
||||
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
|
||||
@ -57,11 +60,88 @@ const validateUpdatePost = async (resolve, root, args, context, info) => {
|
||||
return validatePost(resolve, root, args, context, info)
|
||||
}
|
||||
|
||||
const validateReport = async (resolve, root, args, context, info) => {
|
||||
const { resourceId } = args
|
||||
const { user } = context
|
||||
if (resourceId === user.id) throw new Error('You cannot report yourself!')
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
|
||||
const validateReview = async (resolve, root, args, context, info) => {
|
||||
const { resourceId } = args
|
||||
let existingReportedResource
|
||||
const { user, driver } = context
|
||||
if (resourceId === user.id) throw new Error('You cannot review yourself!')
|
||||
const session = driver.session()
|
||||
const reportReadTxPromise = session.readTransaction(async transaction => {
|
||||
const validateReviewTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (resource {id: $resourceId})
|
||||
WHERE resource:User OR resource:Post OR resource:Comment
|
||||
OPTIONAL MATCH (:User)-[filed:FILED]->(:Report {closed: false})-[:BELONGS_TO]->(resource)
|
||||
OPTIONAL MATCH (resource)<-[:WROTE]-(author:User)
|
||||
RETURN labels(resource)[0] AS label, author, filed
|
||||
`,
|
||||
{
|
||||
resourceId,
|
||||
submitterId: user.id,
|
||||
},
|
||||
)
|
||||
return validateReviewTransactionResponse.records.map(record => ({
|
||||
label: record.get('label'),
|
||||
author: record.get('author'),
|
||||
filed: record.get('filed'),
|
||||
}))
|
||||
})
|
||||
try {
|
||||
const txResult = await reportReadTxPromise
|
||||
existingReportedResource = txResult
|
||||
if (!existingReportedResource || !existingReportedResource.length)
|
||||
throw new Error(`Resource not found or is not a Post|Comment|User!`)
|
||||
existingReportedResource = existingReportedResource[0]
|
||||
if (!existingReportedResource.filed)
|
||||
throw new Error(
|
||||
`Before starting the review process, please report the ${existingReportedResource.label}!`,
|
||||
)
|
||||
const authorId =
|
||||
existingReportedResource.label !== 'User' && existingReportedResource.author
|
||||
? existingReportedResource.author.properties.id
|
||||
: null
|
||||
if (authorId && authorId === user.id)
|
||||
throw new Error(`You cannot review your own ${existingReportedResource.label}!`)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
|
||||
export const validateNotifyUsers = async (label, reason) => {
|
||||
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post']
|
||||
if (!reasonsAllowed.includes(reason)) throw new Error('Notification reason is not allowed!')
|
||||
if (
|
||||
(label === 'Post' && reason !== 'mentioned_in_post') ||
|
||||
(label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason))
|
||||
) {
|
||||
throw new Error('Notification does not fit the reason!')
|
||||
}
|
||||
}
|
||||
|
||||
const validateUpdateUser = async (resolve, root, params, context, info) => {
|
||||
const { name } = params
|
||||
if (typeof name === 'string' && name.trim().length < USERNAME_MIN_LENGTH)
|
||||
throw new UserInputError(`Username must be at least ${USERNAME_MIN_LENGTH} character long!`)
|
||||
return resolve(root, params, context, info)
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateComment: validateCommentCreation,
|
||||
CreateComment: validateCreateComment,
|
||||
UpdateComment: validateUpdateComment,
|
||||
CreatePost: validatePost,
|
||||
UpdatePost: validateUpdatePost,
|
||||
UpdateUser: validateUpdateUser,
|
||||
fileReport: validateReport,
|
||||
review: validateReview,
|
||||
},
|
||||
}
|
||||
|
||||
437
backend/src/middleware/validation/validationMiddleware.spec.js
Normal file
437
backend/src/middleware/validation/validationMiddleware.spec.js
Normal file
@ -0,0 +1,437 @@
|
||||
import { gql } from '../../helpers/jest'
|
||||
import Factory from '../../factories'
|
||||
import { getNeode, getDriver } from '../../db/neo4j'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer from '../../server'
|
||||
|
||||
const factory = Factory()
|
||||
const neode = getNeode()
|
||||
const driver = getDriver()
|
||||
let authenticatedUser,
|
||||
mutate,
|
||||
users,
|
||||
offensivePost,
|
||||
reportVariables,
|
||||
disableVariables,
|
||||
reportingUser,
|
||||
moderatingUser,
|
||||
commentingUser
|
||||
|
||||
const createCommentMutation = gql`
|
||||
mutation($id: ID, $postId: ID!, $content: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
const updateCommentMutation = gql`
|
||||
mutation($content: String!, $id: ID!) {
|
||||
UpdateComment(content: $content, id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
const createPostMutation = gql`
|
||||
mutation($id: ID, $title: String!, $content: String!, $language: String, $categoryIds: [ID]) {
|
||||
CreatePost(
|
||||
id: $id
|
||||
title: $title
|
||||
content: $content
|
||||
language: $language
|
||||
categoryIds: $categoryIds
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const updatePostMutation = gql`
|
||||
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
const reportMutation = gql`
|
||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||
fileReport(
|
||||
resourceId: $resourceId
|
||||
reasonCategory: $reasonCategory
|
||||
reasonDescription: $reasonDescription
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
const reviewMutation = gql`
|
||||
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
|
||||
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const updateUserMutation = gql`
|
||||
mutation($id: ID!, $name: String) {
|
||||
UpdateUser(id: $id, name: $name) {
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
beforeAll(() => {
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode,
|
||||
driver,
|
||||
}
|
||||
},
|
||||
})
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
users = await Promise.all([
|
||||
factory.create('User', {
|
||||
id: 'reporting-user',
|
||||
}),
|
||||
factory.create('User', {
|
||||
id: 'moderating-user',
|
||||
role: 'moderator',
|
||||
}),
|
||||
factory.create('User', {
|
||||
id: 'commenting-user',
|
||||
}),
|
||||
])
|
||||
reportVariables = {
|
||||
resourceId: 'whatever',
|
||||
reasonCategory: 'other',
|
||||
reasonDescription: 'Violates code of conduct !!!',
|
||||
}
|
||||
disableVariables = {
|
||||
resourceId: 'undefined-resource',
|
||||
disable: true,
|
||||
closed: false,
|
||||
}
|
||||
reportingUser = users[0]
|
||||
moderatingUser = users[1]
|
||||
commentingUser = users[2]
|
||||
const posts = await Promise.all([
|
||||
factory.create('Post', {
|
||||
id: 'offensive-post',
|
||||
authorId: 'moderating-user',
|
||||
}),
|
||||
factory.create('Post', {
|
||||
id: 'post-4-commenting',
|
||||
authorId: 'commenting-user',
|
||||
}),
|
||||
])
|
||||
offensivePost = posts[0]
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('validateCreateComment', () => {
|
||||
let createCommentVariables
|
||||
beforeEach(async () => {
|
||||
createCommentVariables = {
|
||||
postId: 'whatever',
|
||||
content: '',
|
||||
}
|
||||
authenticatedUser = await commentingUser.toJson()
|
||||
})
|
||||
|
||||
it('throws an error if content is empty', async () => {
|
||||
createCommentVariables = { ...createCommentVariables, postId: 'post-4-commenting' }
|
||||
await expect(
|
||||
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreateComment: null },
|
||||
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('sanitizes content and throws an error if not longer than 1 character', async () => {
|
||||
createCommentVariables = { postId: 'post-4-commenting', content: '<a></a>' }
|
||||
await expect(
|
||||
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreateComment: null },
|
||||
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an error if there is no post with given id in the database', async () => {
|
||||
createCommentVariables = {
|
||||
...createCommentVariables,
|
||||
postId: 'non-existent-post',
|
||||
content: 'valid content',
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreateComment: null },
|
||||
errors: [{ message: 'Comment cannot be created without a post!' }],
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateUpdateComment', () => {
|
||||
let updateCommentVariables
|
||||
beforeEach(async () => {
|
||||
await factory.create('Comment', {
|
||||
id: 'comment-id',
|
||||
authorId: 'commenting-user',
|
||||
})
|
||||
updateCommentVariables = {
|
||||
id: 'whatever',
|
||||
content: '',
|
||||
}
|
||||
authenticatedUser = await commentingUser.toJson()
|
||||
})
|
||||
|
||||
it('throws an error if content is empty', async () => {
|
||||
updateCommentVariables = { ...updateCommentVariables, id: 'comment-id' }
|
||||
await expect(
|
||||
mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdateComment: null },
|
||||
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('sanitizes content and throws an error if not longer than 1 character', async () => {
|
||||
updateCommentVariables = { id: 'comment-id', content: '<a></a>' }
|
||||
await expect(
|
||||
mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdateComment: null },
|
||||
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validatePost', () => {
|
||||
let createPostVariables
|
||||
beforeEach(async () => {
|
||||
createPostVariables = {
|
||||
title: 'I am a title',
|
||||
content: 'Some content',
|
||||
}
|
||||
authenticatedUser = await commentingUser.toJson()
|
||||
})
|
||||
|
||||
describe('categories', () => {
|
||||
describe('null', () => {
|
||||
it('throws UserInputError', async () => {
|
||||
createPostVariables = { ...createPostVariables, categoryIds: null }
|
||||
await expect(
|
||||
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreatePost: null },
|
||||
errors: [
|
||||
{
|
||||
message: 'You cannot save a post without at least one category or more than three',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty', () => {
|
||||
it('throws UserInputError', async () => {
|
||||
createPostVariables = { ...createPostVariables, categoryIds: [] }
|
||||
await expect(
|
||||
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreatePost: null },
|
||||
errors: [
|
||||
{
|
||||
message: 'You cannot save a post without at least one category or more than three',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('more than 3 categoryIds', () => {
|
||||
it('throws UserInputError', async () => {
|
||||
createPostVariables = {
|
||||
...createPostVariables,
|
||||
categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'],
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreatePost: null },
|
||||
errors: [
|
||||
{
|
||||
message: 'You cannot save a post without at least one category or more than three',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateUpdatePost', () => {
|
||||
describe('post created without categories somehow', () => {
|
||||
let owner, updatePostVariables
|
||||
beforeEach(async () => {
|
||||
const postSomehowCreated = await neode.create('Post', {
|
||||
id: 'how-was-this-created',
|
||||
})
|
||||
owner = await neode.create('User', {
|
||||
id: 'author-of-post-without-category',
|
||||
slug: 'hacker',
|
||||
})
|
||||
await postSomehowCreated.relateTo(owner, 'author')
|
||||
authenticatedUser = await owner.toJson()
|
||||
updatePostVariables = {
|
||||
id: 'how-was-this-created',
|
||||
title: 'I am a title',
|
||||
content: 'Some content',
|
||||
categoryIds: [],
|
||||
}
|
||||
})
|
||||
|
||||
it('requires at least one category for successful update', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: updatePostMutation, variables: updatePostVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdatePost: null },
|
||||
errors: [
|
||||
{ message: 'You cannot save a post without at least one category or more than three' },
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateReport', () => {
|
||||
it('throws an error if a user tries to report themself', async () => {
|
||||
authenticatedUser = await reportingUser.toJson()
|
||||
reportVariables = { ...reportVariables, resourceId: 'reporting-user' }
|
||||
await expect(
|
||||
mutate({ mutation: reportMutation, variables: reportVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { fileReport: null },
|
||||
errors: [{ message: 'You cannot report yourself!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateReview', () => {
|
||||
beforeEach(async () => {
|
||||
const reportAgainstModerator = await factory.create('Report')
|
||||
await Promise.all([
|
||||
reportAgainstModerator.relateTo(reportingUser, 'filed', {
|
||||
...reportVariables,
|
||||
resourceId: 'moderating-user',
|
||||
}),
|
||||
reportAgainstModerator.relateTo(moderatingUser, 'belongsTo'),
|
||||
])
|
||||
authenticatedUser = await moderatingUser.toJson()
|
||||
})
|
||||
|
||||
it('throws an error if a user tries to review a report against them', async () => {
|
||||
disableVariables = { ...disableVariables, resourceId: 'moderating-user' }
|
||||
await expect(
|
||||
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { review: null },
|
||||
errors: [{ message: 'You cannot review yourself!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an error for invaild resource', async () => {
|
||||
disableVariables = { ...disableVariables, resourceId: 'non-existent-resource' }
|
||||
await expect(
|
||||
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { review: null },
|
||||
errors: [{ message: 'Resource not found or is not a Post|Comment|User!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an error if no report exists', async () => {
|
||||
disableVariables = { ...disableVariables, resourceId: 'offensive-post' }
|
||||
await expect(
|
||||
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { review: null },
|
||||
errors: [{ message: 'Before starting the review process, please report the Post!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an error if a moderator tries to review their own resource(Post|Comment)', async () => {
|
||||
const reportAgainstOffensivePost = await factory.create('Report')
|
||||
await Promise.all([
|
||||
reportAgainstOffensivePost.relateTo(reportingUser, 'filed', {
|
||||
...reportVariables,
|
||||
resourceId: 'offensive-post',
|
||||
}),
|
||||
reportAgainstOffensivePost.relateTo(offensivePost, 'belongsTo'),
|
||||
])
|
||||
disableVariables = { ...disableVariables, resourceId: 'offensive-post' }
|
||||
await expect(
|
||||
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { review: null },
|
||||
errors: [{ message: 'You cannot review your own Post!' }],
|
||||
})
|
||||
})
|
||||
|
||||
describe('moderate a resource that is not a (Comment|Post|User) ', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all([factory.create('Tag', { id: 'tag-id' })])
|
||||
})
|
||||
|
||||
it('returns null', async () => {
|
||||
disableVariables = {
|
||||
...disableVariables,
|
||||
resourceId: 'tag-id',
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { review: null },
|
||||
errors: [{ message: 'Resource not found or is not a Post|Comment|User!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateUpdateUser', () => {
|
||||
let userParams, variables, updatingUser
|
||||
|
||||
beforeEach(async () => {
|
||||
userParams = {
|
||||
id: 'updating-user',
|
||||
name: 'John Doe',
|
||||
}
|
||||
|
||||
variables = {
|
||||
id: 'updating-user',
|
||||
name: 'John Doughnut',
|
||||
}
|
||||
updatingUser = await factory.create('User', userParams)
|
||||
authenticatedUser = await updatingUser.toJson()
|
||||
})
|
||||
|
||||
it('with name too short', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
name: ' ',
|
||||
}
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
||||
data: { UpdateUser: null },
|
||||
errors: [{ message: 'Username must be at least 3 character long!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -85,7 +85,7 @@ function clean(dirty) {
|
||||
return dirty
|
||||
}
|
||||
|
||||
const fields = ['content', 'contentExcerpt']
|
||||
const fields = ['content', 'contentExcerpt', 'reasonDescription']
|
||||
|
||||
export default {
|
||||
Mutation: async (resolve, root, args, context, info) => {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
id: { type: 'string', primary: true, lowercase: true },
|
||||
status: { type: 'string', valid: ['permanent', 'temporary'] },
|
||||
type: { type: 'string', valid: ['role', 'crowdfunding'] },
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
id: { type: 'string', primary: true, default: uuid },
|
||||
name: { type: 'string', required: true, default: false },
|
||||
slug: { type: 'string' },
|
||||
slug: { type: 'string', unique: 'true' },
|
||||
icon: { type: 'string', required: true, default: false },
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
updatedAt: {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
id: { type: 'string', primary: true, default: uuid },
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
updatedAt: {
|
||||
@ -25,12 +25,6 @@ module.exports = {
|
||||
target: 'User',
|
||||
direction: 'in',
|
||||
},
|
||||
disabledBy: {
|
||||
type: 'relationship',
|
||||
relationship: 'DISABLED',
|
||||
target: 'User',
|
||||
direction: 'in',
|
||||
},
|
||||
notified: {
|
||||
type: 'relationship',
|
||||
relationship: 'NOTIFIED',
|
||||
|
||||
14
backend/src/models/Donations.js
Normal file
14
backend/src/models/Donations.js
Normal file
@ -0,0 +1,14 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
export default {
|
||||
id: { type: 'string', primary: true, default: uuid },
|
||||
goal: { type: 'number' },
|
||||
progress: { type: 'number' },
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
isoDate: true,
|
||||
required: true,
|
||||
default: () => new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
email: { type: 'string', primary: true, lowercase: true, email: true },
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
verifiedAt: { type: 'string', isoDate: true },
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user