Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into dependabot/npm_and_yarn/backend/yargs-parser-18.1.3

This commit is contained in:
Wolfgang Huß 2021-07-22 13:47:13 +02:00
commit 2e9d5eae09
572 changed files with 13437 additions and 9319 deletions

View File

@ -1,25 +1,9 @@
---
name: 🐛 Bug report
about: Create a report to help us improve
name: 🐛 Bug Report
about: Create a report to help us to improve.
labels: bug
title: 🐛 [Bug]
---
## :bug: Bugreport
## :bug: Bug Report
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the bug is.-->
### Steps to reproduce the behavior
1.
2.
3.
4. ...
5. Profit
### Expected behavior
<!-- A clear and concise description of what you expected to happen. -->
### Version & Environment
<!-- Add context about your environment and used version here. -->
### Additional context
<!-- Add any other context about the problem here. -->

View File

@ -1,24 +1,9 @@
---
name: 💥 DevOps ticket
about: Help us manage our deployed App.
name: 💥 DevOps Ticket
about: Help us manage our deployed app.
labels: devops
title: 💥 [DevOps]
---
## 💥 DevOps ticket
## 💥 DevOps Ticket
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the problem is.-->
### Motive
<!-- Why does this task need to be done? What can we benefit from this? -->
### Related issues
<!-- Are there any related issues to link to? Please paste them below for reference. -->
### Implementation
<!-- Please, document any ideas of how the task can be performed. -->
### Validation
<!-- How can we make sure that this task was successful? -->
### Additional context
<!-- Add other context or background about the feature request here.-->

12
.github/ISSUE_TEMPLATE/epic.md vendored Normal file
View File

@ -0,0 +1,12 @@
---
name: 🌟 Epic
about: Define a big development step.
labels: epic
title: 🌟 [EPIC]
---
<!-- THIS ISSUE-TYPE IS NOT FOR YOU! -->
<!-- If you need an answer right away, visit the ocelot.social Discord:
https://discord.gg/AJSX9DCSUA -->
## 🌟 EPIC
<!-- Describe your Epic in detail. Include screenshots and drawings -->

View File

@ -1,24 +1,9 @@
---
name: 🚀 Feature request
about: Suggest an idea for this project
name: 🚀 Feature Request
about: Suggest an idea for this project.
labels: feature
title: 🚀 [Feature]
---
## :rocket: Feature
## :rocket: Feature Request
<!-- 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 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 other context or background about the feature request here.-->

View File

@ -1,12 +1,12 @@
---
name: 💬 Question
about: If you need help understanding HumanConnection.
about: If you need help understanding ocelot.social.
labels: question
title: 💬 [Question]
---
<!-- Chat with Team HumanConnection -->
<!-- If you need an answer right away, visit the HumanConnection Discord:
https://discord.gg/Q3mpcgr -->
<!-- Chat with ocelot.social team -->
<!-- If you need an answer right away, visit the ocelot.social Discord:
https://discord.gg/AJSX9DCSUA -->
## :speech_balloon: Question
## 💬 Question
<!-- Describe your Question in detail. Include screenshots and drawings if needed. -->

View File

@ -1,21 +1,10 @@
---
name: 🔧 Refactor ticket
name: 🔧 Refactor
about: Help us improve our code by refactoring it.
labels: refactor
title: 🔧 [Refactor]
---
## :zap: Refactor ticket
## 🔧 Refactor
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the problem is.-->
### Motive
<!-- What is the purpose of this refactoring? If it's removing depcrecated code, please link to the deprecation notice. -->
### Related issues
<!-- Are there any related issues to link to? Please paste them below for reference. -->
### Implementation
<!-- Please, document any ideas of how the code should be refactored. -->
### Additional context
<!-- Add other context or background about the feature request here.-->

179
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,179 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: cypress
versions:
- 6.3.0
- 6.4.0
- 6.5.0
- 6.6.0
- 6.7.1
- 6.8.0
- 7.0.0
- 7.0.1
- 7.1.0
- dependency-name: cypress-cucumber-preprocessor
versions:
- 4.0.0
- 4.0.1
- 4.0.3
- dependency-name: date-fns
versions:
- 2.16.1
- 2.17.0
- 2.18.0
- 2.19.0
- 2.20.0
- 2.20.1
- 2.20.2
- 2.20.3
- 2.21.0
- dependency-name: cypress-file-upload
versions:
- 5.0.2
- 5.0.3
- 5.0.4
- 5.0.5
- dependency-name: neo4j-driver
versions:
- 4.2.2
- package-ecosystem: npm
directory: "/backend"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: y18n
versions:
- 4.0.1
- 4.0.2
- dependency-name: metascraper-publisher
versions:
- 5.16.16
- 5.18.1
- 5.18.12
- 5.18.2
- 5.18.4
- 5.18.5
- 5.18.6
- 5.18.9
- 5.20.0
- 5.21.0
- 5.21.2
- 5.21.3
- 5.21.4
- 5.21.5
- dependency-name: metascraper-author
versions:
- 5.16.16
- 5.18.1
- 5.18.12
- 5.18.2
- 5.18.4
- 5.18.5
- 5.18.6
- 5.18.9
- 5.20.0
- 5.21.0
- 5.21.2
- 5.21.3
- 5.21.4
- 5.21.5
- dependency-name: neo4j-driver
versions:
- 4.2.2
- dependency-name: neo4j-graphql-js
versions:
- 2.19.1
- dependency-name: mustache
versions:
- 4.1.0
- package-ecosystem: npm
directory: "/webapp"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: nuxt
versions:
- 2.14.12
- 2.15.0
- 2.15.1
- 2.15.2
- 2.15.3
- dependency-name: v-tooltip
versions:
- 2.1.2
- dependency-name: "@vue/server-test-utils"
versions:
- 1.1.2
- 1.1.3
- dependency-name: node-notifier
versions:
- 8.0.1
- package-ecosystem: docker
directory: "/webapp"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: node
versions:
- ">= 15.5.a, < 15.6"
- dependency-name: node
versions:
- 15.10.0.pre.alpine3.10
- 15.11.0.pre.alpine3.10
- 15.12.0.pre.alpine3.10
- 15.13.0.pre.alpine3.10
- 15.7.0.pre.alpine3.10
- 15.8.0.pre.alpine3.10
- 15.9.0.pre.alpine3.10
- package-ecosystem: docker
directory: "/backend"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: node
versions:
- ">= 15.4.a, < 15.5"
- dependency-name: node
versions:
- ">= 15.5.a, < 15.6"
- dependency-name: node
versions:
- 15.10.0.pre.alpine3.10
- 15.11.0.pre.alpine3.10
- 15.12.0.pre.alpine3.10
- 15.13.0.pre.alpine3.10
- 15.7.0.pre.alpine3.10
- 15.8.0.pre.alpine3.10
- 15.9.0.pre.alpine3.10
- package-ecosystem: docker
directory: "/neo4j"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: neo4j
versions:
- 4.2.3
- 4.2.4
- package-ecosystem: docker
directory: "/deployment/legacy-migration/maintenance-worker"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10

View File

@ -4,6 +4,7 @@ on:
push:
branches:
- master
# - 4451-new-deployment-with-base-and-code # for testing while developing
jobs:
##############################################################################
@ -48,16 +49,16 @@ jobs:
- name: ENV - BUILD_DATE
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: ENV - BUILD_VERSION
run: echo "BUILD_VERSION=${VERSION}.${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
- name: ENV - BUILD_COMMIT
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
##########################################################################
# NEO4J ##################################################################
##########################################################################
- name: Neo4J | Build `community` image
run: |
docker build --target community -t "ocelotsocialnetwork/neo4j:community" -t "ocelotsocialnetwork/neo4j:${VERSION}" -t "ocelotsocialnetwork/neo4j:${BUILD_VERSION}" neo4j/
docker save "ocelotsocialnetwork/neo4j" > /tmp/neo4j.tar
run: docker build --target community -t "ocelotsocialnetwork/neo4j:latest" -t "ocelotsocialnetwork/neo4j:community" -t "ocelotsocialnetwork/neo4j:${VERSION}" -t "ocelotsocialnetwork/neo4j:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT neo4j/
- name: Neo4J | Save docker image
run: docker save "ocelotsocialnetwork/neo4j" > /tmp/neo4j.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
@ -65,7 +66,7 @@ jobs:
path: /tmp/neo4j.tar
##############################################################################
# JOB: DOCKER BUILD Production BACKEND #######################################
# JOB: DOCKER BUILD PRODUCTION BACKEND #######################################
##############################################################################
build_production_backend:
name: Docker Build Production - Backend
@ -85,16 +86,19 @@ jobs:
- name: ENV - BUILD_DATE
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: ENV - BUILD_VERSION
run: echo "BUILD_VERSION=${VERSION}.${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
- name: ENV - BUILD_COMMIT
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
##########################################################################
# BUILD BACKEND DOCKER IMAGE (production) ################################
##########################################################################
- name: backend | Build `production` image
- name: Backend | Build `production` image
run: |
docker build --target production -t "ocelotsocialnetwork/backend:latest" -t "ocelotsocialnetwork/backend:${VERSION}" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}" backend/
docker save "ocelotsocialnetwork/backend" > /tmp/backend.tar
docker build --target base -t "ocelotsocialnetwork/backend:latest-base" -t "ocelotsocialnetwork/backend:${VERSION}-base" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}-base" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/
docker build --target code -t "ocelotsocialnetwork/backend:latest-code" -t "ocelotsocialnetwork/backend:${VERSION}-code" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}-code" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/
docker build --target production -t "ocelotsocialnetwork/backend:latest" -t "ocelotsocialnetwork/backend:${VERSION}" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/
- name: Backend | Save docker image
run: docker save "ocelotsocialnetwork/backend" > /tmp/backend.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
@ -122,16 +126,19 @@ jobs:
- name: ENV - BUILD_DATE
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: ENV - BUILD_VERSION
run: echo "BUILD_VERSION=${VERSION}.${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
- name: ENV - BUILD_COMMIT
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
##########################################################################
# BUILD WEBAPP DOCKER IMAGE (build) ######################################
##########################################################################
- name: webapp | Build `production` image
- name: Webapp | Build `production` image
run: |
docker build --target production -t "ocelotsocialnetwork/webapp:latest" -t "ocelotsocialnetwork/webapp:${VERSION}" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}" webapp/
docker save "ocelotsocialnetwork/webapp" > /tmp/webapp.tar
docker build --target base -t "ocelotsocialnetwork/webapp:latest-base" -t "ocelotsocialnetwork/webapp:${VERSION}-base" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}-base" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/
docker build --target code -t "ocelotsocialnetwork/webapp:latest-code" -t "ocelotsocialnetwork/webapp:${VERSION}-code" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}-code" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/
docker build --target production -t "ocelotsocialnetwork/webapp:latest" -t "ocelotsocialnetwork/webapp:${VERSION}" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/
- name: Webapp | Save docker image
run: docker save "ocelotsocialnetwork/webapp" > /tmp/webapp.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
@ -159,17 +166,19 @@ jobs:
- name: ENV - BUILD_DATE
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: ENV - BUILD_VERSION
run: echo "BUILD_VERSION=${VERSION}.${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
- name: ENV - BUILD_COMMIT
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
##########################################################################
# BUILD MAINTENANCE DOCKER IMAGE (build) #################################
##########################################################################
- name: maintenance | Build `production` image
# TODO: --target production
- name: Maintenance | Build `production` image
run: |
docker build -t "ocelotsocialnetwork/maintenance:latest" -t "ocelotsocialnetwork/maintenance:${VERSION}" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}" webapp/ -f webapp/Dockerfile.maintenance
docker save "ocelotsocialnetwork/maintenance" > /tmp/maintenance.tar
docker build --target base -t "ocelotsocialnetwork/maintenance:latest-base" -t "ocelotsocialnetwork/maintenance:${VERSION}-base" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-base" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance
docker build --target code -t "ocelotsocialnetwork/maintenance:latest-code" -t "ocelotsocialnetwork/maintenance:${VERSION}-code" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-code" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance
docker build --target production -t "ocelotsocialnetwork/maintenance:latest" -t "ocelotsocialnetwork/maintenance:${VERSION}" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance
- name: Maintenance | Save docker image
run: docker save "ocelotsocialnetwork/maintenance" > /tmp/maintenance.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
@ -229,14 +238,13 @@ jobs:
- name: login to dockerhub
run: echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
- name: Push neo4j
# TODO: at some point --all-tags will be needed -.-
run: docker push ocelotsocialnetwork/neo4j
run: docker push --all-tags ocelotsocialnetwork/neo4j
- name: Push backend
run: docker push ocelotsocialnetwork/backend
run: docker push --all-tags ocelotsocialnetwork/backend
- name: Push webapp
run: docker push ocelotsocialnetwork/webapp
run: docker push --all-tags ocelotsocialnetwork/webapp
- name: Push maintenance
run: docker push ocelotsocialnetwork/maintenance
run: docker push --all-tags ocelotsocialnetwork/maintenance
##############################################################################
# JOB: GITHUB TAG LATEST VERSION #############################################
@ -261,7 +269,7 @@ jobs:
- name: ENV - BUILD_DATE
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: ENV - BUILD_VERSION
run: echo "BUILD_VERSION=${VERSION}.${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
- name: ENV - BUILD_COMMIT
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
##########################################################################

View File

@ -194,7 +194,6 @@ jobs:
##########################################################################
# UNIT TESTS BACKEND #####################################################
##########################################################################
# TODO: Why do we need those .envs?
- name: backend | copy env files webapp
run: cp webapp/.env.template webapp/.env
- name: backend | copy env files backend
@ -205,6 +204,17 @@ jobs:
run: docker-compose exec -T backend yarn db:migrate init
- name: backend | Unit test
run: docker-compose exec -T backend yarn test
##########################################################################
# COVERAGE CHECK BACKEND #################################################
##########################################################################
- name: backend | Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Backend
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 58
token: ${{ github.token }}
##############################################################################
# JOB: UNIT TEST WEBAPP ######################################################
@ -230,9 +240,8 @@ jobs:
- name: Load Docker Image
run: docker load < /tmp/webapp.tar
##########################################################################
# UNIT TESTS WEBAPP #####################################################
# UNIT TESTS WEBAPP ######################################################
##########################################################################
# TODO: Why do we need those .envs?
- name: backend | copy env files webapp
run: cp webapp/.env.template webapp/.env
- name: backend | copy env files backend
@ -240,5 +249,93 @@ jobs:
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp
- name: webapp | Unit tests
#run: docker run --rm ocelotsocialnetwork/webapp:build yarn run test
run: docker-compose exec -T webapp yarn test
run: docker-compose exec -T webapp yarn test
##########################################################################
# COVERAGE REPORT FRONTEND ################################################
##########################################################################
#- name: frontend | Coverage report
# uses: romeovs/lcov-reporter-action@v0.2.21
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# lcov-file: ./coverage/lcov.info
##########################################################################
# COVERAGE CHECK WEBAPP ##################################################
##########################################################################
- name: webapp | Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Webapp
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 65
token: ${{ github.token }}
##############################################################################
# JOB: FULLSTACK TESTS #######################################################
##############################################################################
fullstack_tests:
name: Fullstack tests
runs-on: ubuntu-latest
needs: [build_test_webapp, build_test_backend, build_test_neo4j]
env:
jobs: 8
strategy:
matrix:
# run copies of the current job in parallel
job: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Neo4J)
uses: actions/download-artifact@v2
with:
name: docker-neo4j-image
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/neo4j.tar
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v2
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/backend.tar
- name: Download Docker Image (Webapp)
uses: actions/download-artifact@v2
with:
name: docker-webapp-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/webapp.tar
##########################################################################
# FULLSTACK TESTS CYPRESS ################################################
##########################################################################
- name: webapp | copy env files webapp
run: cp webapp/.env.template webapp/.env
- name: backend | copy env files backend
run: cp backend/.env.template backend/.env
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
- name: cypress | Fullstack tests
run: |
yarn install
yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
##########################################################################
# UPLOAD SCREENSHOTS & VIDEO #############################################
##########################################################################
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: cypress-screenshots
path: cypress/screenshots/
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: cypress-videos
path: cypress/videos/

View File

@ -4,7 +4,173 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.6.5](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.4...v0.6.5)
#### [v1.0.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.3...v1.0.4)
- fixed wrong env variable [`#4474`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4474)
- chore: [WIP] 🍰 New Deployment With 'base' And 'code' Docker Images [`#4452`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4452)
- feat: 🍰 Flexible Footer Links [`#4468`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4468)
- docs: 🍰 Correct 'Contribution.md' [`#4466`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4466)
- docs: 🍰 Correct Discord Links And Divers [`#4461`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4461)
- Implement flexible page footer links [`1bd4af6`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/1bd4af6fd3b5db167575910948a0a72461a1129a)
- Implement tests for flexible page footer links [`627a20f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/627a20f66a65450996a5fe3128fd37769fdfd629)
- Correct Discord links and divers [`0318910`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/0318910488e245c4a1d09181265de63d05a89cf1)
#### [1.0.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.2...1.0.3)
> 19 May 2021
- chore: 🍰 Release v1.0.3 [`#4435`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4435)
- chore: 🍰 Refactor Logos [`#4433`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4433)
- feat: 🍰 Show Password Component [`#4370`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4370)
- chore: 🍰 Replace Ocelot Logos 619x593 With 600x570 [`#4428`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4428)
- Coverage [`#4393`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4393)
- correct_docker_tagging [`#4391`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4391)
- 🍰 Get Cypress Tests Running Again [`#4338`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4338)
- frontend + backend coverage tests [`#4367`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4367)
- refactor: 🍰 Refactor E-Mail Templates [`#4350`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4350)
- feat: 🍰 Remove More-Info Of Post [`#4316`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4316)
- have cypress running locally - the tests still fail [`e3e0341`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e3e03415e1bd9e0be88f33930a52e63d8af64ee1)
- have cypress running locally - the tests still fail [`0ec0574`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/0ec05743751cfef0cb86c17b87e9e4ef9a2c9e47)
- Refactor logos, first step [`ff6cc30`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ff6cc306aff6150a924f1a647387e498d050ea9f)
#### [1.0.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.1...1.0.2)
> 6 April 2021
- fix: Email Confirmation-Link When An Invite-Code Is Given [`#4336`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4336)
- release v1.0.2 [`e583010`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e5830101e4b449905fe2d0018627d75af62b2a20)
- slider jumps to enter-nonce when link contains invite-code, email and nonce and method is invite-code. Thanks @tirokk [`c80b3a2`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/c80b3a212835f76cfc2f11542345d6c3b226995b)
- fix enail confirmation link when an invite-code is given [`27f0de9`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/27f0de9464685c8f960bde9d07986fdc5b20f8f9)
#### [1.0.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.0...1.0.1)
> 4 April 2021
- readme_update [`#4331`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4331)
- jwt_expiretime [`#4330`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4330)
- Clean env, docker & workflow [`#4337`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4337)
- fix: 🍰 Fixing The Avatars unwanted Border [`#4320`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4320)
- Change background color of avatar image to white [`e48a99a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e48a99afe63e96cffafe16db7bf5ae35cfdebd7a)
- removed config warning [`f6c070a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/f6c070a3cb36c5593123b27b4d1b6a5b7a10aba3)
- include env files in build process [`dfe6f67`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/dfe6f679b3bb0f440aee986d6d12925cccee9050)
### [1.0.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.15...1.0.0)
> 31 March 2021
- v1.0.0 [`#4321`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4321)
- fix: 🐛 Fix Wrong Truncation Of Hashtags In Admin List Via Adding 'truncateStr' [`#4314`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4314)
- feat: 🍰Implement Registration Slider [`#4270`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4270)
- feat: 🍰 Invite Button [`#4301`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4301)
- fix: 🍰 Suggestion List Filter [`#4296`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4296)
- fix: JWT Expires In 2 Years [`#4278`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4278)
- Deployment [`#4263`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4263)
- feat: 🍰 Redesign Registration Process Frontend [`#4168`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4168)
- Delete unnecessary code [`3fa7e04`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3fa7e04d4895161db1f764ccad58e35188c9d065)
- basic invite button in frontend [`356f026`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/356f02622655a57561fcdecfcc8a735d23cac79f)
- setting up invite button [`e6dc3f4`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e6dc3f42cedaf9953d737cf30cf7ed317b634be7)
#### [0.6.15](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.14...0.6.15)
> 1 March 2021
- feat: Count Post Teaser Views [`#4255`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4255)
- feat: 🍰 Count Post Clicks [`#4248`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4248)
- Correct version style [`#4218`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4218)
- dashboard restructuring image [`#4266`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4266)
- fix: 🐛 Adapted Editor List Styles [`#4239`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4239)
- fix: Scrolling On Profile Page [`#4234`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4234)
- release 0.6.15 [`ef4265d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ef4265d9387d94aa09db2b80461c2ec90b4623ae)
- count views of post teaser [`1c3f628`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/1c3f628fb2e161400319b32da274952c1b57836e)
- tests fixed for clickedCount [`96066ea`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/96066eae02e659a00bce280f9f97a28ac1446ce0)
#### [0.6.14](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.13...0.6.14)
> 17 February 2021
- fix: Add Null Migration [`#4233`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4233)
- release 0.6.14 [`394860c`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/394860c1d0d6ff23f6653b81288890a67720deab)
- add null mutation [`a7489a0`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a7489a044c4b4b556f6b26d555657478ebd6409b)
- fixed misspelling in changelog [`3ad6d73`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3ad6d738fd5d9ed25c0cccc6ae60ed5cc37540ba)
#### [0.6.13](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.12...0.6.13)
> 17 February 2021
- fix: css so follow button isn't blocked by adblock [`#4230`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4230)
- fix: Query Available Roles As Admin [`#4225`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4225)
- release 0.6.13 [`e2503d4`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e2503d496606aa79b5ab59319f76466e04a79bd0)
- avoid introspection to get available roles as admin [`a5df793`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a5df793c55ec8792066e61547ae287f7702675fd)
- css fix to trick adblock on follow button by @nila99 [`13931c9`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/13931c90e7642da93de0e884afd1649de8181c6c)
#### [0.6.12](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.11...0.6.12)
> 15 February 2021
- Smtp secure option [`#4223`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4223)
- chore: 🐛 Fix Migrations By Migrations Folder Having .gitkeep [`#4222`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4222)
- SMTP_SECURE option [`8e2d8a7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8e2d8a77c3a825880deb4c668d647ee6503edc7f)
- New file .gitkeep in migrations [`814c1b8`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/814c1b88aed2e6e353f2af2f277229629cc2e788)
#### [0.6.11](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.10...0.6.11)
> 15 February 2021
- chore: 🍰 Resolve WEBSOCKETS_URI .env Problem [`#4219`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4219)
- chore: 🐛 'db:migrate up' By Moving Examples Outside Of Migrations Folder [`#4221`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4221)
- fix: Location Tests In Backend [`#4220`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4220)
- MAPBOX introduced district for US-Cities. So I used a German City instead [`7a31334`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/7a313344cbddaca7bcb073b01a549bfe2a5c3852)
- Resolve WEBSOCKETS_URI config problem [`ac27d6a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ac27d6ac9653046d98e559fe1532648d7ddfa92e)
- Fix 'db:migrate up' by moving examples outside of migrations folder [`063c730`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/063c730e5fb3e3ea363deb5adebdbf69589ec7b4)
#### [0.6.10](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.9...0.6.10)
> 12 February 2021
- Use original images [`#4217`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4217)
- original images instead of broken whitelabled ones [`2303b92`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2303b92146d71d8ec12b6fa75a4a0c513db96594)
#### [0.6.9](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.8...0.6.9)
> 12 February 2021
- fix_webapp_production [`#4216`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4216)
- Move Old Migrations From HC To Examples Folder To Avoid Conflicts [`#4215`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4215)
- typo [`7d21196`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/7d21196973282ce904db8776fbef8e0961dac6e5)
- fixed prodution stage of webapp container [`02d31d0`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/02d31d0a94aa73a5026d2d9c183c63d1a7e43b6c)
- moved old migrations from HC to examples folder to avoid conflicts with new migrations [`efec46d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/efec46d9d485ec439a25adcac6da105683a221ad)
#### [0.6.8](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.7...0.6.8)
> 11 February 2021
- refactor: 🍰 Remove Emojis From Post Page [`#4208`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4208)
- release 0.6.8 [`b947918`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/b94791873bb3c9524daff78fc9d0f7bdb385127d)
- Refinied design [`58728df`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/58728df97b3a21d34e9476f2be139add33c75b42)
- remove emojis from post page [`4097cc1`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/4097cc16f723dfd2af64c4adcfef978b1c81e07b)
#### [0.6.7](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.6...0.6.7)
> 10 February 2021
- Support newest docker [`#4210`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4210)
- push all tags again, since docker is updated on github (...) [`c491fd6`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/c491fd692155b16822426372c58b5770daf2c0aa)
#### [0.6.6](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.5...0.6.6)
> 10 February 2021
- - add latest tag for neo4j [`#4209`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4209)
- feat: 🍰 Switch User Role As Admin [`#4136`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4136)
- feat: Image Cropping Is Optional [`#4199`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4199)
- publish workflow [`#4195`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4195)
- change user roles is working, test fails [`c528269`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/c528269cb2972e6ea937d31ba22d0e11168141f2)
- file upload: refactored [`650e83f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/650e83f4c250389477933a2e7d21d8245b0ce882)
- change user role: tests are working [`14dfe2a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/14dfe2ae2cd4a24c06c9229893b33586dfceae4f)
#### [0.6.5](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.4...0.6.5)
> 8 February 2021
- updated CHANGELOG.md [`9d9075f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d9075f2117b2eb4b607e7d59ab18c7e655c6ea7)

View File

@ -4,31 +4,33 @@ Thank you so much for thinking of contributing to the Human Connection project!
## Getting Set Up
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/).
Instructions for how to install all the necessary software and some code guidelines can be found in our main [Readme](/README.md) or in our [documentation](https://docs.human-connection.org/human-connection/).
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/Ocelot-Social-Community/Ocelot-Social/issues) or [Zenhub](https://app.zenhub.com/workspaces/ocelotsocial-5fb21ff922cb410015dd6535/board?filterLogic=any&repos=301151089):
To get you started we recommend that you join forces with a regular contributor. Please join [our Discord instance](https://discord.gg/AJSX9DCSUA) to chat with developers or just get in touch directly on an issue on either [Github](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues) or [Zenhub](https://app.zenhub.com/workspaces/ocelotsocial-5fb21ff922cb410015dd6535/board?filterLogic=any&repos=301151089):
![](https://dl.dropbox.com/s/vbmcihkduy9dhko/Screenshot%202019-01-03%2015.50.11.png?dl=0)
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.
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 Flow
We operate in two week sprints that are planned, estimated and prioritised on [Zenhub](https://app.zenhub.com/workspaces/ocelotsocial-5fb21ff922cb410015dd6535/board?filterLogic=any&repos=301151089). All issues are also linked to and synced with [Github](https://github.com/Ocelot-Social-Community/Ocelot-Social/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.
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).
4. We assign ourselves to the issue and move it to `In Progress` on [Zenhub](https://app.zenhub.com/workspaces/ocelotsocial-5fb21ff922cb410015dd6535/board?filterLogic=any&repos=301151089).
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)
@ -38,37 +40,46 @@ Every pull request needs to:
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)
You can talk to our core team on [Discord](https://discord.gg/AJSX9DCSUA). And on Github you will mostly run into our core developers:
* Ulf (@ulfgebhardt)
* Moriz (@Mogge)
* Wolle (@Tirokk)
* Alex (@ogerly)
<!-- * Robert (@roschaefer)
* Matt (@mattwr18)
* Alina (@alina-beck)
* Martin (@datenbrei), our head of IT
* and sometimes Dennis (@DennisHack), the founder of Human Connection
* 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 MondayFriday 11:30
* in the discord `Conference Room`
* every MondayThursday 11:30 am (german time see above 👆🏼)
* in our [Discord](https://discord.gg/AJSX9DCSUA) `Office Cube`
* 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)
* the link will be posted in the [Discord chat](https://discord.gg/AJSX9DCSUA) 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
* bi-weekly on Mondays 13:00 (when there is no sprint retrospective)
* 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)
* the link will be posted in the [Discord chat](https://discord.gg/AJSX9DCSUA) 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)
@ -76,6 +87,7 @@ Meet the team
* 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)
@ -87,6 +99,7 @@ Sprint retrospective
* 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
@ -102,10 +115,9 @@ We use pair programming sessions as a tool for knowledge sharing. We can learn a
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!
## Open-Source Bounties
There are so many good reasons to contribute to Human Connection
There are so many good reasons to contribute to ocelot.social
* You learn state-of-the-art technologies
* You build your portfolio
@ -121,7 +133,7 @@ pull request approved and merged for free**. You can choose something really
quick and easy. What's important is starting a working relationship with the
team, learning the workflow, and understanding this contribution guide. You can
filter issues by 'good first issue', to get an idea where to start. Please join
our our [community chat](https://human-connection.org/discord), too.
our our [Discord community chat](https://discord.gg/AJSX9DCSUA), too.
You can filter Github issues with label [bounty](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues?q=is%3Aopen+is%3Aissue+label%3Abounty). These issues should have a second label `€<amount>`
which indicate their respective financial compensation in Euros.

View File

@ -12,11 +12,13 @@ ocelot.social is a nonprofit social, action and knowledge network that connects
* **Knowledge**: Read articles about interesting topics and find related posts in the **More Info** tab or by **Filtering** based on **Categories** and **Tagging** or by using the **Fulltext Search**.
* **Action**: Don't just read about how to make the world a better place, but come into **Action** by following provided suggestions on the **Action** tab provided by other people or **Organisations**.
[![Ocelot-Social](webapp/static/img/custom/welcome.svg)](https://ocelot.social)
<p align="center">
<img src="webapp/static/img/custom/logo-squared.svg" alt="ocelot.social" width="40%" height="40%">
</p>
## Live demo
Try out our deployed [development environment](https://develop.human-connection.org/).
__Try out our deployed [development environment](https://develop.human-connection.org/).__
Logins:
@ -29,9 +31,9 @@ Logins:
## Directory Layout
There are four important directories:
* [Backend](./backend) runs on the server and is a middleware between database and frontend
* [Frontend](./webapp) is a server-side-rendered and client-side-rendered web frontend
* [Deployment](./deployment) configuration for kubernetes
* [Cypress](./cypress) contains end-to-end tests and executable feature specifications
In order to setup the application and start to develop features you have to
@ -45,16 +47,19 @@ There are two approaches:
## Installation
### Clone the Repository
Clone the repository, this will create a new folder called `Ocelot-Social`:
Using HTTPS:
```bash
$ git clone https://github.com/Ocelot-Social-Community/Ocelot-Social.git
```
Using SSH:
```bash
$ git clone git@github.com:Human-Connection/Human-Connection.git
$ git clone git@github.com:Ocelot-Social-Community/Ocelot-Social.git
```
Change into the new folder.
@ -87,25 +92,33 @@ docker-compose version 1.23.2
#### Start Ocelot-Social via Docker-Compose
For Development:
```bash
docker-compose up
$ docker-compose up
```
For Production
For Production:
```bash
docker-compose -f docker-compose.yml up
$ docker-compose -f docker-compose.yml up
```
This will start all required Docker containers
## Deployment
Deployment methods can be found in the [Ocelot-Social-Deploy-Rebranding](https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding) repository.
The only deployment method in this repository is `docker-compose` for development purposes as described above.
## Developer Chat
Join our friendly open-source community on [Discord](https://discordapp.com/invite/DFSjPaX) :heart_eyes_cat:
Join our friendly open-source community on [Discord](https://discord.gg/AJSX9DCSUA) :heart_eyes_cat:
Just introduce yourself at `#introduce-yourself` and mention `@@Mentor` to get you onboard :neckbeard:
Check out the [contribution guideline](./CONTRIBUTING.md), too!
We give write permissions to every developer who asks for it. Just text us on
[Discord](https://discord.gg/6ub73U3).
[Discord](https://discord.gg/AJSX9DCSUA).
## Technology Stack
@ -124,4 +137,5 @@ 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).

View File

@ -15,24 +15,8 @@
* [End-to-end tests](cypress/README.md)
* [Frontend tests](webapp/testing.md)
* [Backend tests](backend/testing.md)
* [Deployment](https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/blob/master/deployment/README.md)
* [Contributing](CONTRIBUTING.md)
* [Kubernetes Deployment](deployment/README.md)
* [Minikube](deployment/minikube/README.md)
* [Digital Ocean](deployment/digital-ocean/README.md)
* [Kubernetes Dashboard](deployment/digital-ocean/dashboard/README.md)
* [HTTPS](deployment/digital-ocean/https/README.md)
* [ocelot.social](deployment/ocelot-social/README.md)
* [Error Reporting](deployment/ocelot-social/error-reporting/README.md)
* [Mailserver](deployment/ocelot-social/mailserver/README.md)
* [Maintenance](deployment/ocelot-social/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)
* [License](LICENSE.md)

View File

@ -10,6 +10,7 @@ SMTP_USERNAME=
SMTP_PASSWORD=
JWT_SECRET="b/&&7b78BF&fv/Vd"
JWT_EXPIRES="2y"
MAPBOX_TOKEN="pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g"
PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78"
@ -17,6 +18,7 @@ PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78"
SENTRY_DSN_BACKEND=
COMMIT=
PUBLIC_REGISTRATION=false
INVITE_REGISTRATION=true
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=

View File

@ -1,17 +1,20 @@
##################################################################################
# BASE ###########################################################################
# BASE (Is pushed to DockerHub for rebranding) ###################################
##################################################################################
FROM node:12.19.0-alpine3.10 as base
# ENVs (available in production aswell, can be overwritten by commandline or env file)
# ENVs
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
ENV DOCKER_WORKDIR="/app"
## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
## We cannot do $(yarn run version).${BUILD_NUMBER} here so we default to 0.0.0.0
ENV BUILD_VERSION="0.0.0.0"
ARG BBUILD_DATE="1970-01-01T00:00:00.00Z"
ENV BUILD_DATE=$BBUILD_DATE
## We cannot do $(yarn run version)-${BUILD_NUMBER} here so we default to 0.0.0-0
ARG BBUILD_VERSION="0.0.0-0"
ENV BUILD_VERSION=$BBUILD_VERSION
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
ENV BUILD_COMMIT="0000000"
ARG BBUILD_COMMIT="0000000"
ENV BUILD_COMMIT=$BBUILD_COMMIT
## SET NODE_ENV
ENV NODE_ENV="production"
## App relevant Envs
@ -56,12 +59,18 @@ FROM base as development
CMD /bin/sh -c "yarn install && yarn run dev"
##################################################################################
# BUILD (Does contain all files and is therefore bloated) ########################
# CODE (Does contain all code files and is pushed to DockerHub for rebranding) ###
##################################################################################
FROM base as build
FROM base as code
# Copy everything
# copy everything, but do not build.
COPY . .
##################################################################################
# BUILD (Does contain all files and the compilate and is therefore bloated) ######
##################################################################################
FROM code as build
# yarn install
RUN yarn install --production=false --frozen-lockfile --non-interactive
# yarn build
@ -82,7 +91,6 @@ FROM base as production
# Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/dist ./dist
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
# Copy static files
# TODO - externalize the uploads so we can copy the whole folder
COPY --from=build ${DOCKER_WORKDIR}/public/img/ ./public/img/
@ -91,4 +99,4 @@ COPY --from=build ${DOCKER_WORKDIR}/public/providers.json ./public/providers.jso
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
# Run command
CMD /bin/sh -c "yarn run start"
CMD /bin/sh -c "yarn run start"

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-backend",
"version": "0.6.5",
"version": "1.0.4",
"description": "GraphQL Backend for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
@ -15,7 +15,7 @@
"dev": "nodemon --exec babel-node src/ -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",
"test": "cross-env NODE_ENV=test jest --forceExit --detectOpenHandles --runInBand",
"test": "cross-env NODE_ENV=test jest --forceExit --detectOpenHandles --runInBand --coverage",
"db:clean": "babel-node src/db/clean.js",
"db:reset": "yarn run db:clean",
"db:seed": "babel-node src/db/seed.js",
@ -39,6 +39,12 @@
]
},
"dependencies": {
"@babel/cli": "~7.8.4",
"@babel/core": "~7.9.0",
"@babel/node": "~7.8.7",
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
"@babel/preset-env": "~7.9.5",
"@babel/register": "^7.9.0",
"@hapi/joi": "^17.1.1",
"@sentry/node": "^5.15.4",
"apollo-cache-inmemory": "~1.6.5",
@ -48,12 +54,15 @@
"apollo-server": "~2.14.2",
"apollo-server-express": "^2.14.2",
"aws-sdk": "^2.652.0",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.1.0",
"babel-jest": "~25.2.6",
"babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~7.0.2",
"date-fns": "2.11.1",
"date-fns": "2.22.1",
"debug": "~4.1.1",
"dotenv": "~8.2.0",
"express": "^4.17.1",
@ -72,7 +81,7 @@
"languagedetect": "^2.0.0",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.7",
"merge-graphql-schemas": "^1.7.8",
"metascraper": "^5.11.8",
"metascraper-audio": "^5.14.26",
"metascraper-author": "^5.14.22",
@ -92,7 +101,7 @@
"migrate": "^1.7.0",
"mime-types": "^2.1.26",
"minimatch": "^3.0.4",
"mustache": "^4.0.1",
"mustache": "^4.2.0",
"neo4j-driver": "^4.0.2",
"neo4j-graphql-js": "^2.11.5",
"neode": "^0.3.7",
@ -111,16 +120,7 @@
"xregexp": "^4.3.0"
},
"devDependencies": {
"@babel/cli": "~7.8.4",
"@babel/core": "~7.9.0",
"@babel/node": "~7.8.7",
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
"@babel/preset-env": "~7.9.5",
"@babel/register": "^7.9.0",
"apollo-server-testing": "~2.11.0",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.1.0",
"babel-jest": "~25.2.6",
"chai": "~4.2.0",
"cucumber": "~6.0.5",
"eslint": "~6.8.0",
@ -134,7 +134,7 @@
"eslint-plugin-standard": "~4.0.1",
"jest": "~25.3.0",
"nodemon": "~2.0.2",
"prettier": "~2.2.0",
"prettier": "~2.3.2",
"rosie": "^2.0.1",
"supertest": "~4.0.2"
},

View File

@ -7,9 +7,8 @@ if (require.resolve) {
try {
dotenv.config({ path: require.resolve('../../.env') })
} catch (error) {
if (error.code === 'MODULE_NOT_FOUND') {
console.log('WARN: No `.env` file found in `/app` (docker) or `/backend` (no docker)') // eslint-disable-line no-console
} else {
// This error is thrown when the .env is not found
if (error.code !== 'MODULE_NOT_FOUND') {
throw error
}
}
@ -35,12 +34,14 @@ const required = {
const server = {
CLIENT_URI: env.CLIENT_URI || 'http://localhost:3000',
GRAPHQL_URI: env.GRAPHQL_URI || 'http://localhost:4000',
JWT_EXPIRES: env.JWT_EXPIRES || '2y',
}
const smtp = {
SMTP_HOST: env.SMTP_HOST,
SMTP_PORT: env.SMTP_PORT,
SMTP_IGNORE_TLS: env.SMTP_IGNORE_TLS || true,
SMTP_IGNORE_TLS: env.SMTP_IGNORE_TLS !== 'false', // default = true
SMTP_SECURE: env.SMTP_SECURE === 'true',
SMTP_USERNAME: env.SMTP_USERNAME,
SMTP_PASSWORD: env.SMTP_PASSWORD,
}
@ -81,7 +82,8 @@ const options = {
SUPPORT_URL: links.SUPPORT,
APPLICATION_NAME: metadata.APPLICATION_NAME,
ORGANIZATION_URL: links.ORGANIZATION,
PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true',
PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true' || false,
INVITE_REGISTRATION: env.INVITE_REGISTRATION !== 'false', // default = true
}
// Check if all required configs are present

View File

@ -1,6 +1,13 @@
// this file is duplicated in `backend/src/config/links.js` and `webapp/constants/links.js` and replaced on rebranding
export default {
ORGANIZATION: 'https://ocelot.social',
DONATE: 'https://ocelot-social.herokuapp.com/donations',
FAQ: 'https://ocelot.social',
SUPPORT: 'https://ocelot.social',
// on null or empty strings internal imprint is used, see 'webapp/locales/html/'
DONATE: 'https://ocelot-social.herokuapp.com/donations', // we use 'ocelot-social.herokuapp.com' at the moment, because redirections of 'ocelot.social' subpages are not working correctly
IMPRINT: 'https://ocelot-social.herokuapp.com/imprint', // we use 'ocelot-social.herokuapp.com' at the moment, because redirections of 'ocelot.social' subpages are not working correctly
TERMS_AND_CONDITIONS: null,
CODE_OF_CONDUCT: null,
DATA_PRIVACY: null,
FAQ: 'https://ocelot.social',
}

View File

@ -0,0 +1,10 @@
// this file is duplicated in `backend/src/config/logos.js` and `webapp/constants/logos.js` and replaced on rebranding
// this are the paths in the webapp
export default {
LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg',
LOGO_SIGNUP_PATH: '/img/custom/logo-squared.svg',
LOGO_WELCOME_PATH: '/img/custom/logo-squared.svg',
LOGO_LOGOUT_PATH: '/img/custom/logo-squared.svg',
LOGO_PASSWORD_RESET_PATH: '/img/custom/logo-squared.svg',
LOGO_MAINTENACE_RESET_PATH: '/img/custom/logo-squared.svg',
}

View File

@ -1,7 +1,9 @@
// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js` and replaced on rebranding
export default {
APPLICATION_NAME: 'ocelot.social',
APPLICATION_SHORT_NAME: 'ocelot',
APPLICATION_DESCRIPTION: 'ocelot.social Community Network',
COOKIE_NAME: 'ocelot-social-token',
ORGANIZATION_NAME: 'ocelot.social Community',
ORGANIZATION_JURISDICTION: 'City of Angels',
}

View File

@ -49,8 +49,9 @@ Factory.define('badge')
Factory.define('image')
.attr('url', faker.image.unsplash.imageUrl)
.attr('aspectRatio', 1)
.attr('aspectRatio', 1.3333333333333333)
.attr('alt', faker.lorem.sentence)
.attr('type', 'image/jpeg')
.after((buildObject, options) => {
const { url: imageUrl } = buildObject
if (imageUrl) buildObject.url = uniqueImageUrl(imageUrl)
@ -104,12 +105,12 @@ Factory.define('user')
})
Factory.define('post')
.option('categoryIds', [])
/* .option('categoryIds', [])
.option('categories', ['categoryIds'], (categoryIds) => {
if (categoryIds.length) return Promise.all(categoryIds.map((id) => neode.find('Category', id)))
// there must be at least one category
return Promise.all([Factory.build('category')])
})
}) */
.option('tagIds', [])
.option('tags', ['tagIds'], (tagIds) => {
return Promise.all(tagIds.map((id) => neode.find('Tag', id)))
@ -129,6 +130,8 @@ Factory.define('post')
deleted: false,
imageBlurred: false,
imageAspectRatio: 1.333,
clickedCount: 0,
viewedTeaserCount: 0,
})
.attr('pinned', ['pinned'], (pinned) => {
// Convert false to null
@ -144,16 +147,16 @@ Factory.define('post')
return language || 'en'
})
.after(async (buildObject, options) => {
const [post, author, image, categories, tags] = await Promise.all([
const [post, author, image, /* categories, */ tags] = await Promise.all([
neode.create('Post', buildObject),
options.author,
options.image,
options.categories,
// options.categories,
options.tags,
])
await Promise.all([
post.relateTo(author, 'author'),
Promise.all(categories.map((c) => c.relateTo(post, 'post'))),
// Promise.all(categories.map((c) => c.relateTo(post, 'post'))),
Promise.all(tags.map((t) => t.relateTo(post, 'post'))),
])
if (image) await post.relateTo(image, 'image')

View File

@ -0,0 +1,9 @@
'use strict'
module.exports.up = function (next) {
next()
}
module.exports.down = function (next) {
next()
}

View File

@ -0,0 +1,53 @@
import { getDriver } from '../../db/neo4j'
export const description = `
This migration adds the clickedCount property to all posts, setting it to 0.
`
module.exports.up = async function (next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(`
MATCH (p:Post)
SET p.clickedCount = 0
`)
await transaction.commit()
next()
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
throw new Error(error)
} finally {
session.close()
}
}
module.exports.down = async function (next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(`
MATCH (p:Post)
REMOVE p.clickedCount
`)
await transaction.commit()
next()
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
throw new Error(error)
} finally {
session.close()
}
}

View File

@ -0,0 +1,53 @@
import { getDriver } from '../../db/neo4j'
export const description = `
This migration adds the viewedTeaserCount property to all posts, setting it to 0.
`
module.exports.up = async function (next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(`
MATCH (p:Post)
SET p.viewedTeaserCount = 0
`)
await transaction.commit()
next()
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
throw new Error(error)
} finally {
session.close()
}
}
module.exports.down = async function (next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(`
MATCH (p:Post)
REMOVE p.viewedTeaserCount
`)
await transaction.commit()
next()
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
throw new Error(error)
} finally {
session.close()
}
}

View File

@ -137,100 +137,93 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
}),
])
const [
peterLustig,
bobDerBaumeister,
jennyRostock,
huey,
dewey,
louie,
dagobert,
] = await Promise.all([
Factory.build(
'user',
{
id: 'u1',
name: 'Peter Lustig',
slug: 'peter-lustig',
role: 'admin',
},
{
email: 'admin@example.org',
},
),
Factory.build(
'user',
{
id: 'u2',
name: 'Bob der Baumeister',
slug: 'bob-der-baumeister',
role: 'moderator',
},
{
email: 'moderator@example.org',
},
),
Factory.build(
'user',
{
id: 'u3',
name: 'Jenny Rostock',
slug: 'jenny-rostock',
role: 'user',
},
{
email: 'user@example.org',
},
),
Factory.build(
'user',
{
id: 'u4',
name: 'Huey',
slug: 'huey',
role: 'user',
},
{
email: 'huey@example.org',
},
),
Factory.build(
'user',
{
id: 'u5',
name: 'Dewey',
slug: 'dewey',
role: 'user',
},
{
email: 'dewey@example.org',
},
),
Factory.build(
'user',
{
id: 'u6',
name: 'Louie',
slug: 'louie',
role: 'user',
},
{
email: 'louie@example.org',
},
),
Factory.build(
'user',
{
id: 'u7',
name: 'Dagobert',
slug: 'dagobert',
role: 'user',
},
{
email: 'dagobert@example.org',
},
),
])
const [peterLustig, bobDerBaumeister, jennyRostock, huey, dewey, louie, dagobert] =
await Promise.all([
Factory.build(
'user',
{
id: 'u1',
name: 'Peter Lustig',
slug: 'peter-lustig',
role: 'admin',
},
{
email: 'admin@example.org',
},
),
Factory.build(
'user',
{
id: 'u2',
name: 'Bob der Baumeister',
slug: 'bob-der-baumeister',
role: 'moderator',
},
{
email: 'moderator@example.org',
},
),
Factory.build(
'user',
{
id: 'u3',
name: 'Jenny Rostock',
slug: 'jenny-rostock',
role: 'user',
},
{
email: 'user@example.org',
},
),
Factory.build(
'user',
{
id: 'u4',
name: 'Huey',
slug: 'huey',
role: 'user',
},
{
email: 'huey@example.org',
},
),
Factory.build(
'user',
{
id: 'u5',
name: 'Dewey',
slug: 'dewey',
role: 'user',
},
{
email: 'dewey@example.org',
},
),
Factory.build(
'user',
{
id: 'u6',
name: 'Louie',
slug: 'louie',
role: 'user',
},
{
email: 'louie@example.org',
},
),
Factory.build(
'user',
{
id: 'u7',
name: 'Dagobert',
slug: 'dagobert',
role: 'user',
},
{
email: 'dagobert@example.org',
},
),
])
await Promise.all([
peterLustig.relateTo(Berlin, 'isIn'),
@ -561,7 +554,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
const hashtagAndMention1 =
'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]) {
mutation ($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
id
}
@ -618,7 +611,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
const mentionInComment2 =
'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!) {
mutation ($id: ID, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
}

View File

@ -5,7 +5,7 @@ import CONFIG from './../config'
export default function encode(user) {
const { id, name, slug } = user
const token = jwt.sign({ id, name, slug }, CONFIG.JWT_SECRET, {
expiresIn: '1d',
expiresIn: CONFIG.JWT_EXPIRES,
issuer: CONFIG.GRAPHQL_URI,
audience: CONFIG.CLIENT_URI,
subject: user.id.toString(),

View File

@ -22,8 +22,8 @@ if (!hasEmailConfig) {
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
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
secure: CONFIG.SMTP_SECURE, // true for 465, false for other ports
auth: hasAuthData && {
user: CONFIG.SMTP_USERNAME,
pass: CONFIG.SMTP_PASSWORD,
@ -43,9 +43,14 @@ if (!hasEmailConfig) {
}
const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
const { inviteCode } = args
const response = await resolve(root, args, context, resolveInfo)
const { email, nonce } = response
await sendMail(signupTemplate({ email, nonce }))
if (inviteCode) {
await sendMail(signupTemplate({ email, nonce, inviteCode }))
} else {
await sendMail(signupTemplate({ email, nonce }))
}
delete response.nonce
return response
}
@ -71,6 +76,5 @@ export default {
AddEmailAddress: sendEmailVerificationMail,
requestPasswordReset: sendPasswordResetMail,
Signup: sendSignupMail,
SignupByInvitation: sendSignupMail,
},
}

View File

@ -1,10 +1,11 @@
import mustache from 'mustache'
import CONFIG from '../../config'
import logosWebapp from '../../config/logos.js'
import * as templates from './templates'
const from = CONFIG.EMAIL_DEFAULT_SENDER
const welcomeImageUrl = new URL(`/img/custom/welcome.svg`, CONFIG.CLIENT_URI)
const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI)
const defaultParams = {
supportUrl: CONFIG.SUPPORT_URL,
@ -13,11 +14,18 @@ const defaultParams = {
welcomeImageUrl,
}
export const signupTemplate = ({ email, nonce }) => {
export const signupTemplate = ({ email, nonce, inviteCode = null }) => {
const subject = `Willkommen, Bienvenue, Welcome to ${CONFIG.APPLICATION_NAME}!`
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
// dev format example: http://localhost:3000/registration?method=invite-mail&email=wolle.huss%40pjannto.com&nonce=64853
const actionUrl = new URL('/registration', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('email', email)
actionUrl.searchParams.set('nonce', nonce)
if (inviteCode) {
actionUrl.searchParams.set('inviteCode', inviteCode)
actionUrl.searchParams.set('method', 'invite-code')
} else {
actionUrl.searchParams.set('method', 'invite-mail')
}
return {
from,
@ -34,8 +42,8 @@ export const signupTemplate = ({ email, nonce }) => {
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)
actionUrl.searchParams.set('nonce', nonce)
return {
from,
@ -77,7 +85,7 @@ export const wrongAccountTemplate = ({ email }) => {
subject,
html: mustache.render(
templates.layout,
{ actionUrl, supportUrl: CONFIG.SUPPORT_URL, welcomeImageUrl },
{ ...defaultParams, actionUrl, supportUrl: CONFIG.SUPPORT_URL, welcomeImageUrl },
{ content: templates.wrongAccount },
),
}

View File

@ -7,8 +7,8 @@
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="600" height="" alt="Welcome image" 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;"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
@ -105,8 +105,8 @@
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="600" height="" alt="Welcome image" 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;"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>

View File

@ -7,8 +7,8 @@
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="600" height="" alt="Welcome image" 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;"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
@ -105,8 +105,8 @@
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="600" height="" alt="Welcome image" 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;"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>

View File

@ -7,8 +7,8 @@
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="600" height="" alt="Welcome image" 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;"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
@ -118,8 +118,8 @@
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="600" height="" alt="Welcome image" 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;"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>

View File

@ -7,8 +7,8 @@
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="600" height="" alt="Welcome image" 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;"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
@ -105,8 +105,8 @@
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="600" height="" alt="Welcome image" 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;"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>

View File

@ -13,7 +13,7 @@ const driver = getDriver()
const neode = getNeode()
const categoryIds = ['cat9']
const createPostMutation = gql`
mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
mutation ($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
CreatePost(id: $id, title: $title, content: $postContent, categoryIds: $categoryIds) {
id
title
@ -22,7 +22,7 @@ const createPostMutation = gql`
}
`
const updatePostMutation = gql`
mutation($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
mutation ($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
UpdatePost(id: $id, content: $postContent, title: $title, categoryIds: $categoryIds) {
title
content
@ -95,7 +95,7 @@ describe('hashtags', () => {
</p>
`
const postWithHastagsQuery = gql`
query($id: ID) {
query ($id: ID) {
Post(id: $id) {
tags {
id

View File

@ -15,6 +15,7 @@ import hashtags from './hashtags/hashtagsMiddleware'
import email from './email/emailMiddleware'
import sentry from './sentryMiddleware'
import languages from './languages/languages'
import userInteractions from './userInteractions'
export default (schema) => {
const middlewares = {
@ -32,6 +33,7 @@ export default (schema) => {
includedFields,
orderBy,
languages,
userInteractions,
}
let order = [
@ -40,6 +42,7 @@ export default (schema) => {
'xss',
// 'activityPub', disabled temporarily
'validation',
'userInteractions',
'sluggify',
'languages',
'excerpt',

View File

@ -29,7 +29,7 @@ afterAll(async () => {
})
const createPostMutation = gql`
mutation($title: String!, $content: String!, $categoryIds: [ID]) {
mutation ($title: String!, $content: String!, $categoryIds: [ID]) {
CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
language
}

View File

@ -10,7 +10,7 @@ const driver = getDriver()
const neode = getNeode()
const categoryIds = ['cat9']
const createPostMutation = gql`
mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
mutation ($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
CreatePost(id: $id, title: $title, content: $postContent, categoryIds: $categoryIds) {
id
title
@ -19,7 +19,7 @@ const createPostMutation = gql`
}
`
const updatePostMutation = gql`
mutation($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
mutation ($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
UpdatePost(id: $id, content: $postContent, title: $title, categoryIds: $categoryIds) {
title
content
@ -27,7 +27,7 @@ const updatePostMutation = gql`
}
`
const createCommentMutation = gql`
mutation($id: ID, $postId: ID!, $commentContent: String!) {
mutation ($id: ID, $postId: ID!, $commentContent: String!) {
CreateComment(id: $id, postId: $postId, content: $commentContent) {
id
content
@ -80,7 +80,7 @@ afterEach(async () => {
describe('notifications', () => {
const notificationQuery = gql`
query($read: Boolean) {
query ($read: Boolean) {
notifications(read: $read, orderBy: updatedAt_desc) {
read
reason
@ -367,7 +367,7 @@ describe('notifications', () => {
describe('if the notification was marked as read earlier', () => {
const markAsReadAction = async () => {
const mutation = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
markAsRead(id: $id) {
read
}

View File

@ -1,6 +1,7 @@
import { rule, shield, deny, allow, or } from 'graphql-shield'
import { getNeode } from '../db/neo4j'
import CONFIG from '../config'
import { validateInviteCode } from '../schema/resolvers/transactions/inviteCodes'
const debug = !!CONFIG.DEBUG
const allowExternalErrors = true
@ -87,7 +88,14 @@ const noEmailFilter = rule({
return !('email' in args)
})
const publicRegistration = rule()(() => !!CONFIG.PUBLIC_REGISTRATION)
const publicRegistration = rule()(() => CONFIG.PUBLIC_REGISTRATION)
const inviteRegistration = rule()(async (_parent, args, { user, driver }) => {
if (!CONFIG.INVITE_REGISTRATION) return false
const { inviteCode } = args
const session = driver.session()
return validateInviteCode(session, inviteCode)
})
// Permissions
export default shield(
@ -121,13 +129,15 @@ export default shield(
userData: isAuthenticated,
MyInviteCodes: isAuthenticated,
isValidInviteCode: allow,
VerifyNonce: allow,
queryLocations: isAuthenticated,
availableRoles: isAdmin,
getInviteCode: isAuthenticated, // and inviteRegistration
},
Mutation: {
'*': deny,
login: allow,
SignupByInvitation: allow,
Signup: or(publicRegistration, isAdmin),
Signup: or(publicRegistration, inviteRegistration, isAdmin),
SignupVerification: allow,
UpdateUser: onlyYourself,
CreatePost: isAuthenticated,
@ -166,6 +176,8 @@ export default shield(
unpinPost: isAdmin,
UpdateDonations: isAdmin,
GenerateInviteCode: isAuthenticated,
switchUserRole: isAdmin,
markTeaserAsViewed: allow,
},
User: {
email: or(isMyOwn, isAdmin),

View File

@ -3,11 +3,13 @@ import createServer from '../server'
import Factory, { cleanDatabase } from '../db/factories'
import { gql } from '../helpers/jest'
import { getDriver, getNeode } from '../db/neo4j'
import CONFIG from '../config'
const instance = getNeode()
const driver = getDriver()
let query, authenticatedUser, owner, anotherRegularUser, administrator, variables, moderator
let query, mutate, variables
let authenticatedUser, owner, anotherRegularUser, administrator, moderator
describe('authorization', () => {
beforeAll(async () => {
@ -20,6 +22,7 @@ describe('authorization', () => {
}),
})
query = createTestClient(server).query
mutate = createTestClient(server).mutate
})
afterEach(async () => {
@ -77,7 +80,7 @@ describe('authorization', () => {
describe('access email address', () => {
const userQuery = gql`
query($name: String) {
query ($name: String) {
User(name: $name) {
email
}
@ -159,5 +162,132 @@ describe('authorization', () => {
})
})
})
describe('access Signup', () => {
const signupMutation = gql`
mutation ($email: String!, $inviteCode: String) {
Signup(email: $email, inviteCode: $inviteCode) {
email
}
}
`
describe('admin invite only', () => {
beforeEach(async () => {
variables = {
email: 'some@email.org',
inviteCode: 'AAAAAA',
}
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = false
await Factory.build('inviteCode', {
code: 'AAAAAA',
})
})
describe('as user', () => {
beforeEach(async () => {
authenticatedUser = await anotherRegularUser.toJson()
})
it('denies permission', async () => {
await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { Signup: null },
})
})
})
describe('as admin', () => {
beforeEach(async () => {
authenticatedUser = await administrator.toJson()
})
it('returns an email', async () => {
await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
errors: undefined,
data: {
Signup: { email: 'some@email.org' },
},
})
})
})
})
describe('public registration', () => {
beforeEach(async () => {
variables = {
email: 'some@email.org',
inviteCode: 'AAAAAA',
}
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = true
await Factory.build('inviteCode', {
code: 'AAAAAA',
})
})
describe('as anyone', () => {
beforeEach(async () => {
authenticatedUser = null
})
it('returns an email', async () => {
await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
errors: undefined,
data: {
Signup: { email: 'some@email.org' },
},
})
})
})
})
describe('invite registration', () => {
beforeEach(async () => {
CONFIG.INVITE_REGISTRATION = true
CONFIG.PUBLIC_REGISTRATION = false
await Factory.build('inviteCode', {
code: 'AAAAAA',
})
})
describe('as anyone with valid invite code', () => {
beforeEach(async () => {
variables = {
email: 'some@email.org',
inviteCode: 'AAAAAA',
}
authenticatedUser = null
})
it('returns an email', async () => {
await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
errors: undefined,
data: {
Signup: { email: 'some@email.org' },
},
})
})
})
describe('as anyone without valid invite', () => {
beforeEach(async () => {
variables = {
email: 'some@email.org',
inviteCode: 'no valid invite code',
}
authenticatedUser = null
})
it('denies permission', async () => {
await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { Signup: null },
})
})
})
})
})
})
})

View File

@ -11,7 +11,8 @@ let variables
const driver = getDriver()
const neode = getNeode()
beforeAll(() => {
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
@ -53,7 +54,7 @@ describe('slugifyMiddleware', () => {
describe('CreatePost', () => {
const categoryIds = ['cat9']
const createPostMutation = gql`
mutation($title: String!, $content: String!, $categoryIds: [ID]!, $slug: String) {
mutation ($title: String!, $content: String!, $categoryIds: [ID]!, $slug: String) {
CreatePost(title: $title, content: $content, categoryIds: $categoryIds, slug: $slug) {
slug
}
@ -163,7 +164,7 @@ describe('slugifyMiddleware', () => {
describe('SignupVerification', () => {
const mutation = gql`
mutation(
mutation (
$password: String!
$email: String!
$name: String!

View File

@ -0,0 +1,44 @@
const createRelatedCypher = (relation) => `
MATCH (user:User { id: $currentUser})
MATCH (post:Post { id: $postId})
OPTIONAL MATCH (post)<-[r:${relation}]-(u:User)
WHERE NOT u.disabled AND NOT u.deleted
WITH user, post, count(DISTINCT u) AS count
MERGE (user)-[relation:${relation} { }]->(post)
ON CREATE
SET relation.count = 1,
relation.createdAt = toString(datetime()),
post.clickedCount = count + 1
ON MATCH
SET relation.count = relation.count + 1,
relation.updatedAt = toString(datetime()),
post.clickedCount = count
RETURN user, post, relation
`
const setPostCounter = async (postId, relation, context) => {
const {
user: { id: currentUser },
} = context
const session = context.driver.session()
try {
await session.writeTransaction((txc) => {
return txc.run(createRelatedCypher(relation), { currentUser, postId })
})
} finally {
session.close()
}
}
const userClickedPost = async (resolve, root, args, context, info) => {
if (args.id) {
await setPostCounter(args.id, 'CLICKED', context)
}
return resolve(root, args, context, info)
}
export default {
Query: {
Post: userClickedPost,
},
}

View File

@ -0,0 +1,98 @@
import Factory, { cleanDatabase } from '../db/factories'
import { gql } from '../helpers/jest'
import { getNeode, getDriver } from '../db/neo4j'
import createServer from '../server'
import { createTestClient } from 'apollo-server-testing'
let query, aUser, bUser, post, authenticatedUser, variables
const driver = getDriver()
const neode = getNeode()
const postQuery = gql`
query ($id: ID) {
Post(id: $id) {
clickedCount
}
}
`
beforeAll(async () => {
await cleanDatabase()
aUser = await Factory.build('user', {
id: 'a-user',
})
bUser = await Factory.build('user', {
id: 'b-user',
})
post = await Factory.build('post')
authenticatedUser = await aUser.toJson()
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
query = createTestClient(server).query
})
afterAll(async () => {
await cleanDatabase()
})
describe('middleware/userInteractions', () => {
describe('given one post', () => {
it('does not change clickedCount when queried without ID', async () => {
await expect(query({ query: postQuery, variables })).resolves.toMatchObject({
data: {
Post: expect.arrayContaining([
{
clickedCount: 0,
},
]),
},
})
})
it('changes clickedCount when queried with ID', async () => {
variables = { id: post.get('id') }
await expect(query({ query: postQuery, variables })).resolves.toMatchObject({
data: {
Post: expect.arrayContaining([
{
clickedCount: 1,
},
]),
},
})
})
it('does not change clickedCount when same user queries the post again', async () => {
await expect(query({ query: postQuery, variables })).resolves.toMatchObject({
data: {
Post: expect.arrayContaining([
{
clickedCount: 1,
},
]),
},
})
})
it('changes clickedCount when another user queries the post', async () => {
authenticatedUser = await bUser.toJson()
await expect(query({ query: postQuery, variables })).resolves.toMatchObject({
data: {
Post: expect.arrayContaining([
{
clickedCount: 2,
},
]),
},
})
})
})
})

View File

@ -17,14 +17,14 @@ let authenticatedUser,
commentingUser
const createCommentMutation = gql`
mutation($id: ID, $postId: ID!, $content: String!) {
mutation ($id: ID, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
}
}
`
const updateCommentMutation = gql`
mutation($content: String!, $id: ID!) {
mutation ($content: String!, $id: ID!) {
UpdateComment(content: $content, id: $id) {
id
}
@ -32,7 +32,7 @@ const updateCommentMutation = gql`
`
const reportMutation = gql`
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
mutation ($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
fileReport(
resourceId: $resourceId
reasonCategory: $reasonCategory
@ -43,7 +43,7 @@ const reportMutation = gql`
}
`
const reviewMutation = gql`
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
mutation ($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
createdAt
updatedAt
@ -52,7 +52,7 @@ const reviewMutation = gql`
`
const updateUserMutation = gql`
mutation($id: ID!, $name: String) {
mutation ($id: ID!, $name: String) {
UpdateUser(id: $id, name: $name) {
name
}

View File

@ -3,5 +3,6 @@ export default {
alt: { type: 'string' },
sensitive: { type: 'boolean', default: false },
aspectRatio: { type: 'float', default: 1.0 },
type: { type: 'string' },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
}

View File

@ -22,6 +22,8 @@ export default {
contentExcerpt: { type: 'string', allow: [null] },
deleted: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },
clickedCount: { type: 'int', default: 0 },
viewedTeaserCount: { type: 'int', default: 0 },
notified: {
type: 'relationship',
relationship: 'NOTIFIED',

View File

@ -36,7 +36,7 @@ afterEach(async () => {
})
const createCommentMutation = gql`
mutation($id: ID, $postId: ID!, $content: String!) {
mutation ($id: ID, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
content
@ -128,7 +128,7 @@ describe('CreateComment', () => {
describe('UpdateComment', () => {
const updateCommentMutation = gql`
mutation($content: String!, $id: ID!) {
mutation ($content: String!, $id: ID!) {
UpdateComment(content: $content, id: $id) {
id
content
@ -220,7 +220,7 @@ describe('UpdateComment', () => {
describe('DeleteComment', () => {
const deleteCommentMutation = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
DeleteComment(id: $id) {
id
content

View File

@ -9,7 +9,7 @@ const instance = getNeode()
const driver = getDriver()
const updateDonationsMutation = gql`
mutation($goal: Int, $progress: Int) {
mutation ($goal: Int, $progress: Int) {
UpdateDonations(goal: $goal, progress: $progress) {
id
goal

View File

@ -6,6 +6,27 @@ import Validator from 'neode/build/Services/Validator.js'
import normalizeEmail from './helpers/normalizeEmail'
export default {
Query: {
VerifyNonce: async (_parent, args, context, _resolveInfo) => {
const session = context.driver.session()
const readTxResultPromise = session.readTransaction(async (txc) => {
const result = await txc.run(
`
MATCH (email:EmailAddress {email: $email, nonce: $nonce})
RETURN count(email) > 0 AS result
`,
{ email: args.email, nonce: args.nonce },
)
return result
})
try {
const txResult = await readTxResultPromise
return txResult.records[0].get('result')
} finally {
session.close()
}
},
},
Mutation: {
AddEmailAddress: async (_parent, args, context, _resolveInfo) => {
let response

View File

@ -6,7 +6,7 @@ import { createTestClient } from 'apollo-server-testing'
const neode = getNeode()
let mutate
let mutate, query
let authenticatedUser
let user
let variables
@ -16,7 +16,8 @@ beforeEach(async () => {
variables = {}
})
beforeAll(() => {
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
@ -27,6 +28,7 @@ beforeAll(() => {
},
})
mutate = createTestClient(server).mutate
query = createTestClient(server).query
})
afterEach(async () => {
@ -35,7 +37,7 @@ afterEach(async () => {
describe('AddEmailAddress', () => {
const mutation = gql`
mutation($email: String!) {
mutation ($email: String!) {
AddEmailAddress(email: $email) {
email
verifiedAt
@ -140,7 +142,7 @@ describe('AddEmailAddress', () => {
describe('VerifyEmailAddress', () => {
const mutation = gql`
mutation($email: String!, $nonce: String!) {
mutation ($email: String!, $nonce: String!) {
VerifyEmailAddress(email: $email, nonce: $nonce) {
email
createdAt
@ -185,7 +187,7 @@ describe('VerifyEmailAddress', () => {
let emailAddress
beforeEach(async () => {
emailAddress = await Factory.build('unverifiedEmailAddress', {
nonce: 'abcdef',
nonce: '12345',
verifiedAt: null,
createdAt: new Date().toISOString(),
email: 'to-be-verified@example.org',
@ -204,7 +206,7 @@ describe('VerifyEmailAddress', () => {
describe('given valid nonce for `UnverifiedEmailAddress` node', () => {
beforeEach(() => {
variables = { ...variables, nonce: 'abcdef' }
variables = { ...variables, nonce: '12345' }
})
describe('but the address does not belong to the authenticated user', () => {
@ -295,3 +297,40 @@ describe('VerifyEmailAddress', () => {
})
})
})
describe('VerifyNonce', () => {
beforeEach(async () => {
await Factory.build('emailAddress', {
nonce: '12345',
verifiedAt: null,
createdAt: new Date().toISOString(),
email: 'to-be-verified@example.org',
})
})
const verifyNonceQuery = gql`
query ($email: String!, $nonce: String!) {
VerifyNonce(email: $email, nonce: $nonce)
}
`
it('returns true when nonce and email match', async () => {
variables = {
email: 'to-be-verified@example.org',
nonce: '12345',
}
await expect(query({ query: verifyNonceQuery, variables })).resolves.toMatchObject({
data: { VerifyNonce: true },
})
})
it('returns false when nonce and email do not match', async () => {
variables = {
email: 'to-be-verified@example.org',
nonce: '---',
}
await expect(query({ query: verifyNonceQuery, variables })).resolves.toMatchObject({
data: { VerifyNonce: false },
})
})
})

View File

@ -37,8 +37,7 @@ const babyLovesCatEmbedResponse = new Response(
thumbnail_height: 360,
provider_url: 'https://www.youtube.com/',
thumbnail_width: 480,
html:
'<iframe width="480" height="270" src="https://www.youtube.com/embed/qkdXAtO40Fo?start=18&feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
html: '<iframe width="480" height="270" src="https://www.youtube.com/embed/qkdXAtO40Fo?start=18&feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
thumbnail_url: 'https://i.ytimg.com/vi/qkdXAtO40Fo/hqdefault.jpg',
version: '1.0',
author_name: 'Merkley Family',
@ -57,7 +56,7 @@ describe('Query', () => {
})
const { query } = createTestClient(server)
const embed = gql`
query($url: String!) {
query ($url: String!) {
embed(url: $url) {
type
title
@ -204,8 +203,7 @@ Have all the information for the brand in separate config files. Set these defau
video: null,
lang: 'de',
sources: ['resource', 'oembed'],
html:
'<iframe width="480" height="270" src="https://www.youtube.com/embed/qkdXAtO40Fo?start=18&feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
html: '<iframe width="480" height="270" src="https://www.youtube.com/embed/qkdXAtO40Fo?start=18&feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
},
},
errors: undefined,

View File

@ -16,7 +16,7 @@ let user2
let variables
const mutationFollowUser = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
followUser(id: $id) {
name
followedBy {
@ -29,7 +29,7 @@ const mutationFollowUser = gql`
`
const mutationUnfollowUser = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
unfollowUser(id: $id) {
name
followedBy {
@ -42,7 +42,7 @@ const mutationUnfollowUser = gql`
`
const userQuery = gql`
query($id: ID) {
query ($id: ID) {
User(id: $id) {
followedBy {
id

View File

@ -1,4 +1,5 @@
import { v4 as uuid } from 'uuid'
export default function generateNonce() {
return uuid().substring(0, 6)
return Array.from({ length: 5 }, (n = Math.floor(Math.random() * 10)) => {
return String.fromCharCode(n + 48)
}).join('')
}

View File

@ -2,7 +2,7 @@ import Resolver from './helpers/Resolver'
export default {
Image: {
...Resolver('Image', {
undefinedToNull: ['sensitive', 'alt', 'aspectRatio'],
undefinedToNull: ['sensitive', 'alt', 'aspectRatio', 'type'],
}),
},
}

View File

@ -53,8 +53,8 @@ export async function mergeImage(resource, relationshipType, imageInput, opts =
if (!(existingImage || upload)) throw new UserInputError('Cannot find image for given resource')
if (existingImage && upload) deleteImageFile(existingImage, deleteCallback)
const url = await uploadImageFile(upload, uploadCallback)
const { alt, sensitive, aspectRatio } = imageInput
const image = { alt, sensitive, aspectRatio, url }
const { alt, sensitive, aspectRatio, type } = imageInput
const image = { alt, sensitive, aspectRatio, url, type }
txResult = await transaction.run(
`
MATCH (resource {id: $resource.id})

View File

@ -1,5 +1,6 @@
import generateInviteCode from './helpers/generateInviteCode'
import Resolver from './helpers/Resolver'
import { validateInviteCode } from './transactions/inviteCodes'
const uniqueInviteCode = async (session, code) => {
return session.readTransaction(async (txc) => {
@ -12,6 +13,52 @@ const uniqueInviteCode = async (session, code) => {
export default {
Query: {
getInviteCode: async (_parent, args, context, _resolveInfo) => {
const {
user: { id: userId },
} = context
const session = context.driver.session()
const readTxResultPromise = session.readTransaction(async (txc) => {
const result = await txc.run(
`MATCH (user:User {id: $userId})-[:GENERATED]->(ic:InviteCode)
WHERE ic.expiresAt IS NULL
OR datetime(ic.expiresAt) >= datetime()
RETURN properties(ic) AS inviteCodes`,
{
userId,
},
)
return result.records.map((record) => record.get('inviteCodes'))
})
try {
const inviteCode = await readTxResultPromise
if (inviteCode && inviteCode.length > 0) return inviteCode[0]
let code = generateInviteCode()
while (!(await uniqueInviteCode(session, code))) {
code = generateInviteCode()
}
const writeTxResultPromise = session.writeTransaction(async (txc) => {
const result = await txc.run(
`MATCH (user:User {id: $userId})
MERGE (user)-[:GENERATED]->(ic:InviteCode { code: $code })
ON CREATE SET
ic.createdAt = toString(datetime()),
ic.expiresAt = $expiresAt
RETURN ic AS inviteCode`,
{
userId,
code,
expiresAt: null,
},
)
return result.records.map((record) => record.get('inviteCode').properties)
})
const txResult = await writeTxResultPromise
return txResult[0]
} finally {
session.close()
}
},
MyInviteCodes: async (_parent, args, context, _resolveInfo) => {
const {
user: { id: userId },
@ -36,28 +83,9 @@ export default {
},
isValidInviteCode: async (_parent, args, context, _resolveInfo) => {
const { code } = args
if (!code) return false
const session = context.driver.session()
const readTxResultPromise = session.readTransaction(async (txc) => {
const result = await txc.run(
`MATCH (ic:InviteCode { code: toUpper($code) })
RETURN
CASE
WHEN ic.expiresAt IS NULL THEN true
WHEN datetime(ic.expiresAt) >= datetime() THEN true
ELSE false END AS result`,
{
code,
},
)
return result.records.map((record) => record.get('result'))
})
try {
const txResult = await readTxResultPromise
return !!txResult[0]
} finally {
session.close()
}
if (!code) return false
return validateInviteCode(session, code)
},
},
Mutation: {

View File

@ -11,7 +11,7 @@ let mutate
const driver = getDriver()
const generateInviteCodeMutation = gql`
mutation($expiresAt: String = null) {
mutation ($expiresAt: String = null) {
GenerateInviteCode(expiresAt: $expiresAt) {
code
createdAt
@ -31,7 +31,7 @@ const myInviteCodesQuery = gql`
`
const isValidInviteCodeQuery = gql`
query($code: ID!) {
query ($code: ID!) {
isValidInviteCode(code: $code)
}
`

View File

@ -31,7 +31,7 @@ describe('resolvers', () => {
describe('custom mutation, not handled by neo4j-graphql-js', () => {
let variables
const updateUserMutation = gql`
mutation($id: ID!, $name: String) {
mutation ($id: ID!, $name: String) {
UpdateUser(id: $id, name: $name) {
name
location {

View File

@ -16,7 +16,7 @@ let mutate,
closeReportVariables
const reviewMutation = gql`
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
mutation ($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
createdAt
updatedAt

View File

@ -139,7 +139,7 @@ describe('given some notifications', () => {
describe('notifications', () => {
const notificationQuery = gql`
query($read: Boolean, $orderBy: NotificationOrdering) {
query ($read: Boolean, $orderBy: NotificationOrdering) {
notifications(read: $read, orderBy: $orderBy) {
from {
__typename
@ -249,7 +249,7 @@ describe('given some notifications', () => {
const deletePostAction = async () => {
authenticatedUser = await author.toJson()
const deletePostMutation = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
DeletePost(id: $id) {
id
deleted
@ -284,7 +284,7 @@ describe('given some notifications', () => {
describe('markAsRead', () => {
const markAsReadMutation = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
markAsRead(id: $id) {
from {
__typename

View File

@ -55,7 +55,7 @@ describe('passwordReset', () => {
describe('requestPasswordReset', () => {
const mutation = gql`
mutation($email: String!) {
mutation ($email: String!) {
requestPasswordReset(email: $email)
}
`
@ -116,7 +116,7 @@ describe('resetPassword', () => {
}
const mutation = gql`
mutation($nonce: String!, $email: String!, $newPassword: String!) {
mutation ($nonce: String!, $email: String!, $newPassword: String!) {
resetPassword(nonce: $nonce, email: $email, newPassword: $newPassword)
}
`
@ -196,7 +196,7 @@ describe('resetPassword', () => {
it('updates password of the user', async () => {
await mutate({ mutation, variables })
const checkLoginMutation = gql`
mutation($email: String!, $password: String!) {
mutation ($email: String!, $password: String!) {
login(email: $email, password: $password)
}
`

View File

@ -88,6 +88,8 @@ export default {
SET post += $params
SET post.createdAt = toString(datetime())
SET post.updatedAt = toString(datetime())
SET post.clickedCount = 0
SET post.viewedTeaserCount = 0
WITH post
MATCH (author:User {id: $userId})
MERGE (post)<-[:WROTE]-(author)
@ -315,13 +317,38 @@ export default {
}
return unpinnedPost
},
markTeaserAsViewed: async (_parent, params, context, _resolveInfo) => {
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (post:Post { id: $params.id })
MATCH (user:User { id: $userId })
MERGE (user)-[relation:VIEWED_TEASER { }]->(post)
ON CREATE
SET relation.createdAt = toString(datetime()),
post.viewedTeaserCount = post.viewedTeaserCount + 1
RETURN post
`,
{ userId: context.user.id, params },
)
return transactionResponse.records.map((record) => record.get('post').properties)
})
try {
const [post] = await writeTxResultPromise
post.viewedTeaserCount = post.viewedTeaserCount.low
return post
} finally {
session.close()
}
},
},
Post: {
...Resolver('Post', {
undefinedToNull: ['activityId', 'objectId', 'language', 'pinnedAt', 'pinned'],
hasMany: {
tags: '-[:TAGGED]->(related:Tag)',
categories: '-[:CATEGORIZED]->(related:Category)',
// categories: '-[:CATEGORIZED]->(related:Category)',
comments: '<-[:COMMENTS]-(related:Comment)',
shoutedBy: '<-[:SHOUTED]-(related:User)',
emotions: '<-[related:EMOTED]',
@ -341,6 +368,8 @@ export default {
boolean: {
shoutedByCurrentUser:
'MATCH(this)<-[:SHOUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1',
viewedTeaserByCurrentUser:
'MATCH (this)<-[:VIEWED_TEASER]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
},
}),
relatedContributions: async (parent, params, context, resolveInfo) => {

View File

@ -16,7 +16,7 @@ const categoryIds = ['cat9', 'cat4', 'cat15']
let variables
const createPostMutation = gql`
mutation($id: ID, $title: String!, $content: String!, $language: String, $categoryIds: [ID]) {
mutation ($id: ID, $title: String!, $content: String!, $language: String, $categoryIds: [ID]) {
CreatePost(
id: $id
title: $title
@ -147,7 +147,7 @@ describe('Post', () => {
})
})
it('by categories', async () => {
/* it('by categories', async () => {
const postQueryFilteredByCategories = gql`
query Post($filter: _PostFilter) {
Post(filter: $filter) {
@ -172,7 +172,7 @@ describe('Post', () => {
await expect(
query({ query: postQueryFilteredByCategories, variables }),
).resolves.toMatchObject(expected)
})
}) */
describe('by emotions', () => {
const postQueryFilteredByEmotions = gql`
@ -323,14 +323,8 @@ describe('CreatePost', () => {
describe('UpdatePost', () => {
let author, newlyCreatedPost
const updatePostMutation = gql`
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID], $image: ImageInput) {
UpdatePost(
id: $id
title: $title
content: $content
categoryIds: $categoryIds
image: $image
) {
mutation ($id: ID!, $title: String!, $content: String!, $image: ImageInput) {
UpdatePost(id: $id, title: $title, content: $content, image: $image) {
id
title
content
@ -338,9 +332,6 @@ describe('UpdatePost', () => {
name
slug
}
categories {
id
}
createdAt
updatedAt
}
@ -428,7 +419,7 @@ describe('UpdatePost', () => {
expect(newlyCreatedPost.updatedAt).not.toEqual(UpdatePost.updatedAt)
})
describe('no new category ids provided for update', () => {
/* describe('no new category ids provided for update', () => {
it('resolves and keeps current categories', async () => {
const expected = {
data: {
@ -443,9 +434,9 @@ describe('UpdatePost', () => {
expected,
)
})
})
}) */
describe('given category ids', () => {
/* describe('given category ids', () => {
beforeEach(() => {
variables = { ...variables, categoryIds: ['cat27'] }
})
@ -464,7 +455,7 @@ describe('UpdatePost', () => {
expected,
)
})
})
}) */
describe('params.image', () => {
describe('is object', () => {
@ -506,7 +497,7 @@ describe('UpdatePost', () => {
describe('pin posts', () => {
let author
const pinPostMutation = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
pinPost(id: $id) {
id
title
@ -782,7 +773,7 @@ describe('pin posts', () => {
it('pinned post appear first even when created before other posts', async () => {
const postOrderingQuery = gql`
query($orderBy: [_PostOrdering]) {
query ($orderBy: [_PostOrdering]) {
Post(orderBy: $orderBy) {
id
pinned
@ -825,7 +816,7 @@ describe('pin posts', () => {
describe('unpin posts', () => {
let pinnedPost
const unpinPostMutation = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
unpinPost(id: $id) {
id
title
@ -937,7 +928,7 @@ describe('unpin posts', () => {
describe('DeletePost', () => {
let author
const deletePostMutation = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
DeletePost(id: $id) {
id
deleted
@ -1061,14 +1052,14 @@ describe('DeletePost', () => {
describe('emotions', () => {
let author, postToEmote
const PostsEmotionsCountQuery = gql`
query($id: ID!) {
query ($id: ID!) {
Post(id: $id) {
emotionsCount
}
}
`
const PostsEmotionsQuery = gql`
query($id: ID!) {
query ($id: ID!) {
Post(id: $id) {
emotions {
emotion
@ -1102,7 +1093,7 @@ describe('emotions', () => {
describe('AddPostEmotions', () => {
const addPostEmotionsMutation = gql`
mutation($to: _PostInput!, $data: _EMOTEDInput!) {
mutation ($to: _PostInput!, $data: _EMOTEDInput!) {
AddPostEmotions(to: $to, data: $data) {
from {
id
@ -1219,7 +1210,7 @@ describe('emotions', () => {
describe('RemovePostEmotions', () => {
let removePostEmotionsVariables, postsEmotionsQueryVariables
const removePostEmotionsMutation = gql`
mutation($to: _PostInput!, $data: _EMOTEDInput!) {
mutation ($to: _PostInput!, $data: _EMOTEDInput!) {
RemovePostEmotions(to: $to, data: $data) {
from {
id
@ -1318,13 +1309,13 @@ describe('emotions', () => {
let PostsEmotionsByCurrentUserVariables
const PostsEmotionsCountByEmotionQuery = gql`
query($postId: ID!, $data: _EMOTEDInput!) {
query ($postId: ID!, $data: _EMOTEDInput!) {
PostsEmotionsCountByEmotion(postId: $postId, data: $data)
}
`
const PostsEmotionsByCurrentUserQuery = gql`
query($postId: ID!) {
query ($postId: ID!) {
PostsEmotionsByCurrentUser(postId: $postId)
}
`

View File

@ -29,34 +29,22 @@ export default {
}
args.termsAndConditionsAgreedAt = new Date().toISOString()
let { nonce, email } = args
let { nonce, email, inviteCode } = args
email = normalizeEmail(email)
delete args.nonce
delete args.email
delete args.inviteCode
args = encryptPassword(args)
const { driver } = context
const session = driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const createUserTransactionResponse = await transaction.run(
`
MATCH(email:EmailAddress {nonce: $nonce, email: $email})
WHERE NOT (email)-[:BELONGS_TO]->()
CREATE (user:User)
MERGE(user)-[:PRIMARY_EMAIL]->(email)
MERGE(user)<-[:BELONGS_TO]-(email)
SET user += $args
SET user.id = randomUUID()
SET user.role = 'user'
SET user.createdAt = toString(datetime())
SET user.updatedAt = toString(datetime())
SET user.allowEmbedIframes = FALSE
SET user.showShoutsPublicly = FALSE
SET email.verifiedAt = toString(datetime())
RETURN user {.*}
`,
{ args, nonce, email },
)
const createUserTransactionResponse = await transaction.run(signupCypher(inviteCode), {
args,
nonce,
email,
inviteCode,
})
const [user] = createUserTransactionResponse.records.map((record) => record.get('user'))
if (!user) throw new UserInputError('Invalid email or nonce')
return user
@ -74,3 +62,39 @@ export default {
},
},
}
const signupCypher = (inviteCode) => {
let optionalMatch = ''
let optionalMerge = ''
if (inviteCode) {
optionalMatch = `
OPTIONAL MATCH
(inviteCode:InviteCode {code: $inviteCode})<-[:GENERATED]-(host:User)
`
optionalMerge = `
MERGE(user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode)
MERGE(host)-[:INVITED { createdAt: toString(datetime()) }]->(user)
MERGE(user)-[:FOLLOWS { createdAt: toString(datetime()) }]->(host)
MERGE(host)-[:FOLLOWS { createdAt: toString(datetime()) }]->(user)
`
}
const cypher = `
MATCH(email:EmailAddress {nonce: $nonce, email: $email})
WHERE NOT (email)-[:BELONGS_TO]->()
${optionalMatch}
CREATE (user:User)
MERGE(user)-[:PRIMARY_EMAIL]->(email)
MERGE(user)<-[:BELONGS_TO]-(email)
${optionalMerge}
SET user += $args
SET user.id = randomUUID()
SET user.role = 'user'
SET user.createdAt = toString(datetime())
SET user.updatedAt = toString(datetime())
SET user.allowEmbedIframes = FALSE
SET user.showShoutsPublicly = FALSE
SET email.verifiedAt = toString(datetime())
RETURN user {.*}
`
return cypher
}

View File

@ -3,6 +3,7 @@ import { gql } from '../../helpers/jest'
import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
import CONFIG from '../../config'
const neode = getNeode()
@ -15,7 +16,8 @@ beforeEach(async () => {
variables = {}
})
beforeAll(() => {
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
@ -34,8 +36,8 @@ afterEach(async () => {
describe('Signup', () => {
const mutation = gql`
mutation($email: String!) {
Signup(email: $email) {
mutation ($email: String!, $inviteCode: String) {
Signup(email: $email, inviteCode: $inviteCode) {
email
}
}
@ -50,6 +52,8 @@ describe('Signup', () => {
})
it('throws AuthorizationError', async () => {
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = false
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
})
@ -141,7 +145,7 @@ describe('Signup', () => {
describe('SignupVerification', () => {
const mutation = gql`
mutation(
mutation (
$name: String!
$password: String!
$email: String!

View File

@ -11,7 +11,7 @@ describe('file a report on a resource', () => {
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser
const categoryIds = ['cat9']
const fileReportMutation = gql`
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
mutation ($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
fileReport(
resourceId: $resourceId
reasonCategory: $reasonCategory
@ -42,7 +42,7 @@ describe('file a report on a resource', () => {
reasonDescription: 'Violates code of conduct !!!',
}
const reportsQuery = gql`
query($closed: Boolean) {
query ($closed: Boolean) {
reports(orderBy: createdAt_desc, closed: $closed) {
id
createdAt
@ -74,7 +74,7 @@ describe('file a report on a resource', () => {
}
`
const reviewMutation = gql`
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
mutation ($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
createdAt
resource {

View File

@ -16,6 +16,7 @@ describe('rewards', () => {
}
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
@ -75,7 +76,7 @@ describe('rewards', () => {
describe('reward', () => {
const rewardMutation = gql`
mutation($from: ID!, $to: ID!) {
mutation ($from: ID!, $to: ID!) {
reward(badgeKey: $from, userId: $to) {
id
badges {
@ -265,7 +266,7 @@ describe('rewards', () => {
}
const unrewardMutation = gql`
mutation($from: ID!, $to: ID!) {
mutation ($from: ID!, $to: ID!) {
unreward(badgeKey: $from, userId: $to) {
id
badges {

View File

@ -0,0 +1,7 @@
export default {
Query: {
availableRoles: async (_parent, args, context, _resolveInfo) => {
return ['admin', 'moderator', 'user']
},
},
}

View File

@ -38,7 +38,9 @@ const searchPostsSetup = {
__typename: labels(resource)[0],
author: properties(author),
commentsCount: toString(size(comments)),
shoutedCount: toString(size(shouter))
shoutedCount: toString(size(shouter)),
clickedCount: toString(resource.clickedCount),
viewedTeaserCount: toString(resource.viewedTeaserCount)
}`,
limit: 'LIMIT $limit',
}

View File

@ -28,7 +28,7 @@ afterAll(async () => {
})
const searchQuery = gql`
query($query: String!) {
query ($query: String!) {
searchResults(query: $query, limit: 5) {
__typename
... on Post {
@ -49,7 +49,7 @@ const searchQuery = gql`
`
const searchPostQuery = gql`
query($query: String!, $firstPosts: Int, $postsOffset: Int) {
query ($query: String!, $firstPosts: Int, $postsOffset: Int) {
searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
postCount
posts {

View File

@ -9,17 +9,17 @@ const instance = getNeode()
const driver = getDriver()
const mutationShoutPost = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
shout(id: $id, type: Post)
}
`
const mutationUnshoutPost = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
unshout(id: $id, type: Post)
}
`
const queryPost = gql`
query($id: ID!) {
query ($id: ID!) {
Post(id: $id) {
id
shoutedBy {

View File

@ -70,7 +70,7 @@ describe('SocialMedia', () => {
beforeEach(() => {
mutation = gql`
mutation($url: String!) {
mutation ($url: String!) {
CreateSocialMedia(url: $url) {
id
url
@ -131,7 +131,7 @@ describe('SocialMedia', () => {
describe('ownedBy', () => {
beforeEach(() => {
mutation = gql`
mutation($url: String!) {
mutation ($url: String!) {
CreateSocialMedia(url: $url) {
url
ownedBy {
@ -162,7 +162,7 @@ describe('SocialMedia', () => {
const socialMedia = await setUpSocialMedia()
mutation = gql`
mutation($id: ID!, $url: String!) {
mutation ($id: ID!, $url: String!) {
UpdateSocialMedia(id: $id, url: $url) {
id
url
@ -225,7 +225,7 @@ describe('SocialMedia', () => {
const socialMedia = await setUpSocialMedia()
mutation = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
DeleteSocialMedia(id: $id) {
id
url

View File

@ -0,0 +1,22 @@
export async function validateInviteCode(session, inviteCode) {
const readTxResultPromise = session.readTransaction(async (txc) => {
const result = await txc.run(
`MATCH (ic:InviteCode { code: toUpper($inviteCode) })
RETURN
CASE
WHEN ic.expiresAt IS NULL THEN true
WHEN datetime(ic.expiresAt) >= datetime() THEN true
ELSE false END AS result`,
{
inviteCode,
},
)
return result.records.map((record) => record.get('result'))
})
try {
const txResult = await readTxResultPromise
return !!txResult[0]
} finally {
session.close()
}
}

View File

@ -39,7 +39,7 @@ afterAll(async () => {
})
const userDataQuery = gql`
query($id: ID!) {
query ($id: ID!) {
userData(id: $id) {
user {
id

View File

@ -171,7 +171,7 @@ describe('currentUser', () => {
describe('login', () => {
const loginMutation = gql`
mutation($email: String!, $password: String!) {
mutation ($email: String!, $password: String!) {
login(email: $email, password: $password)
}
`
@ -287,7 +287,7 @@ describe('login', () => {
describe('change password', () => {
const changePasswordMutation = gql`
mutation($oldPassword: String!, $newPassword: String!) {
mutation ($oldPassword: String!, $newPassword: String!) {
changePassword(oldPassword: $oldPassword, newPassword: $newPassword)
}
`

View File

@ -244,6 +244,31 @@ export default {
session.close()
}
},
switchUserRole: async (object, args, context, resolveInfo) => {
const { role, id } = args
if (context.user.id === id) throw new Error('you-cannot-change-your-own-role')
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const switchUserRoleResponse = await transaction.run(
`
MATCH (user:User {id: $id})
SET user.role = $role
SET user.updatedAt = toString(datetime())
RETURN user {.*}
`,
{ id, role },
)
const [user] = switchUserRoleResponse.records.map((record) => record.get('user'))
return user
})
try {
const user = await writeTxResultPromise
return user
} finally {
session.close()
}
},
},
User: {
email: async (parent, params, context, resolveInfo) => {

View File

@ -17,7 +17,7 @@ const driver = getDriver()
const neode = getNeode()
const deleteUserMutation = gql`
mutation($id: ID!, $resource: [Deletable]) {
mutation ($id: ID!, $resource: [Deletable]) {
DeleteUser(id: $id, resource: $resource) {
id
name
@ -45,6 +45,18 @@ const deleteUserMutation = gql`
}
`
const switchUserRoleMutation = gql`
mutation ($role: UserGroup!, $id: ID!) {
switchUserRole(role: $role, id: $id) {
name
role
id
updatedAt
email
}
}
`
beforeAll(() => {
const { server } = createServer({
context: () => {
@ -69,7 +81,7 @@ describe('User', () => {
beforeEach(async () => {
userQuery = gql`
query($email: String) {
query ($email: String) {
User(email: $email) {
name
}
@ -132,7 +144,7 @@ describe('UpdateUser', () => {
beforeEach(async () => {
updateUserMutation = gql`
mutation(
mutation (
$id: ID!
$name: String
$termsAndConditionsAgreedVersion: String
@ -458,3 +470,71 @@ describe('Delete a User as admin', () => {
})
})
})
describe('switch user role', () => {
beforeEach(async () => {
user = await Factory.build('user', {
id: 'user',
role: 'user',
})
admin = await Factory.build('user', {
role: 'admin',
id: 'admin',
})
})
describe('as simple user', () => {
it('cannot change the role', async () => {
authenticatedUser = await user.toJson()
variables = {
id: 'user',
role: 'admin',
}
await expect(mutate({ mutation: switchUserRoleMutation, variables })).resolves.toEqual(
expect.objectContaining({
errors: [
expect.objectContaining({
message: 'Not Authorised!',
}),
],
}),
)
})
})
describe('as admin', () => {
it('changes the role of other user', async () => {
authenticatedUser = await admin.toJson()
variables = {
id: 'user',
role: 'moderator',
}
await expect(mutate({ mutation: switchUserRoleMutation, variables })).resolves.toEqual(
expect.objectContaining({
data: {
switchUserRole: expect.objectContaining({
role: 'moderator',
}),
},
}),
)
})
it('cannot change own role', async () => {
authenticatedUser = await admin.toJson()
variables = {
id: 'admin',
role: 'moderator',
}
await expect(mutate({ mutation: switchUserRoleMutation, variables })).resolves.toEqual(
expect.objectContaining({
errors: [
expect.objectContaining({
message: 'you-cannot-change-your-own-role',
}),
],
}),
)
})
})
})

View File

@ -9,7 +9,7 @@ const driver = getDriver()
let authenticatedUser, mutate, query, variables
const updateUserMutation = gql`
mutation($id: ID!, $name: String!, $locationName: String) {
mutation ($id: ID!, $name: String!, $locationName: String) {
UpdateUser(id: $id, name: $name, locationName: $locationName) {
locationName
}
@ -17,7 +17,7 @@ const updateUserMutation = gql`
`
const queryLocations = gql`
query($place: String!, $lang: String!) {
query ($place: String!, $lang: String!) {
queryLocations(place: $place, lang: $lang) {
place_name
id
@ -30,46 +30,46 @@ const newlyCreatedNodesWithLocales = [
city: {
id: expect.stringContaining('place'),
type: 'place',
name: 'Hamburg',
nameEN: 'Hamburg',
nameDE: 'Hamburg',
namePT: 'Hamburg',
nameES: 'Hamburg',
nameFR: 'Hamburg',
nameIT: 'Hamburg',
nameRU: 'Хамбург',
nameNL: 'Hamburg',
namePL: 'Hamburg',
lng: -74.5763,
lat: 41.1534,
name: 'Welzheim',
nameEN: 'Welzheim',
nameDE: 'Welzheim',
namePT: 'Welzheim',
nameES: 'Welzheim',
nameFR: 'Welzheim',
nameIT: 'Welzheim',
nameRU: 'Вельцхайм',
nameNL: 'Welzheim',
namePL: 'Welzheim',
lng: 9.63444,
lat: 48.87472,
},
state: {
id: expect.stringContaining('region'),
type: 'region',
name: 'New Jersey',
nameEN: 'New Jersey',
nameDE: 'New Jersey',
namePT: 'Nova Jérsia',
nameES: 'Nueva Jersey',
nameFR: 'New Jersey',
nameIT: 'New Jersey',
nameRU: 'Нью-Джерси',
nameNL: 'New Jersey',
namePL: 'New Jersey',
name: 'Baden-Württemberg',
nameDE: 'Baden-Württemberg',
nameEN: 'Baden-Württemberg',
nameES: 'Baden-Wurtemberg',
nameFR: 'Bade-Wurtemberg',
nameIT: 'Baden-Württemberg',
nameNL: 'Baden-Württemberg',
namePL: 'Badenia-Wirtembergia',
namePT: 'Baden-Württemberg',
nameRU: 'Баден-Вюртемберг',
},
country: {
id: expect.stringContaining('country'),
type: 'country',
name: 'United States',
nameEN: 'United States',
nameDE: 'Vereinigte Staaten',
namePT: 'Estados Unidos',
nameES: 'Estados Unidos',
nameFR: 'États-Unis',
nameIT: "Stati Uniti d'America",
nameRU: 'Соединённые Штаты Америки',
nameNL: 'Verenigde Staten van Amerika',
namePL: 'Stany Zjednoczone',
name: 'Germany',
nameDE: 'Deutschland',
nameEN: 'Germany',
nameES: 'Alemania',
nameFR: 'Allemagne',
nameIT: 'Germania',
nameNL: 'Duitsland',
namePL: 'Niemcy',
namePT: 'Alemanha',
nameRU: 'Германия',
},
},
]
@ -114,10 +114,22 @@ describe('Location Service', () => {
const result = await query({ query: queryLocations, variables })
expect(result.data.queryLocations).toEqual([
{ id: 'place.14094307404564380', place_name: 'Berlin, Germany' },
{ id: 'place.15095411613564380', place_name: 'Berlin, Maryland, United States' },
{ id: 'place.5225018734564380', place_name: 'Berlin, Connecticut, United States' },
{ id: 'place.16922023226564380', place_name: 'Berlin, New Jersey, United States' },
{ id: 'place.4035845612564380', place_name: 'Berlin Township, New Jersey, United States' },
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin, Maryland, United States',
},
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin, Connecticut, United States',
},
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin, New Jersey, United States',
},
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin Township, New Jersey, United States',
},
])
})
@ -128,11 +140,23 @@ describe('Location Service', () => {
}
const result = await query({ query: queryLocations, variables })
expect(result.data.queryLocations).toEqual([
{ id: 'place.14094307404564380', place_name: 'Berlin, Deutschland' },
{ id: 'place.15095411613564380', place_name: 'Berlin, Maryland, Vereinigte Staaten' },
{ id: 'place.16922023226564380', place_name: 'Berlin, New Jersey, Vereinigte Staaten' },
{ id: 'place.10735893248465990', place_name: 'Berlin Heights, Ohio, Vereinigte Staaten' },
{ id: 'place.1165756679564380', place_name: 'Berlin, Massachusetts, Vereinigte Staaten' },
{ id: expect.stringMatching(/^place\.[0-9]+$/), place_name: 'Berlin, Deutschland' },
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin, Maryland, Vereinigte Staaten',
},
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin, New Jersey, Vereinigte Staaten',
},
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin Heights, Ohio, Vereinigte Staaten',
},
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin, Massachusetts, Vereinigte Staaten',
},
])
})
@ -170,7 +194,7 @@ describe('userMiddleware', () => {
...variables,
id: 'updating-user',
name: 'Updating user',
locationName: 'Hamburg, New Jersey, United States of America',
locationName: 'Welzheim, Baden-Württemberg, Germany',
}
await mutate({ mutation: updateUserMutation, variables })
const locations = await neode.cypher(

View File

@ -93,7 +93,7 @@ describe('muteUser', () => {
muteAction = (variables) => {
const { mutate } = createTestClient(server)
const muteUserMutation = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
muteUser(id: $id) {
id
name
@ -310,7 +310,7 @@ describe('unmuteUser', () => {
unmuteAction = (variables) => {
const { mutate } = createTestClient(server)
const unmuteUserMutation = gql`
mutation($id: ID!) {
mutation ($id: ID!) {
unmuteUser(id: $id) {
id
name

View File

@ -0,0 +1,83 @@
import { createTestClient } from 'apollo-server-testing'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
const driver = getDriver()
const neode = getNeode()
let mutate
let authenticatedUser
let variables
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
mutate = createTestClient(server).mutate
})
afterAll(async () => {
await cleanDatabase()
})
describe('count post teaser views', () => {
let aUser, bUser
const markTeaserAsViewed = gql`
mutation ($id: ID!) {
markTeaserAsViewed(id: $id) {
id
viewedTeaserCount
}
}
`
beforeAll(async () => {
Factory.build('post', { id: 'post-to-be-viewed' })
aUser = await Factory.build('user', { id: 'a-user' })
bUser = await Factory.build('user', { id: 'b-user' })
variables = {
id: 'post-to-be-viewed',
}
authenticatedUser = await aUser.toJson()
})
it('marks the post as viewed and increases the viewedTeaserCount', async () => {
await expect(mutate({ mutation: markTeaserAsViewed, variables })).resolves.toMatchObject({
data: {
markTeaserAsViewed: expect.objectContaining({
viewedTeaserCount: 1,
}),
},
})
})
it('does not increase the viewedTeaserCount when accidently called again', async () => {
await expect(mutate({ mutation: markTeaserAsViewed, variables })).resolves.toMatchObject({
data: {
markTeaserAsViewed: expect.objectContaining({
viewedTeaserCount: 1,
}),
},
})
})
it('increases the viewedTeaserCount when viewed by another user', async () => {
authenticatedUser = await bUser.toJson()
await expect(mutate({ mutation: markTeaserAsViewed, variables })).resolves.toMatchObject({
data: {
markTeaserAsViewed: expect.objectContaining({
viewedTeaserCount: 2,
}),
},
})
})
})

View File

@ -2,4 +2,4 @@ enum UserGroup {
admin
moderator
user
}
}

View File

@ -4,12 +4,16 @@ type EmailAddress {
createdAt: String
}
type Query {
VerifyNonce(email: String!, nonce: String!): Boolean!
}
type Mutation {
Signup(email: String!): EmailAddress
SignupByInvitation(email: String!, token: String!): EmailAddress
Signup(email: String!, inviteCode: String = null): EmailAddress
SignupVerification(
nonce: String!
email: String!
inviteCode: String = null
name: String!
password: String!
slug: String

View File

@ -8,6 +8,7 @@ type Image {
alt: String,
sensitive: Boolean,
aspectRatio: Float,
type: String,
}
input ImageInput {
@ -15,4 +16,5 @@ input ImageInput {
upload: Upload,
sensitive: Boolean,
aspectRatio: Float,
type: String,
}

View File

@ -14,4 +14,5 @@ type Mutation {
type Query {
MyInviteCodes: [InviteCode]
isValidInviteCode(code: ID!): Boolean
getInviteCode: InviteCode
}

View File

@ -156,6 +156,14 @@ type Post {
statement: "MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1"
)
clickedCount: Int!
viewedTeaserCount: Int!
viewedTeaserByCurrentUser: Boolean!
@cypher(
statement: "MATCH (this)<-[:VIEWED_TEASER]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1"
)
emotions: [EMOTED]
emotionsCount: Int!
@cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)")
@ -193,6 +201,7 @@ type Mutation {
RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
pinPost(id: ID!): Post
unpinPost(id: ID!): Post
markTeaserAsViewed(id: ID!): Post
}
type Query {

View File

@ -170,6 +170,7 @@ type Query {
filter: _UserFilter
): [User]
availableRoles: [UserGroup]!
mutedUsers: [User]
blockedUsers: [User]
isLoggedIn: Boolean!
@ -215,4 +216,6 @@ type Mutation {
unmuteUser(id: ID!): User
blockUser(id: ID!): User
unblockUser(id: ID!): User
switchUserRole(role: UserGroup!, id: ID!): User
}

View File

@ -910,6 +910,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.9.2":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
version "7.8.6"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
@ -956,32 +963,32 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@graphql-toolkit/common@0.9.12":
version "0.9.12"
resolved "https://registry.yarnpkg.com/@graphql-toolkit/common/-/common-0.9.12.tgz#a3bf91d00130100cfa0a72be718b16cc1011ab45"
integrity sha512-NjBVxeM1GB0bldiBm4UQoxKtbu/hjRfcqDEt1JPia+uXgFde3qTxtF5IZkworRFNCp+8KPSVrwcreQHSfFW63w==
"@graphql-toolkit/common@0.10.4":
version "0.10.4"
resolved "https://registry.yarnpkg.com/@graphql-toolkit/common/-/common-0.10.4.tgz#7785f2a3f14559d0778859c49f4442078c196695"
integrity sha512-HQ3HaxCqX+UE8y/0h7LMDBBGSIKJxY/gaQesaksvE2Y+N4NpSWdiW6HpOcgXfC2HGf9yM0hEdsERzzL8z3mbHQ==
dependencies:
aggregate-error "3.0.1"
camel-case "4.1.1"
graphql-tools-fork "9.0.1"
graphql-tools "5.0.0"
lodash "4.17.15"
"@graphql-toolkit/file-loading@0.9.12":
version "0.9.12"
resolved "https://registry.yarnpkg.com/@graphql-toolkit/file-loading/-/file-loading-0.9.12.tgz#f7d5d7e042df59c4e6c8fe3417c67dd1cddf9043"
integrity sha512-xEmKkbWI5FXQARTN3dbPzshwcmduOJUvx1QGsfcuryVDdQJZynUEaPF7IxYaFpvQF7vwxGIRdff/jC/a5ypDrA==
"@graphql-toolkit/file-loading@0.10.4":
version "0.10.4"
resolved "https://registry.yarnpkg.com/@graphql-toolkit/file-loading/-/file-loading-0.10.4.tgz#50e8933e44b17853544c1fe63350df93f33a5e80"
integrity sha512-oUmy/sO3BJfax85pVKI7FZ6TWrViNuWXoJkRM293YV9bKGuYU9TgqZoHyM+oEqWO5ruXCL/nCdw3cIBau+rSNA==
dependencies:
globby "11.0.0"
unixify "1.0.0"
"@graphql-toolkit/schema-merging@0.9.12":
version "0.9.12"
resolved "https://registry.yarnpkg.com/@graphql-toolkit/schema-merging/-/schema-merging-0.9.12.tgz#f058a7d256a4ed0e61e9874f90ae03326b81668e"
integrity sha512-ciqxLeMw7KPbJcq/xgnPbGyJGRO6bO1zQcdWCUSssyw8VDRHj5PFqEOAzT88eZQkEtg3qLN/wQEypeyFyNTHzw==
"@graphql-toolkit/schema-merging@0.10.4":
version "0.10.4"
resolved "https://registry.yarnpkg.com/@graphql-toolkit/schema-merging/-/schema-merging-0.10.4.tgz#2428590a531a33e9fe03be27cce9030f1c4c044b"
integrity sha512-naL6reYBuILLMrkMfKz0lOLL0kl6gGYnaaywnO/Dgp9F4NeAxDdAs5CV6Fy9NO5OzePFP58Dnc4sh2RyYrrFJg==
dependencies:
"@graphql-toolkit/common" "0.9.12"
"@graphql-toolkit/common" "0.10.4"
deepmerge "4.2.2"
graphql-tools-fork "9.0.1"
graphql-tools "5.0.0"
tslib "1.11.1"
"@hapi/address@2.x.x":
@ -2036,7 +2043,7 @@ apollo-link-context@~1.0.20:
apollo-link "^1.2.14"
tslib "^1.9.3"
apollo-link-http-common@^0.2.15, apollo-link-http-common@^0.2.16:
apollo-link-http-common@^0.2.14, apollo-link-http-common@^0.2.16:
version "0.2.16"
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz#756749dafc732792c8ca0923f9a40564b7c59ecc"
integrity sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==
@ -2054,7 +2061,7 @@ apollo-link-http@~1.5.17:
apollo-link-http-common "^0.2.16"
tslib "^1.9.3"
apollo-link@^1.0.0, apollo-link@^1.2.13, apollo-link@^1.2.14:
apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.14:
version "1.2.14"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.14.tgz#3feda4b47f9ebba7f4160bef8b977ba725b684d9"
integrity sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==
@ -2211,6 +2218,16 @@ apollo-tracing@^0.12.0:
apollo-server-env "^2.4.5"
apollo-server-plugin-base "^0.10.2"
apollo-upload-client@^13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/apollo-upload-client/-/apollo-upload-client-13.0.0.tgz#146d1ddd85d711fcac8ca97a72d3ca6787f2b71b"
integrity sha512-lJ9/bk1BH1lD15WhWRha2J3+LrXrPIX5LP5EwiOUHv8PCORp4EUrcujrA3rI5hZeZygrTX8bshcuMdpqpSrvtA==
dependencies:
"@babel/runtime" "^7.9.2"
apollo-link "^1.2.12"
apollo-link-http-common "^0.2.14"
extract-files "^8.0.0"
apollo-utilities@1.3.3, apollo-utilities@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.3.tgz#f1854715a7be80cd810bc3ac95df085815c0787c"
@ -3392,10 +3409,10 @@ data-urls@^1.1.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-fns@2.11.1:
version "2.11.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.11.1.tgz#197b8be1bbf5c5e6fe8bea817f0fe111820e7a12"
integrity sha512-3RdUoinZ43URd2MJcquzBbDQo+J87cSzB8NkXdZiN5ia1UNyep0oCyitfiL88+R7clGTeq/RniXAc16gWyAu1w==
date-fns@2.22.1:
version "2.22.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.22.1.tgz#1e5af959831ebb1d82992bf67b765052d8f0efc4"
integrity sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==
dateformat@^3.0.3:
version "3.0.3"
@ -4299,10 +4316,10 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
extract-files@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-7.0.0.tgz#3dc7853320ff7876ec62d6e98f2f4e6f3e6282f6"
integrity sha512-3AUlT7TD+DbQXNe3t70QrgJU6Wgcp7rk1Zm0vqWz8OYnw4vxihgG0TgZ2SIGrVqScc4WfOu7B4a0BezGJ0YqvQ==
extract-files@^8.0.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-8.1.0.tgz#46a0690d0fe77411a2e3804852adeaa65cd59288"
integrity sha512-PTGtfthZK79WUMk+avLmwx3NGdU8+iVFXC2NMGxKsn0MnihOG2lvumj+AZo8CTwTrwjXDgZ5tztbRlEdRjBonQ==
extsprintf@1.3.0:
version "1.3.0"
@ -4849,19 +4866,19 @@ graphql-tag@^2.9.2, graphql-tag@~2.10.3:
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.3.tgz#ea1baba5eb8fc6339e4c4cf049dabe522b0edf03"
integrity sha512-4FOv3ZKfA4WdOKJeHdz6B3F/vxBLSgmBcGeAFPf4n1F64ltJUvOOerNj0rsJxONQGdhUMynQIvd6LzB+1J5oKA==
graphql-tools-fork@9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/graphql-tools-fork/-/graphql-tools-fork-9.0.1.tgz#fc8df40c108bdba3268999dea355cc614c765038"
integrity sha512-kM6mUNVekgnWKtVqLGQ9HvQqQ3zZVPZRg1esltBoohsbUMaChl+9QkjBjoMxnZPnbTGOOGGagopNBQALIBysNg==
graphql-tools@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-5.0.0.tgz#67281c834a0e29f458adba8018f424816fa627e9"
integrity sha512-5zn3vtn//382b7G3Wzz3d5q/sh+f7tVrnxeuhTMTJ7pWJijNqLxH7VEzv8VwXCq19zAzHYEosFHfXiK7qzvk7w==
dependencies:
apollo-link "^1.2.13"
apollo-link-http-common "^0.2.15"
apollo-link "^1.2.14"
apollo-upload-client "^13.0.0"
deprecated-decorator "^0.1.6"
extract-files "^7.0.0"
form-data "^3.0.0"
iterall "^1.3.0"
node-fetch "^2.6.0"
uuid "^7.0.2"
tslib "^1.11.1"
uuid "^7.0.3"
graphql-tools@^4.0.0, graphql-tools@^4.0.4, graphql-tools@^4.0.5:
version "4.0.8"
@ -6606,13 +6623,13 @@ merge-descriptors@1.0.1:
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
merge-graphql-schemas@^1.7.7:
version "1.7.7"
resolved "https://registry.yarnpkg.com/merge-graphql-schemas/-/merge-graphql-schemas-1.7.7.tgz#673898f97c384d88fd66e49359b2f604e5864441"
integrity sha512-3mjFHXpsF+bnsnSy/FlZiWCcNo7AULuG8kEBE+YVX8qbzNrEPJVhSg7ANr5LjQZbGtSlTz7fa3Wqw0bfbQ5R5Q==
merge-graphql-schemas@^1.7.8:
version "1.7.8"
resolved "https://registry.yarnpkg.com/merge-graphql-schemas/-/merge-graphql-schemas-1.7.8.tgz#11a0a672a38a61d988c09ffdebe1bd4f8418de48"
integrity sha512-C3EJ1i86OjmbcCT524wVPRl17M5VZzgyh9kIGYAlYnAILX+7xfh8cCbMKfehh9n4opZg6CtcPogCiVZ6PB2NyQ==
dependencies:
"@graphql-toolkit/file-loading" "0.9.12"
"@graphql-toolkit/schema-merging" "0.9.12"
"@graphql-toolkit/file-loading" "0.10.4"
"@graphql-toolkit/schema-merging" "0.10.4"
tslib "1.11.1"
merge-stream@^2.0.0:
@ -6942,10 +6959,10 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
mustache@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.0.1.tgz#d99beb031701ad433338e7ea65e0489416c854a2"
integrity sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA==
mustache@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
mute-stream@0.0.8:
version "0.0.8"
@ -7816,10 +7833,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.0.tgz#8a03c7777883b29b37fb2c4348c66a78e980418b"
integrity sha512-yYerpkvseM4iKD/BXLYUkQV5aKt4tQPqaGW6EsZjzyu0r7sVZZNPJW4Y8MyKmicp6t42XUPcBVA+H6sB3gqndw==
prettier@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d"
integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==
pretty-format@^25.3.0:
version "25.3.0"
@ -9406,15 +9423,15 @@ ts-invariant@^0.4.0:
dependencies:
tslib "^1.9.3"
tslib@1.11.1, tslib@^1.9.0:
tslib@1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
tslib@^1.10.0, tslib@^1.9.3:
version "1.13.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
tslib@^1.10.0, tslib@^1.11.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tunnel-agent@^0.6.0:
version "0.6.0"
@ -9689,7 +9706,7 @@ uuid@^3.1.0, uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^7.0.2:
uuid@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==

View File

@ -13,16 +13,13 @@ $ docker-compose up
## Setup without docker
First, you have to tell cypress how to connect to your local neo4j database
among other things. You can copy our template configuration and change the new
file according to your needs.
To start the services that are required for cypress testing manually. You basically need the whole setup to run:
To start the services that are required for cypress testing, run:
- backend
- webapp
- neo4j
```bash
# in the top level folder Ocelot-Social/
$ yarn cypress:setup
```
Navigate to the corresponding folders and start the services.
## Install cypress
@ -35,21 +32,11 @@ without docker, you would have to install cypress and its dependencies first:
$ yarn install
```
## Run cypress
After verifying that there are no errors with the servers starting, open another tab in your terminal and run the following command:
```bash
$ yarn cypress:run
```
![Console output after running cypress test](../.gitbook/assets/grafik%20%281%29.png)
### Open Interactive Test Console
If you are like me, you might want to see some visual output. The interactive cypress environment also helps at debugging your tests, you can even time travel between individual steps and see the exact state of the app.
The interactive cypress test console allows to run tests and have visual feedback on that. The interactive cypress environment also helps at debugging the tests, you can even time travel between individual steps and see the exact state of the app.
To use this feature, instead of `yarn cypress:run` you would run the following command:
To use this feature run:
```bash
$ yarn cypress:open
@ -57,7 +44,19 @@ $ yarn cypress:open
![Interactive Cypress Environment](../.gitbook/assets/grafik-1%20%281%29.png)
## Run cypress
To run cypress without the user interface:
```bash
$ yarn cypress:run
```
This is used to run cypress in CI or in console
![Console output after running cypress test](../.gitbook/assets/grafik%20%281%29.png)
## Write some Tests
Check out the Cypress documentation for further information on how to write tests:
[https://docs.cypress.io/guides/getting-started/writing-your-first-test.html\#Write-a-simple-test](https://docs.cypress.io/guides/getting-started/writing-your-first-test.html#Write-a-simple-test)
[Write-a-simple-test](https://docs.cypress.io/guides/getting-started/writing-your-first-test.html#Write-a-simple-test)

View File

@ -1,2 +0,0 @@
// please change also version in file "webapp/constants/terms-and-conditions-version.js"
export const VERSION = '0.0.4'

View File

@ -1,8 +1,10 @@
{
"projectId": "qa7fe2",
"ignoreTestFiles": "*.js",
"chromeWebSecurity": false,
"baseUrl": "http://localhost:3000",
"env": {
"RETRIES": 2
"retries": {
"runMode": 2,
"openMode": 0
}
}

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