Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into feature/mark-all-notification-as-read

# Conflicts:
#	webapp/locales/it.json
This commit is contained in:
Wolfgang Huß 2022-11-16 11:45:19 +01:00
commit b398eabf47
900 changed files with 117752 additions and 29507 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

View File

@ -1,5 +1,7 @@
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
<!--
Please take a look at the issue templates at https://github.com/Human-Connection/Human-Connection/issues/new/choose
Please take a look at the issue templates at https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/new/choose
before submitting a new issue. Following one of the issue templates will ensure maintainers can route your request efficiently.
Thanks!

View File

@ -4,29 +4,7 @@ about: Create a report to help us improve
labels: bug
title: 🐛 [Bug]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## :bug: Bugreport
## 🐛 Bugreport
<!-- 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
Type: [] <!-- [Desktop|Smartphone] -->
- OS: [] <!-- [e.g. iOS8.1 or Windows] -->
- Browser: [] <!-- [e.g. stock browser, safari, chrome] -->
- Version [] <!-- [e.g. 22] -->
- Device: [] <!-- [e.g. iPhone6] -->
### Additional context
<!-- Add any other context about the problem here. -->

View File

@ -1,24 +1,10 @@
---
name: 💥 DevOps ticket
about: Help us manage our deployed App.
about: Help us manage our deployed Software.
labels: devops
title: 💥 [DevOps]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## :fire: 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.-->

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

@ -0,0 +1,13 @@
---
name: 🌟 Epic
about: Define a big development Step
labels: epic
title: 🌟 [EPIC]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
<!-- THIS ISSUE-TYPE IS NOT FOR YOU! -->
<!-- Proceed only if you know what you are doing - have a chat with Project's Team first -->
## 🌟 EPIC
<!-- Describe your Epic in detail. Include screenshots and drawings -->

View File

@ -4,21 +4,7 @@ about: Suggest an idea for this project
labels: feature
title: 🚀 [Feature]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## :rocket: Feature
## 🚀 Feature
<!-- 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,13 @@
---
name: 💬 Question
about: If you need help understanding HumanConnection.
about: If you need help understanding our Software.
labels: question
title: 💬 [Question]
---
<!-- Chat with Team HumanConnection -->
<!-- If you need an answer right away, visit the HumanConnection Discord:
https://discord.gg/Q3mpcgr -->
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## :speech_balloon: Question
<!-- Question the project's team -->
<!-- If you need an answer right away, consider to take other means of communication with the project's team -->
## 💬 Question
<!-- Describe your Question in detail. Include screenshots and drawings if needed. -->

View File

@ -0,0 +1,10 @@
---
name: 🔧 Refactor ticket
about: Help us improve our code by refactoring it.
labels: refactor
title: 🔧 [Refactor]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## 🔧 Refactor 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.-->

View File

@ -1,20 +0,0 @@
---
name: 🔧 Refactor ticket
about: Help us improve our code by refactoring it.
labels: refactor
title: 🔧 [Refactor]
---
## :zap: Refactor 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
<!-- 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.-->

13
.github/ISSUE_TEMPLATE/release.md vendored Normal file
View File

@ -0,0 +1,13 @@
---
name: 🎂 Release
about: Define a Release
labels: release
title: 🎂 [RELEASE]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
<!-- THIS ISSUE-TYPE IS NOT FOR YOU! -->
<!-- Proceed only if you know what you are doing - have a chat with Project's Team first -->
## 🎂 RELEASE
<!-- Describe your Release in detail. Include screenshots and drawings -->

View File

@ -1,3 +1,5 @@
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## 🍰 Pullrequest
<!-- Describe the Pullrequest. Use Screenshots if possible. -->

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

@ -1,2 +0,0 @@
# Always validate the PR title, and ignore the commits
titleOnly: true

View File

@ -1,18 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 30
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- bounty
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

71
.github/workflows/lint_pr.yml vendored Normal file
View File

@ -0,0 +1,71 @@
name: "ocelot.social lint pull request CI"
on:
pull_request:
pull_request_target:
types:
- opened
- edited
- synchronize
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Configure which types are allowed (newline delimited).
# Default: https://github.com/commitizen/conventional-commit-types
#types: |
# fix
# feat
# Configure which scopes are allowed (newline delimited).
scopes: |
backend
webapp
database
release
other
# Configure that a scope must always be provided.
requireScope: true
# Configure which scopes (newline delimited) are disallowed in PR
# titles. For instance by setting # the value below, `chore(release):
# ...` and `ci(e2e,release): ...` will be rejected.
#disallowScopes: |
# release
# Configure additional validation for the subject based on a regex.
# This example ensures the subject doesn't start with an uppercase character.
subjectPattern: ^(?![A-Z]).+$
# If `subjectPattern` is configured, you can use this property to override
# the default error message that is shown when the pattern doesn't match.
# The variables `subject` and `title` can be used within the message.
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}"
didn't match the configured pattern. Please ensure that the subject
doesn't start with an uppercase character.
# If you use GitHub Enterprise, you can set this to the URL of your server
#githubBaseUrl: https://github.myorg.com/api/v3
# If the PR contains one of these labels (newline delimited), the
# validation is skipped.
# If you want to rerun the validation when labels change, you might want
# to use the `labeled` and `unlabeled` event triggers in your workflow.
#ignoreLabels: |
# bot
# ignore-semantic-pull-request
# If you're using a format for the PR title that differs from the traditional Conventional
# Commits spec, you can use these options to customize the parsing of the type, scope and
# subject. The `headerPattern` should contain a regex where the capturing groups in parentheses
# correspond to the parts listed in `headerPatternCorrespondence`.
# See: https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#headerpattern
headerPattern: '^(\w*)(?:\(([\w$.\-*/ ]*)\))?: (.*)$'
headerPatternCorrespondence: type, scope, subject
# For work-in-progress PRs you can typically use draft pull requests
# from GitHub. However, private repositories on the free plan don't have
# this option and therefore this action allows you to opt-in to using the
# special "[WIP]" prefix to indicate this state. This will avoid the
# validation of the PR title and the pull request checks remain pending.
# Note that a second check will be reported if this is enabled.
wip: true

393
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,393 @@
name: ocelot.social publish CI
on:
push:
branches:
- master
# - 5059-epic-groups # for testing while developing
# template branches in repo
# - template--separate-branch-auto-deployment--5059-epic-groups
jobs:
##############################################################################
# JOB: PREPARE ###############################################################
##############################################################################
prepare:
name: Prepare
runs-on: ubuntu-latest
# needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# TODO: DO STUFF ??? #####################################################
##########################################################################
- name: Check translation files
run: |
scripts/translations/sort.sh
scripts/translations/missing-keys.sh
##############################################################################
# JOB: DOCKER BUILD COMMUNITY NEO4J ##########################################
##############################################################################
build_production_neo4j:
name: Docker Build Production - Neo4J
runs-on: ubuntu-latest
needs: [prepare]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# SET ENVS ###############################################################
##########################################################################
- name: ENV - VERSION
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- 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
- 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:latest" -t "ocelotsocialnetwork/neo4j-community:${VERSION}" -t "ocelotsocialnetwork/neo4j-community:${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-community" > /tmp/neo4j.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: docker-neo4j-community
path: /tmp/neo4j.tar
##############################################################################
# JOB: DOCKER BUILD PRODUCTION BACKEND #######################################
##############################################################################
build_production_backend:
name: Docker Build Production - Backend
runs-on: ubuntu-latest
needs: [prepare]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# SET ENVS ###############################################################
##########################################################################
- name: ENV - VERSION
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- 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
- name: ENV - BUILD_COMMIT
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
##########################################################################
# BUILD BACKEND DOCKER IMAGE (production) ################################
##########################################################################
- name: Backend | Build `production` image
run: |
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:
name: docker-backend-production
path: /tmp/backend.tar
##############################################################################
# JOB: DOCKER BUILD PRODUCTION WEBAPP ########################################
##############################################################################
build_production_webapp:
name: Docker Build Production - WebApp
runs-on: ubuntu-latest
needs: [prepare]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# SET ENVS ###############################################################
##########################################################################
- name: ENV - VERSION
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- 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
- name: ENV - BUILD_COMMIT
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
##########################################################################
# BUILD WEBAPP DOCKER IMAGE (build) ######################################
##########################################################################
- name: Webapp | Build `production` image
run: |
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:
name: docker-webapp-production
path: /tmp/webapp.tar
##############################################################################
# JOB: DOCKER BUILD PRODUCTION MAINTENANCE ###################################
##############################################################################
build_production_maintenance:
name: Docker Build Production - Maintenance
runs-on: ubuntu-latest
needs: [prepare]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# SET ENVS ###############################################################
##########################################################################
- name: ENV - VERSION
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- 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
- name: ENV - BUILD_COMMIT
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
##########################################################################
# BUILD MAINTENANCE DOCKER IMAGE (build) #################################
##########################################################################
- name: Maintenance | Build `production` image
run: |
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:
name: docker-maintenance-production
path: /tmp/maintenance.tar
##############################################################################
# JOB: UPLOAD TO DOCKERHUB ###################################################
##############################################################################
upload_to_dockerhub:
name: Upload to Dockerhub
runs-on: ubuntu-latest
needs: [build_production_neo4j,build_production_backend,build_production_webapp,build_production_maintenance]
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
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-community
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-production
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-production
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/webapp.tar
- name: Download Docker Image (Maintenance)
uses: actions/download-artifact@v2
with:
name: docker-maintenance-production
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/maintenance.tar
##########################################################################
# Upload #################################################################
##########################################################################
- name: login to dockerhub
run: echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
- name: Push neo4j
run: docker push --all-tags ocelotsocialnetwork/neo4j-community
- name: Push backend
run: docker push --all-tags ocelotsocialnetwork/backend
- name: Push webapp
run: docker push --all-tags ocelotsocialnetwork/webapp
- name: Push maintenance
run: docker push --all-tags ocelotsocialnetwork/maintenance
##############################################################################
# JOB: KUBERNETES DEPLOY ACTUAL/LATEST VERSION ######################################
##############################################################################
kubernetes_deploy:
# see example https://github.com/do-community/example-doctl-action
# see example https://github.com/do-community/example-doctl-action/blob/main/.github/workflows/workflow.yaml
name: Kubernetes deploy of latest version to stage.ocelot.social cluster at DigitalOcean
runs-on: ubuntu-latest
needs: [upload_to_dockerhub]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# SET ENVS ###############################################################
##########################################################################
- name: ENV - VERSION
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- name: ENV - BUILD_VERSION
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
##########################################################################
# Install DigitalOceans doctl and set kubeconfig #########################
##########################################################################
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Save DigitalOcean kubeconfig with short-lived credentials
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 cluster-stage-ocelot-social
##########################################################################
# Deploy new Docker images to DigitalOcean Kubernetes cluster ############
##########################################################################
# - name: Deploy 'latest' to DigitalOcean Kubernetes
# run: |
# kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:latest
# kubectl -n default rollout restart deployment/ocelot-webapp
# kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:latest
# kubectl -n default rollout restart deployment/ocelot-backend
# kubectl -n default set image deployment/ocelot-maintenance container-ocelot-maintenance=ocelotsocialnetwork/maintenance:latest
# kubectl -n default rollout restart deployment/ocelot-maintenance
# kubectl -n default set image deployment/ocelot-neo4j container-ocelot-neo4j=ocelotsocialnetwork/neo4j-community:latest
# kubectl -n default rollout restart deployment/ocelot-neo4j
- name: Deploy actual version '$BUILD_VERSION' to DigitalOcean Kubernetes
run: |
kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:$BUILD_VERSION
kubectl -n default rollout restart deployment/ocelot-webapp
kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:$BUILD_VERSION
kubectl -n default rollout restart deployment/ocelot-backend
kubectl -n default set image deployment/ocelot-maintenance container-ocelot-maintenance=ocelotsocialnetwork/maintenance:$BUILD_VERSION
kubectl -n default rollout restart deployment/ocelot-maintenance
kubectl -n default set image deployment/ocelot-neo4j container-ocelot-neo4j=ocelotsocialnetwork/neo4j-community:$BUILD_VERSION
kubectl -n default rollout restart deployment/ocelot-neo4j
# because this step 'kubectl -n default rollout status deployment/* --timeout=600s' does not work as expected
# and we need the pods to be up again for cleaning and seeding the Neo4j database and the backend.
# !!! this is not a perfect solution !!!
# deployments are regularly up again after 3 minutes and 10 seconds
- name: Sleep for 4 minutes, means 240 seconds
run: sleep 240s
shell: bash
- name: Verify deployment and wait for the pods of each deployment to get ready for cleaning and seeding of the database
run: |
kubectl -n default rollout status deployment/ocelot-backend --timeout=600s
kubectl -n default rollout status deployment/ocelot-neo4j --timeout=600s
kubectl -n default rollout status deployment/ocelot-maintenance --timeout=600s
kubectl -n default rollout status deployment/ocelot-webapp --timeout=600s
- name: Run migrations for Neo4j database via backend for staging
run: |
kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "yarn prod:migrate up"
- name: Reset and seed Neo4j database via backend for staging
# db cleaning and seeding is only possible in production if env 'PRODUCTION_DB_CLEAN_ALLOW=true' is set in deployment
run: |
kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "node --experimental-repl-await dist/db/clean.js && node --experimental-repl-await dist/db/seed.js"
##############################################################################
# JOB: GITHUB TAG LATEST VERSION #############################################
##############################################################################
github_tag:
name: Tag latest version on Github
runs-on: ubuntu-latest
needs: [upload_to_dockerhub]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0 # Fetch full History for changelog
##########################################################################
# SET ENVS ###############################################################
##########################################################################
- name: ENV - VERSION
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- 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
- name: ENV - BUILD_COMMIT
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
##########################################################################
# Push version tag to GitHub #############################################
##########################################################################
# TODO: this will error on duplicate
#- name: package-version-to-git-tag
# uses: pkgdeps/git-tag-action@v2
# with:
# github_token: ${{ secrets.GITHUB_TOKEN }}
# github_repo: ${{ github.repository }}
# version: ${{ env.VERSION }}
# git_commit_sha: ${{ github.sha }}
# git_tag_prefix: "v"
##########################################################################
# Push build tag to GitHub ###############################################
##########################################################################
- name: package-version-to-git-tag + build number
uses: pkgdeps/git-tag-action@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
github_repo: ${{ github.repository }}
version: ${{ env.BUILD_VERSION }}
git_commit_sha: ${{ github.sha }}
git_tag_prefix: "b"
##########################################################################
# Push release tag to GitHub #############################################
##########################################################################
- name: yarn install
run: yarn install
- name: generate changelog
run: yarn auto-changelog --latest-version ${{ env.VERSION }} --unreleased-only
- name: package-version-to-git-release
continue-on-error: true # Will fail if tag exists
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ env.VERSION }}
release_name: ${{ env.VERSION }}
body_path: ./CHANGELOG.md
draft: false
prerelease: false

343
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,343 @@
name: ocelot.social test CI
on: [push]
jobs:
##############################################################################
# JOB: PREPARE #####################################################
##############################################################################
prepare:
name: Prepare
runs-on: ubuntu-latest
# needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# TODO: DO STUFF ??? #####################################################
##########################################################################
- name: Check translation files
run: |
scripts/translations/sort.sh
scripts/translations/missing-keys.sh
##############################################################################
# JOB: DOCKER BUILD TEST NEO4J ###############################################
##############################################################################
build_test_neo4j:
name: Docker Build Test - Neo4J
runs-on: ubuntu-latest
needs: [prepare]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# NEO4J ##################################################################
##########################################################################
- name: Neo4J | Build `community` image
run: |
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/neo4j.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: docker-neo4j-image
path: /tmp/neo4j.tar
##############################################################################
# JOB: DOCKER BUILD TEST BACKEND #############################################
##############################################################################
build_test_backend:
name: Docker Build Test - Backend
runs-on: ubuntu-latest
needs: [prepare]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# BUILD BACKEND DOCKER IMAGE (build) #####################################
##########################################################################
- name: backend | Build `test` image
run: |
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
docker save "ocelotsocialnetwork/backend:test" > /tmp/backend.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: docker-backend-test
path: /tmp/backend.tar
##############################################################################
# JOB: DOCKER BUILD TEST WEBAPP ##############################################
##############################################################################
build_test_webapp:
name: Docker Build Test - WebApp
runs-on: ubuntu-latest
needs: [prepare]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# BUILD WEBAPP DOCKER IMAGE (build) ######################################
##########################################################################
- name: webapp | Build `test` image
run: |
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: docker-webapp-test
path: /tmp/webapp.tar
##############################################################################
# JOB: LINT BACKEND ##########################################################
##############################################################################
lint_backend:
name: Lint backend
runs-on: ubuntu-latest
needs: [build_test_backend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- 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
##########################################################################
# LINT BACKEND ###########################################################
##########################################################################
- name: backend | Lint
run: docker run --rm ocelotsocialnetwork/backend:test yarn run lint
##############################################################################
# JOB: LINT WEBAPP ###########################################################
##############################################################################
lint_webapp:
name: Lint webapp
runs-on: ubuntu-latest
needs: [build_test_webapp]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- 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
##########################################################################
# LINT WEBAPP ############################################################
##########################################################################
- name: webapp | Lint
run: docker run --rm ocelotsocialnetwork/webapp:test yarn run lint
##############################################################################
# JOB: UNIT TEST BACKEND #####################################################
##############################################################################
unit_test_backend:
name: Unit tests - backend
runs-on: ubuntu-latest
needs: [build_test_neo4j,build_test_backend]
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
##########################################################################
# UNIT TESTS BACKEND #####################################################
##########################################################################
- name: backend | 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 neo4j backend
- name: backend | Initialize Database
run: docker-compose exec -T backend yarn db:migrate init
- name: backend | Migrate Database Up
run: docker-compose exec -T backend yarn db:migrate up
- 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: 57
token: ${{ github.token }}
##############################################################################
# JOB: UNIT TEST WEBAPP ######################################################
##############################################################################
unit_test_webapp:
name: Unit tests - webapp
runs-on: ubuntu-latest
needs: [build_test_webapp]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- 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
##########################################################################
# UNIT TESTS WEBAPP ######################################################
##########################################################################
- name: backend | 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
- name: webapp | Unit tests
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: 63
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/

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v17.9.0

19162
CHANGELOG.md

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at developer@human-connection.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at devops@ocelot.social. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

View File

@ -1,74 +1,87 @@
# CONTRIBUTING
Thank you so much for thinking of contributing to the Human Connection project! It's awesome you're here, we really appreciate it. :-\)
Thank you so much for thinking of contributing to the [ocelot.social](https://ocelot.social) project! It's awesome you're here, we really appreciate it. :-\)
## 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](/SUMMARY.md).
To get you started we recommend that you join forces with a regular contributor. Please join [our discord instance](https://human-connection.org/discord) to chat with developers or just get in touch directly on an issue on either [Github](https://github.com/Human-Connection/Human-Connection/issues) or [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f/boards?repos=152252353):
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 can have pair programming sessions for you! 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.
## Development Flow
We operate in two week sprints that are planned, estimated and prioritised on [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f). All issues are also linked to and synced with [Github](https://github.com/Human-Connection/Human-Connection/issues). Look for the `good first issue` label if you're not sure where to start!
We 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](https://discord.gg/AJSX9DCSUA) 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.)
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 @Ocelot-Social-Community/core-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).
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 @Ocelot-Social-Community/core-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)
* pass all tests (linter, backend, webapp, code coverage, end-to-end)
* be approved by at least 1 developer who is not the owner of the PR (when more than 10 files were changed it needs 2 approvals)
## Contribution Flow For Open Source Contributors
See [contributing in main README.md](/README.md#contributing)
## The Team
There are many volunteers all around the world helping us build this network and without their contributions we wouldn't be where we are today. Big thank you to all of you!
You can see the core team behind Human Connection [on our website](https://human-connection.org/en/the-team/). On Github you will mostly run into our developers:
* Robert (@roschaefer)
* Matt (@mattwr18)
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.
Times below refer to **German Time** that's CET (GMT+1) in winter and CEST (GMT+2) in summer because most ocelot.social Community 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,34 +89,39 @@ 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)
* we select and prioritise the issues we will work on in the following two weeks
Sprint retrospective
* bi-weekly on Monday 13:00
* via this [zoom link](https://zoom.us/j/7743582385)
* all contributors welcome (most interesting for those who participated in the sprint)
* we review the past sprint and talk about what went well and what we could improve
-->
## Philosophy
We practise [collective code ownership](http://www.extremeprogramming.org/rules/collective.html) rather than strong code ownership, which means that:
* developers can make contributions to other people's PRs (after checking in with them)
* we avoid blocking because someone else isn't working, so we sometimes take over PRs from other developers
* everyone should always push their code to branches so others can see it
We believe in open source contributions as a learning experience everyone is welcome to join our team of volunteers and to contribute to the project, no matter their background or level of experience.
We believe in open source contributions as a learning experience everyone is welcome to join our team of volunteers and to contribute to the project, no matter their background or level of experience. To support your learning experience we founded the charity association [busFaktor() e.V.](https://www.busfaktor.org/en).
We use pair programming sessions as a tool for knowledge sharing. We can learn a lot from each other and only by sharing what we know and overcoming challenges together can we grow as a team and truly own this project collectively.
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
* You contribute to a good cause
@ -118,9 +136,9 @@ 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/Human-Connection/Human-Connection/issues?q=is%3Aopen+is%3Aissue+label%3Abounty). These issues should have a second label `€<amount>`
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.
You can bill us after your pull request got approved and merged into `master`.
@ -130,3 +148,4 @@ us your invoice as .pdf file attached to an E-Mail once you are done.
Our Open-Source bounty program is a work-in-progress. Based on our future
experience we will make changes and improvements. So keep an eye on this
contribution guide.
-->

86
DOCKER_MORE_CLOSELY.md Normal file
View File

@ -0,0 +1,86 @@
# Docker More Closely
## Apple M1 Platform
***Attention:** For using Docker commands in Apple M1 environments!*
### Enviroment Variable For Apple M1 Platform
To set the Docker platform environment variable in your terminal tab, run:
```bash
# set env variable for your shell
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64
```
### Docker Compose Override File For Apple M1 Platform
For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform:
```bash
# in main folder
# for development
$ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up
# only once: init admin user and create indexes and contraints in Neo4j database
$ docker compose exec backend yarn prod:migrate init
# clean db
$ docker compose exec backend yarn db:reset
# seed db
$ docker compose exec backend yarn db:seed
# for production
$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up
# only once: init admin user and create indexes and contraints in Neo4j database
$ docker compose exec backend /bin/sh -c "yarn prod:migrate init"
```
## Analysing Docker Builds
To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it!
The `dive build` command is exactly the right one to fulfill what we are looking for.
We can use it just like the `docker build` command and get an analysis afterwards.
So, in our main folder, we use it in the following way:
```bash
# in main folder
$ dive build --target <layer-name> -t "ocelotsocialnetwork/<app-name>:local-<layer-name>" --build-arg BBUILD_DATE="<build-date>" --build-arg BBUILD_VERSION="<build-version>" --build-arg BBUILD_COMMIT="<build-commit>" <app-folder-name-or-dot>/
```
The build arguments are optional.
For the specific applications, we use them as follows.
### Backend
#### Production For Backend
```bash
# in main folder
$ dive build --target production -t "ocelotsocialnetwork/backend:local-production" backend/
```
#### Development For Backend
```bash
# in main folder
$ dive build --target development -t "ocelotsocialnetwork/backend:local-development" backend/
```
### Webapp
#### Production For Webapp
```bash
# in main folder
$ dive build --target production -t "ocelotsocialnetwork/webapp:local-production" webapp/
```
#### Development For Webapp
```bash
# in main folder
$ dive build --target development -t "ocelotsocialnetwork/webapp:local-development" webapp/
```

View File

@ -2,11 +2,10 @@
MIT License
Copyright \(c\) 2018 Human-Connection gGmbH
Copyright \(c\) 2018-2021 [Ocelot.Social Community](https://github.com/Ocelot-Social-Community)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files \(the "Software"\), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

222
README.md
View File

@ -1,33 +1,82 @@
# Human-Connection
# Ocelot.Social
[![Build Status](https://travis-ci.com/Human-Connection/Human-Connection.svg?branch=master)](https://travis-ci.com/Human-Connection/Human-Connection)
[![Codecov Coverage](https://img.shields.io/codecov/c/github/Human-Connection/Human-Connection/master.svg?style=flat-square)](https://codecov.io/gh/Human-Connection/Human-Connection/)
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitro-Backend/blob/backend/LICENSE.md)
[![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discordapp.com/invite/DFSjPaX)
[![Open Source Helpers](https://www.codetriage.com/human-connection/human-connection/badges/users.svg)](https://www.codetriage.com/human-connection/human-connection)
[![Build Status Test](https://github.com/Ocelot-Social-Community/Ocelot-Social/actions/workflows/test.yml/badge.svg)](https://github.com/Ocelot-Social-Community/Ocelot-Social/actions)
[![Build Status Publish](https://github.com/Ocelot-Social-Community/Ocelot-Social/actions/workflows/publish.yml/badge.svg)](https://github.com/Ocelot-Social-Community/Ocelot-Social/actions)
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/LICENSE.md)
[![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/AJSX9DCSUA)
[![Open Source Helpers](https://www.codetriage.com/ocelot-social-community/ocelot-social/badges/users.svg)](https://www.codetriage.com/ocelot-social-community/ocelot-social)
Human Connection is a nonprofit social, action and knowledge network that connects information to action and promotes positive local and global change in all areas of life.
[ocelot.social](https://ocelot.social) is free and open source software program code to run social networks. Its development is supported by a community of programmers and interested network operators.
* **Social**: Interact with other people not just by commenting their posts, but by providing **Pro & Contra** arguments, give a **Versus** or ask them by integrated **Chat** or **Let's Talk**
* **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**.
<p align="center">
<a href="https://ocelot.social" target="_blank"><img src="webapp/static/img/custom/logo-squared.svg" alt="ocelot.social" width="40%" height="40%"></a>
</p>
[![Human-Connection](.gitbook/assets/lets_get_together.png)](https://human-connection.org)
Our goal is to enable people to participate fairly and equally in online social networks. The equality of opportunity applies both to the fundamental equality of all people and to the possibility of letting their diverse voices be heard.
**Technology Stack**
We therefore consider it desirable that operators offer such networks so that people can choose where they want to be on the move.
* [VueJS](https://vuejs.org/)
* [NuxtJS](https://nuxtjs.org/)
* [GraphQL](https://graphql.org/)
* [NodeJS](https://nodejs.org/en/)
* [Neo4J](https://neo4j.com/)
At the same time, it should be possible in the future to link these networks with each other (ActivityPub, Fediverse), so that users can also connect with people from other networks - for example by making friends or following posts or other contributions.
In other words, we are interested in a network of networks and in keeping the data as close as possible to the user and the operator they trusts.
## Live demo
## Introduction
Try out our deployed [development environment](https://develop.human-connection.org/).
Have a look into our short video:
[ocelot.social - GitHub - Developer Welcome - Tutorial (english)](https://www.youtube.com/watch?v=gZSL6KvBIiY&list=PLFMD5liPP01kbuReHxYXxv_1fI5rIgS1f&index=1)
Logins:
## Directory Layout
There are three 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
* [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
setup **frontend** and **backend**.
There are two approaches:
1. [Local](#local-installation) installation, which means you have to take care of dependencies yourself.
2. **Or** Install everything through [Docker](#docker-installation) which takes care of dependencies for you.
## 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:Ocelot-Social-Community/Ocelot-Social.git
```
Change into the new folder.
```bash
$ cd Ocelot-Social
```
## Live Demo And Developer Logins
**Try out our deployed [development environment](https://stage.ocelot.social).**
Visit our staging networks:
* central staging network: [stage.ocelot.social](https://stage.ocelot.social)
<!-- - rebranded staging network: [rebrand.ocelot.social](https://stage.ocelot.social). -->
### Login
Logins for the live demos and developers (local developers after the following installations) in the browser:
| email | password | role |
| :--- | :--- | :--- |
@ -35,27 +84,133 @@ Logins:
| `moderator@example.org` | 1234 | moderator |
| `admin@example.org` | 1234 | admin |
## Documentation
### Docker Installation
Learn how to set up a local development environment in our [Docs](https://docs.human-connection.org/human-connection/) :mag_right:
Docker is a software development container tool that combines software and its dependencies into one standardized unit that contains everything needed to run it. This helps us to avoid problems with dependencies and makes installation easier.
## Translations
#### General Installation of Docker
You can help translating the interface by joining us on [lokalise.co](https://lokalise.co/public/556252725c18dd752dd546.13222042/).
Thank you lokalise for providing us with a premium account :raised_hands:.
There are [several ways to install Docker CE](https://docs.docker.com/install/) on your computer or server.
## Developer Chat
* [install Docker Desktop on macOS](https://docs.docker.com/docker-for-mac/install/)
* [install Docker Desktop on Windows](https://docs.docker.com/docker-for-windows/install/)
* [install Docker CE on Linux](https://docs.docker.com/install/)
Join our friendly open-source community on [Discord](https://discordapp.com/invite/DFSjPaX) :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!
Check the correct Docker installation by checking the version before proceeding. E.g. we have the following versions:
[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/0)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/0)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/1)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/1)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/2)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/2)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/3)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/3)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/4)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/4)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/5)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/5)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/6)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/6)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/7)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/7)
```bash
$ docker --version
Docker version 18.09.2
$ docker-compose --version
docker-compose version 1.23.2
```
## Open-Source Bounties
#### Start Ocelot-Social via Docker-Compose
You can get a small financial compensation for your contribution :moneybag: See
details in our [Contribution Guidelines](./CONTRIBUTING.md#open-source-bounties).
Prepare ENVs once beforehand:
```bash
# in folder webapp/
$ cp .env.template .env
# in folder backend/
$ cp .env.template .env
```
For Development:
```bash
# in main folder
$ docker-compose up
```
For Production:
```bash
# in main folder
$ docker-compose -f docker-compose.yml up
```
This will start all required Docker containers.
Make sure your database is running on `http://localhost:7474/browser/`.
Prepare database once before you start by running the following command in a second terminal:
```bash
# in main folder while docker-compose is up
$ docker-compose exec backend yarn run db:migrate init
```
Then clear and seed database by running the following command as well in the second terminal:
```bash
# in main folder while docker-compose is up
$ docker-compose exec backend yarn run db:reset
$ docker-compose exec backend yarn run db:seed
```
For a closer description see [backend README.md](./backend/README.md).
For a full documentation see [SUMMARY](./SUMMARY.md).
### Local Installation
For a full documentation see [SUMMARY](./SUMMARY.md).
## Contributing
Choose an issue (consider our label [good-first-issue](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)) and leave a comment there. We will then invite you to join our volunteers team.
To have the necessary permission to push directly to this repository, please accept our invitation to join our volunteers team, you will receive via the email, Github will send you, once invited. If we did not invite you yet, please request an invitation via Discord.
We are happy if you fork our repository, but we don't recommend it for development. You do not need a fork.
Clone this repository locally as [described above](#clone-the-repository), create your branch named `<issue-number>-<description>`, add your code and push your branch to this repository. Then create a PR by comparing it to our `master`.
Please run the following commands before you push:
```bash
# in folder backend/
$ yarn lint --fix
$ yarn test
```
```bash
# in folder webapp/
$ yarn lint --fix
$ yarn locales --fix
$ yarn test
```
Check out our [contribution guideline](./CONTRIBUTING.md), too!
### Developer Chat
Join our friendly open-source community on [Discord](https://discord.gg/AJSX9DCSUA) :heart_eyes_cat:
Just introduce yourself at `#introduce-yourself` and mention a mentor or `@@Mentors` to get you onboard :neckbeard:
We give write permissions to every developer who asks for it. Just text us on
[Discord](https://discord.gg/AJSX9DCSUA).
## 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 for development purposes as described above is `docker-compose`.
## Technology Stack
* [VueJS](https://vuejs.org/)
* [NuxtJS](https://nuxtjs.org/)
* [GraphQL](https://graphql.org/)
* [NodeJS](https://nodejs.org/en/)
* [Neo4J](https://neo4j.com/)
### For Testing
* [Cypress](https://docs.cypress.io/)
* [Storybook](https://storybook.js.org/)
* [Jest](https://jestjs.io/)
* [Vue Test Utils](https://vue-test-utils.vuejs.org/)
* [ESLint](https://eslint.org/)
## Attributions
@ -66,4 +221,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

@ -2,7 +2,6 @@
* [Introduction](README.md)
* [Edit this Documentation](edit-this-documentation.md)
* [Installation](installation.md)
* [Neo4J](neo4j/README.md)
* [Backend](backend/README.md)
* [GraphQL](backend/graphql.md)
@ -16,24 +15,9 @@
* [End-to-end tests](cypress/README.md)
* [Frontend tests](webapp/testing.md)
* [Backend tests](backend/testing.md)
* [Docker More Closely](DOCKER_MORE_CLOSELY.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)
* [Human Connection](deployment/human-connection/README.md)
* [Error Reporting](deployment/human-connection/error-reporting/README.md)
* [Mailserver](deployment/human-connection/mailserver/README.md)
* [Maintenance](deployment/human-connection/maintenance/README.md)
* [Volumes](deployment/volumes/README.md)
* [Neo4J Offline-Backups](deployment/volumes/neo4j-offline-backup/README.md)
* [Neo4J Online-Backups](deployment/volumes/neo4j-online-backup/README.md)
* [Volume Snapshots](deployment/volumes/volume-snapshots/README.md)
* [Reclaim Policy](deployment/volumes/reclaim-policy/README.md)
* [Velero](deployment/volumes/velero/README.md)
* [Metrics](deployment/monitoring/README.md)
* [Legacy Migration](deployment/legacy-migration/README.md)
* [Feature Specification](cypress/features.md)
* [Code of conduct](CODE_OF_CONDUCT.md)
* [License](LICENSE.md)

View File

@ -10,16 +10,23 @@ SMTP_USERNAME=
SMTP_PASSWORD=
JWT_SECRET="b/&&7b78BF&fv/Vd"
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"
JWT_EXPIRES="2y"
MAPBOX_TOKEN="pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g"
PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78"
SENTRY_DSN_BACKEND=
COMMIT=
PUBLIC_REGISTRATION=false
INVITE_REGISTRATION=true
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_ENDPOINT=
AWS_REGION=
AWS_BUCKET=
EMAIL_DEFAULT_SENDER="devops@ocelot.social"
EMAIL_SUPPORT="devops@ocelot.social"
CATEGORIES_ACTIVE=false

1
backend/.nvmrc Normal file
View File

@ -0,0 +1 @@
v12.19.0

View File

@ -1,28 +1,103 @@
##################################################################################
# BASE (Is pushed to DockerHub for rebranding) ###################################
##################################################################################
FROM node:12.19.0-alpine3.10 as base
LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
EXPOSE 4000
CMD ["yarn", "run", "start"]
ARG BUILD_COMMIT
ENV BUILD_COMMIT=$BUILD_COMMIT
ARG WORKDIR=/develop-backend
RUN mkdir -p $WORKDIR
WORKDIR $WORKDIR
# 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
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
ARG BBUILD_COMMIT="0000000"
ENV BUILD_COMMIT=$BBUILD_COMMIT
## SET NODE_ENV
ENV NODE_ENV="production"
## App relevant Envs
ENV PORT="4000"
# Labels
LABEL org.label-schema.build-date="${BUILD_DATE}"
LABEL org.label-schema.name="ocelot.social:backend"
LABEL org.label-schema.description="Backend of the Social Network Software ocelot.social"
LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md"
LABEL org.label-schema.url="https://ocelot.social"
LABEL org.label-schema.vcs-url="https://github.com/Ocelot-Social-Community/Ocelot-Social/tree/master/backend"
LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}"
LABEL org.label-schema.vendor="ocelot.social Community"
LABEL org.label-schema.version="${BUILD_VERSION}"
LABEL org.label-schema.schema-version="1.0"
LABEL maintainer="devops@ocelot.social"
# Install Additional Software
## install: git
RUN apk --no-cache add git
COPY package.json yarn.lock ./
COPY .env.template .env
# Settings
## Expose Container Port
EXPOSE ${PORT}
FROM base as build-and-test
RUN yarn install --production=false --frozen-lockfile --non-interactive
## Workdir
RUN mkdir -p ${DOCKER_WORKDIR}
WORKDIR ${DOCKER_WORKDIR}
##################################################################################
# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
##################################################################################
FROM base as development
# We don't need to copy or build anything since we gonna bind to the
# local filesystem which will need a rebuild anyway
# Run command
# (for development we need to execute yarn install since the
# node_modules are on another volume and need updating)
CMD /bin/sh -c "yarn install && yarn run dev"
##################################################################################
# CODE (Does contain all code files and is pushed to DockerHub for rebranding) ###
##################################################################################
FROM base as code
# copy everything, but do not build.
COPY . .
RUN NODE_ENV=production yarn run build
# reduce image size with a multistage build
##################################################################################
# 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
RUN yarn run build
##################################################################################
# TEST ###########################################################################
##################################################################################
FROM build as test
# Run command
CMD /bin/sh -c "yarn run dev"
##################################################################################
# PRODUCTION (Does contain only "binary"- and static-files to reduce image size) #
##################################################################################
FROM base as production
ENV NODE_ENV=production
COPY --from=build-and-test /develop-backend/dist ./dist
COPY ./public/img/ ./public/img/
COPY ./public/providers.json ./public/providers.json
RUN yarn install --production=true --frozen-lockfile --non-interactive --no-cache
# 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/
COPY --from=build ${DOCKER_WORKDIR}/public/providers.json ./public/providers.json
# Copy package.json for script definitions (lock file should not be needed)
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
# Run command
CMD /bin/sh -c "yarn run start"

View File

@ -7,8 +7,9 @@ Run the following command to install everything through docker.
The installation takes a bit longer on the first pass or on rebuild ...
```bash
# in main folder
$ docker-compose up
# or
# rebuild the containers for a cleanup
$ docker-compose up --build
```
@ -17,31 +18,44 @@ Wait a little until your backend is up and running at [http://localhost:4000/](h
## Installation without Docker
For the local installation you need a recent version of [node](https://nodejs.org/en/)
(&gt;= `v10.12.0`).
For the local installation you need a recent version of
[node](https://nodejs.org/en/) (&gt;= `v10.12.0`). We are using
`12.19.0` and therefore we recommend to use the same version
([see](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4082)
some known problems with more recent node versions). You can use the
[node version manager](https://github.com/nvm-sh/nvm) to switch
between different local node versions.
Install node dependencies with [yarn](https://yarnpkg.com/en/):
```bash
# in main folder
$ cd backend
$ yarn install
```
Copy Environment Variables:
```bash
# in backend/
$ cp .env.template .env
```
Configure the new file according to your needs and your local setup. Make sure
a [local Neo4J](http://localhost:7474) instance is up and running.
Start the backend for development with:
```bash
# in backend/
$ yarn run dev
```
or start the backend in production environment with:
```bash
yarn run start
# in backend/
$ yarn run start
```
For e-mail delivery, please configure at least `SMTP_HOST` and `SMTP_PORT` in
@ -50,6 +64,7 @@ your `.env` configuration file.
Your backend is up and running at [http://localhost:4000/](http://localhost:4000/)
This will start the GraphQL service \(by default on localhost:4000\) where you
can issue GraphQL requests or access GraphQL Playground in the browser.
More details about our GraphQL playground and how to use it with ocelot.social can be found [here](./src/graphql/GraphQL-Playground.md).
![GraphQL Playground](../.gitbook/assets/graphql-playground.png)
@ -60,21 +75,24 @@ backend is running:
{% tabs %}
{% tab title="Docker" %}
```bash
docker-compose exec backend yarn run db:migrate init
```
{% endtab %}
{% tab title="Without Docker" %}
```bash
# in folder backend/
# in main folder while docker-compose is running
$ docker-compose exec backend yarn run db:migrate init
```
{% endtab %}
{% tab title="Without Docker" %}
```bash
# in folder backend/ while database is running
# make sure your database is running on http://localhost:7474/browser/
yarn run db:migrate init
```
{% endtab %}
{% endtabs %}
#### Seed Database
If you want your backend to return anything else than an empty response, you
@ -84,30 +102,40 @@ need to seed your database:
{% tab title="Docker" %}
In another terminal run:
```bash
# in main folder while docker-compose is running
$ docker-compose exec backend yarn run db:seed
```
To reset the database run:
```bash
# in main folder while docker-compose is running
$ docker-compose exec backend yarn run db:reset
# you could also wipe out your neo4j database and delete all volumes with:
$ docker-compose down -v
# if container is not running, run this command to set up your database indeces and contstraints
$ docker-compose run backend yarn run db:migrate init
$ docker-compose exec backend yarn run db:migrate init
```
{% endtab %}
{% endtab %}
{% tab title="Without Docker" %}
Run:
```bash
# in backend/ while database is running
$ yarn run db:seed
```
To reset the database run:
```bash
# in backend/ while database is running
$ yarn run db:reset
```
{% endtab %}
{% endtabs %}
@ -118,66 +146,67 @@ you have to migrate your data e.g. because your data modeling has changed.
{% tabs %}
{% tab title="Docker" %}
Generate a data migration file:
```bash
# in main folder while docker-compose is running
$ docker-compose exec backend yarn run db:migrate:create your_data_migration
# Edit the file in ./src/db/migrations/
```
To run the migration:
```bash
# in main folder while docker-compose is running
$ docker-compose exec backend yarn run db:migrate up
```
{% endtab %}
{% tab title="Without Docker" %}
Generate a data migration file:
```bash
# in backend/
$ yarn run db:migrate:create your_data_migration
# Edit the file in ./src/db/migrations/
```
To run the migration:
```bash
# in backend/ while database is running
$ yarn run db:migrate up
```
{% endtab %}
{% endtabs %}
# Testing
## Testing
**Beware**: We have no multiple database setup at the moment. We clean the
database after each test, running the tests will wipe out all your data!
{% tabs %}
{% tab title="Docker" %}
Run the _**jest**_ tests:
Run the unit tests:
```bash
$ docker-compose exec backend yarn run test:jest
```
Run the _**cucumber**_ features:
```bash
$ docker-compose exec backend yarn run test:cucumber
# in main folder while docker-compose is running
$ docker-compose exec backend yarn run test
```
{% endtab %}
{% tab title="Without Docker" %}
Run the _**jest**_ tests:
Run the unit tests:
```bash
$ yarn run test:jest
```
Run the _**cucumber**_ features:
```bash
$ yarn run test:cucumber
# in backend/ while database is running
$ yarn run test
```
{% endtab %}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

View File

@ -1,7 +1,11 @@
{
"name": "human-connection-backend",
"version": "0.6.3",
"description": "GraphQL Backend for Human Connection",
"name": "ocelot-social-backend",
"version": "2.2.0",
"description": "GraphQL Backend for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
"license": "MIT",
"private": false,
"main": "src/index.js",
"scripts": {
"__migrate": "migrate --compiler 'js:@babel/register' --migrations-dir ./src/db/migrations",
@ -11,15 +15,13 @@
"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": "jest --forceExit --detectOpenHandles --runInBand",
"test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --forceExit --detectOpenHandles --runInBand --coverage --logHeapUsage",
"db:clean": "babel-node src/db/clean.js",
"db:reset": "yarn run db:clean",
"db:seed": "babel-node src/db/seed.js",
"db:migrate": "yarn run __migrate --store ./src/db/migrate/store.js",
"db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.js --date-format 'yyyymmddHHmmss' create"
},
"author": "Human Connection gGmbH",
"license": "MIT",
"jest": {
"verbose": true,
"collectCoverageFrom": [
@ -37,25 +39,33 @@
]
},
"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",
"apollo-client": "~2.6.8",
"apollo-link-context": "~1.0.20",
"apollo-link-http": "~1.5.17",
"apollo-server": "~2.11.0",
"apollo-server-express": "^2.12.0",
"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",
"cross-env": "~7.0.3",
"date-fns": "2.22.1",
"debug": "~4.1.1",
"dotenv": "~8.2.0",
"express": "^4.17.1",
"faker": "Marak/faker.js#master",
"graphql": "^14.6.0",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
@ -67,71 +77,64 @@
"helmet": "~3.22.0",
"ioredis": "^4.16.1",
"jsonwebtoken": "~8.5.1",
"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.11.8",
"metascraper-author": "^5.11.8",
"metascraper-audio": "^5.14.26",
"metascraper-author": "^5.14.22",
"metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.11.8",
"metascraper-description": "^5.11.6",
"metascraper-description": "^5.23.1",
"metascraper-image": "^5.11.8",
"metascraper-lang": "^5.11.8",
"metascraper-lang": "^5.23.1",
"metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.11.6",
"metascraper-publisher": "^5.11.8",
"metascraper-soundcloud": "^5.11.8",
"metascraper-logo": "^5.14.26",
"metascraper-publisher": "^5.23.0",
"metascraper-soundcloud": "^5.23.0",
"metascraper-title": "^5.11.8",
"metascraper-url": "^5.11.8",
"metascraper-url": "^5.14.26",
"metascraper-video": "^5.11.8",
"metascraper-youtube": "^5.11.8",
"migrate": "^1.6.2",
"metascraper-youtube": "^5.23.0",
"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",
"node-fetch": "~2.6.0",
"neode": "^0.4.8",
"node-fetch": "~2.6.1",
"nodemailer": "^6.4.4",
"nodemailer-html-to-text": "^3.1.0",
"nodemailer-html-to-text": "^3.2.0",
"npm-run-all": "~4.1.5",
"request": "~2.88.2",
"sanitize-html": "~1.22.0",
"slug": "~2.1.1",
"subscriptions-transport-ws": "^0.9.16",
"slug": "~6.0.0",
"subscriptions-transport-ws": "^0.9.19",
"trunc-html": "~1.1.2",
"uuid": "~7.0.3",
"uuid": "~8.3.2",
"validator": "^13.0.0",
"wait-on": "~4.0.1",
"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",
"@faker-js/faker": "5.1.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",
"eslint-config-prettier": "~6.10.1",
"eslint-config-prettier": "~6.15.0",
"eslint-config-standard": "~14.1.1",
"eslint-plugin-import": "~2.20.2",
"eslint-plugin-jest": "~23.8.2",
"eslint-plugin-node": "~11.1.0",
"eslint-plugin-prettier": "~3.1.2",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-prettier": "~3.4.1",
"eslint-plugin-promise": "~4.3.1",
"eslint-plugin-standard": "~4.0.1",
"jest": "~25.3.0",
"nodemon": "~2.0.2",
"prettier": "~2.0.4",
"prettier": "~2.3.2",
"rosie": "^2.0.1",
"supertest": "~4.0.2"
},

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory'
import fetch from 'node-fetch'
import { ApolloClient } from 'apollo-client'
import trunc from 'trunc-html'
const debug = require('debug')('ea:nitro-datasource')
const debug = require('debug')('ea:datasource')
export default class NitroDataSource {
constructor(uri) {

View File

@ -45,7 +45,7 @@ export async function handler(req, res) {
} catch (error) {
debug(error)
return res.status(500).json({
error: 'Something went terribly wrong. Please contact support@human-connection.org',
error: `Something went terribly wrong. Please visit ${CONFIG.SUPPORT_URL}`,
})
} finally {
session.close()

View File

@ -1,6 +1,7 @@
import { handler } from './webfinger'
import Factory, { cleanDatabase } from '../../db/factories'
import { getDriver } from '../../db/neo4j'
import CONFIG from '../../config'
let resource, res, json, status, contentType
@ -26,6 +27,15 @@ const request = () => {
return handler(req, res)
}
beforeAll(async () => {
await cleanDatabase()
})
afterAll(async () => {
await cleanDatabase()
})
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
afterEach(async () => {
await cleanDatabase()
})
@ -98,12 +108,12 @@ describe('webfinger', () => {
expect(json).toHaveBeenCalledWith({
links: [
{
href: 'http://localhost:3000/activitypub/users/some-user',
href: `${CONFIG.CLIENT_URI}/activitypub/users/some-user`,
rel: 'self',
type: 'application/activity+json',
},
],
subject: 'acct:some-user@localhost:3000',
subject: `acct:some-user@${new URL(CONFIG.CLIENT_URI).host}`,
})
})
})

View File

@ -0,0 +1,8 @@
// this file is duplicated in `backend/src/config/` and `webapp/constants/` and replaced on rebranding by https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/tree/master/branding/constants/
export default {
SUPPORT_EMAIL: 'devops@ocelot.social',
MODERATION_EMAIL: 'devops@ocelot.social',
// ATTENTION: the following links have to be defined even for internal pages with full URLs as example like 'https://staging.ocelot.social/support', because they are used in e-mails!
ORGANIZATION_LINK: 'https://ocelot.social',
SUPPORT_LINK: 'https://ocelot.social',
}

View File

@ -1,95 +1,112 @@
import dotenv from 'dotenv'
import emails from './emails.js'
import metadata from './metadata.js'
// Load env file
if (require.resolve) {
// are we in a nodejs environment?
dotenv.config({ path: require.resolve('../../.env') })
try {
dotenv.config({ path: require.resolve('../../.env') })
} catch (error) {
// This error is thrown when the .env is not found
if (error.code !== 'MODULE_NOT_FOUND') {
throw error
}
}
}
// eslint-disable-next-line no-undef
const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env
// Use Cypress env or process.env
const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env // eslint-disable-line no-undef
const {
MAPBOX_TOKEN,
JWT_SECRET,
PRIVATE_KEY_PASSPHRASE,
SMTP_IGNORE_TLS = true,
SMTP_HOST,
SMTP_PORT,
SMTP_USERNAME,
SMTP_PASSWORD,
SENTRY_DSN_BACKEND,
COMMIT,
AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY,
AWS_ENDPOINT,
AWS_REGION,
AWS_BUCKET,
NEO4J_URI = 'bolt://localhost:7687',
NEO4J_USERNAME = 'neo4j',
NEO4J_PASSWORD = 'neo4j',
CLIENT_URI = 'http://localhost:3000',
GRAPHQL_URI = 'http://localhost:4000',
REDIS_DOMAIN,
REDIS_PORT,
REDIS_PASSWORD,
} = env
export const requiredConfigs = {
MAPBOX_TOKEN,
JWT_SECRET,
PRIVATE_KEY_PASSPHRASE,
const environment = {
NODE_ENV: env.NODE_ENV || process.NODE_ENV,
DEBUG: env.NODE_ENV !== 'production' && env.DEBUG,
TEST: env.NODE_ENV === 'test',
PRODUCTION: env.NODE_ENV === 'production',
// used for staging enviroments if 'PRODUCTION=true' and 'PRODUCTION_DB_CLEAN_ALLOW=true'
PRODUCTION_DB_CLEAN_ALLOW: env.PRODUCTION_DB_CLEAN_ALLOW === 'true' || false, // default = false
DISABLED_MIDDLEWARES: (env.NODE_ENV !== 'production' && env.DISABLED_MIDDLEWARES) || false,
}
const required = {
MAPBOX_TOKEN: env.MAPBOX_TOKEN,
JWT_SECRET: env.JWT_SECRET,
PRIVATE_KEY_PASSPHRASE: env.PRIVATE_KEY_PASSPHRASE,
}
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 !== 'false', // default = true
SMTP_SECURE: env.SMTP_SECURE === 'true',
SMTP_USERNAME: env.SMTP_USERNAME,
SMTP_PASSWORD: env.SMTP_PASSWORD,
}
const neo4j = {
NEO4J_URI: env.NEO4J_URI || 'bolt://localhost:7687',
NEO4J_USERNAME: env.NEO4J_USERNAME || 'neo4j',
NEO4J_PASSWORD: env.NEO4J_PASSWORD || 'neo4j',
}
const sentry = {
SENTRY_DSN_BACKEND: env.SENTRY_DSN_BACKEND,
COMMIT: env.COMMIT,
}
const redis = {
REDIS_DOMAIN: env.REDIS_DOMAIN,
REDIS_PORT: env.REDIS_PORT,
REDIS_PASSWORD: env.REDIS_PASSWORD,
}
const s3 = {
AWS_ACCESS_KEY_ID: env.AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY: env.AWS_SECRET_ACCESS_KEY,
AWS_ENDPOINT: env.AWS_ENDPOINT,
AWS_REGION: env.AWS_REGION,
AWS_BUCKET: env.AWS_BUCKET,
S3_CONFIGURED:
env.AWS_ACCESS_KEY_ID &&
env.AWS_SECRET_ACCESS_KEY &&
env.AWS_ENDPOINT &&
env.AWS_REGION &&
env.AWS_BUCKET,
}
const options = {
EMAIL_DEFAULT_SENDER: env.EMAIL_DEFAULT_SENDER,
SUPPORT_URL: emails.SUPPORT_LINK,
APPLICATION_NAME: metadata.APPLICATION_NAME,
ORGANIZATION_URL: emails.ORGANIZATION_LINK,
PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true' || false,
INVITE_REGISTRATION: env.INVITE_REGISTRATION !== 'false', // default = true
CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false,
}
// Check if all required configs are present
if (require.resolve) {
// are we in a nodejs environment?
Object.entries(requiredConfigs).map((entry) => {
Object.entries(required).map((entry) => {
if (!entry[1]) {
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
}
})
}
export const smtpConfigs = {
SMTP_HOST,
SMTP_PORT,
SMTP_IGNORE_TLS,
SMTP_USERNAME,
SMTP_PASSWORD,
}
export const neo4jConfigs = { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD }
export const serverConfigs = {
CLIENT_URI,
GRAPHQL_URI,
PUBLIC_REGISTRATION: process.env.PUBLIC_REGISTRATION === 'true',
}
export const developmentConfigs = {
DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG,
DISABLED_MIDDLEWARES:
(process.env.NODE_ENV !== 'production' && process.env.DISABLED_MIDDLEWARES) || '',
}
export const sentryConfigs = { SENTRY_DSN_BACKEND, COMMIT }
export const redisConfigs = { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD }
const S3_CONFIGURED =
AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY && AWS_ENDPOINT && AWS_REGION && AWS_BUCKET
export const s3Configs = {
AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY,
AWS_ENDPOINT,
AWS_REGION,
AWS_BUCKET,
S3_CONFIGURED,
}
export default {
...requiredConfigs,
...smtpConfigs,
...neo4jConfigs,
...serverConfigs,
...developmentConfigs,
...sentryConfigs,
...redisConfigs,
...s3Configs,
...environment,
...server,
...required,
...smtp,
...neo4j,
...sentry,
...redis,
...s3,
...options,
}

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

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

@ -0,0 +1,102 @@
// this file is duplicated in `backend/src/constants/metadata.js` and `webapp/constants/metadata.js`
export const CATEGORIES_MIN = 1
export const CATEGORIES_MAX = 3
export const categories = [
{
icon: 'networking',
name: 'networking',
description: 'Kooperation, Aktionsbündnisse, Solidarität, Hilfe',
},
{
icon: 'home',
name: 'home',
description: 'Bauen, Lebensgemeinschaften, Tiny Houses, Gemüsegarten',
},
{
icon: 'energy',
name: 'energy',
description: 'Öl, Gas, Kohle, Wind, Wasserkraft, Biogas, Atomenergie, ...',
},
{
icon: 'psyche',
name: 'psyche',
description: 'Seele, Gefühle, Glück',
},
{
icon: 'movement',
name: 'body-and-excercise',
description: 'Sport, Yoga, Massage, Tanzen, Entspannung',
},
{
icon: 'balance-scale',
name: 'law',
description: 'Menschenrechte, Gesetze, Verordnungen',
},
{
icon: 'finance',
name: 'finance',
description: 'Geld, Finanzsystem, Alternativwährungen, ...',
},
{
icon: 'child',
name: 'children',
description: 'Familie, Pädagogik, Schule, Prägung',
},
{
icon: 'mobility',
name: 'mobility',
description: 'Reise, Verkehr, Elektromobilität',
},
{
icon: 'shopping-cart',
name: 'economy',
description: 'Handel, Konsum, Marketing, Lebensmittel, Lieferketten, ...',
},
{
icon: 'peace',
name: 'peace',
description: 'Krieg, Militär, soziale Verteidigung, Waffen, Cyberattacken',
},
{
icon: 'politics',
name: 'politics',
description: 'Demokratie, Mitbestimmung, Wahlen, Korruption, Parteien',
},
{
icon: 'nature',
name: 'nature',
description: 'Tiere, Pflanzen, Landwirtschaft, Ökologie, Artenvielfalt',
},
{
icon: 'science',
name: 'science',
description: 'Bildung, Hochschule, Publikationen, ...',
},
{
icon: 'health',
name: 'health',
description: 'Medizin, Ernährung, WHO, Impfungen, Schadstoffe, ...',
},
{
icon: 'media',
name: 'it-and-media',
description:
'Nachrichten, Manipulation, Datenschutz, Überwachung, Datenkraken, AI, Software, Apps',
},
{
icon: 'spirituality',
name: 'spirituality',
description: 'Religion, Werte, Ethik',
},
{
icon: 'culture',
name: 'culture',
description: 'Kunst, Theater, Musik, Fotografie, Film',
},
{
icon: 'miscellaneous',
name: 'miscellaneous',
description: '',
},
]

View File

@ -0,0 +1,3 @@
// this file is duplicated in `backend/src/constants/group.js` and `webapp/constants/group.js`
export const DESCRIPTION_WITHOUT_HTML_LENGTH_MIN = 100 // with removed HTML tags
export const DESCRIPTION_EXCERPT_HTML_LENGTH = 250 // with removed HTML tags

View File

@ -0,0 +1,5 @@
// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js`
export default {
NONCE_LENGTH: 5,
INVITE_CODE_LENGTH: 6,
}

View File

@ -1,7 +1,8 @@
import CONFIG from '../config'
import { cleanDatabase } from '../db/factories'
if (process.env.NODE_ENV === 'production') {
throw new Error(`You cannot clean the database in production environment!`)
if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
throw new Error(`You cannot clean the database in a non-staging and real production environment!`)
}
;(async function () {

View File

@ -1,10 +1,11 @@
import { v4 as uuid } from 'uuid'
import faker from 'faker'
import slugify from 'slug'
import { hashSync } from 'bcryptjs'
import { Factory } from 'rosie'
import faker from '@faker-js/faker'
import { getDriver, getNeode } from './neo4j'
import CONFIG from '../config/index.js'
import generateInviteCode from '../schema/resolvers/helpers/generateInviteCode.js'
const neode = getNeode()
@ -48,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)
@ -68,6 +70,7 @@ Factory.define('basicUser')
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
allowEmbedIframes: false,
showShoutsPublicly: false,
sendNotificationEmails: true,
locale: 'en',
})
.attr('slug', ['slug', 'name'], (slug, name) => {
@ -103,12 +106,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)))
@ -128,6 +131,8 @@ Factory.define('post')
deleted: false,
imageBlurred: false,
imageAspectRatio: 1.333,
clickedCount: 0,
viewedTeaserCount: 0,
})
.attr('pinned', ['pinned'], (pinned) => {
// Convert false to null
@ -143,16 +148,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')
@ -193,8 +198,9 @@ Factory.define('comment')
Factory.define('donations')
.attr('id', uuid)
.attr('showDonations', true)
.attr('goal', 15000)
.attr('progress', 0)
.attr('progress', 7000)
.after((buildObject, options) => {
return neode.create('Donations', buildObject)
})
@ -205,7 +211,7 @@ const emailDefaults = {
}
Factory.define('emailAddress')
.attr(emailDefaults)
.attrs(emailDefaults)
.after((buildObject, options) => {
return neode.create('EmailAddress', buildObject)
})
@ -216,6 +222,28 @@ Factory.define('unverifiedEmailAddress')
return neode.create('UnverifiedEmailAddress', buildObject)
})
const inviteCodeDefaults = {
code: () => generateInviteCode(),
createdAt: () => new Date().toISOString(),
expiresAt: () => null,
}
Factory.define('inviteCode')
.attrs(inviteCodeDefaults)
.option('generatedById', null)
.option('generatedBy', ['generatedById'], (generatedById) => {
if (generatedById) return neode.find('User', generatedById)
return Factory.build('user')
})
.after(async (buildObject, options) => {
const [inviteCode, generatedBy] = await Promise.all([
neode.create('InviteCode', buildObject),
options.generatedBy,
])
await Promise.all([inviteCode.relateTo(generatedBy, 'generated')])
return inviteCode
})
Factory.define('location')
.attrs({
name: 'Germany',

View File

@ -1,17 +1,95 @@
import { getDriver, getNeode } from '../../db/neo4j'
import { hashSync } from 'bcryptjs'
import { v4 as uuid } from 'uuid'
import { categories } from '../../constants/categories'
import CONFIG from '../../config'
const defaultAdmin = {
email: 'admin@example.org',
password: hashSync('1234', 10),
name: 'admin',
id: uuid(),
slug: 'admin',
}
const createCategories = async (session) => {
const createCategoriesTxResultPromise = session.writeTransaction(async (txc) => {
categories.forEach(({ icon, name }, index) => {
const id = `cat${index + 1}`
txc.run(
`MERGE (c:Category {
icon: "${icon}",
slug: "${name}",
name: "${name}",
id: "${id}",
createdAt: toString(datetime())
})`,
)
})
})
try {
await createCategoriesTxResultPromise
console.log('Successfully created categories!') // eslint-disable-line no-console
} catch (error) {
console.log(`Error creating categories: ${error}`) // eslint-disable-line no-console
}
}
const createDefaultAdminUser = async (session) => {
const readTxResultPromise = session.readTransaction(async (txc) => {
const result = await txc.run('MATCH (user:User) RETURN count(user) AS userCount')
return result.records.map((r) => r.get('userCount'))
})
let createAdmin = false
try {
const userCount = parseInt(String(await readTxResultPromise))
if (userCount === 0) createAdmin = true
} catch (error) {
console.log(error) // eslint-disable-line no-console
}
if (createAdmin) {
const createAdminTxResultPromise = session.writeTransaction(async (txc) => {
txc.run(
`MERGE (e:EmailAddress {
email: "${defaultAdmin.email}",
createdAt: toString(datetime())
})-[:BELONGS_TO]->(u:User {
name: "${defaultAdmin.name}",
encryptedPassword: "${defaultAdmin.password}",
role: "admin",
id: "${defaultAdmin.id}",
slug: "${defaultAdmin.slug}",
createdAt: toString(datetime()),
allowEmbedIframes: false,
showShoutsPublicly: false,
sendNotificationEmails: true,
deleted: false,
disabled: false
})-[:PRIMARY_EMAIL]->(e)`,
)
})
try {
await createAdminTxResultPromise
console.log('Successfully created default admin user!') // eslint-disable-line no-console
} catch (error) {
console.log(error) // eslint-disable-line no-console
}
}
}
class Store {
async init(next) {
const neode = getNeode()
const { driver } = neode
const session = driver.session()
// eslint-disable-next-line no-console
await createDefaultAdminUser(session)
if (CONFIG.CATEGORIES_ACTIVE) await createCategories(session)
const writeTxResultPromise = session.writeTransaction(async (txc) => {
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices and contraints
return Promise.all(
[
'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])',
'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])',
'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])',
'CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])',
].map((statement) => txc.run(statement)),
)

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

@ -0,0 +1,66 @@
import { getDriver } from '../../db/neo4j'
import { v4 as uuid } from 'uuid'
export const description =
'This migration adds a Donations node with default settings to the database.'
export async function up(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
const donationId = uuid()
await transaction.run(
`
MERGE (donationInfo:Donations)
SET donationInfo.id = $donationId
SET donationInfo.createdAt = toString(datetime())
SET donationInfo.updatedAt = donationInfo.createdAt
SET donationInfo.showDonations = false
SET donationInfo.goal = 15000.0
SET donationInfo.progress = 1200.0
RETURN donationInfo {.*}
`,
{ donationId },
)
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()
}
}
export async function down(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(`
MATCH (donationInfo:Donations)
DETACH DELETE donationInfo
RETURN donationInfo
`)
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,59 @@
import { getDriver } from '../../db/neo4j'
export const description = ''
export async function up(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(
`
MATCH (user:User)
SET user.sendNotificationEmails = true
RETURN user {.*}
`,
)
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()
}
}
export async function down(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(
`
MATCH (user:User)
REMOVE user.sendNotificationEmails
RETURN user {.*}
`,
)
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,66 @@
import { getDriver } from '../../db/neo4j'
export const description = `
We introduced a new node label 'Group' and we need two primary keys 'id' and 'slug' for it.
Additional we like to have fulltext indices the keys 'name', 'slug', 'about', and 'description'.
`
export async function up(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(`
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
`)
await transaction.run(`
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
`)
await transaction.run(`
CALL db.index.fulltext.createNodeIndex("group_fulltext_search",["Group"],["name", "slug", "about", "description"])
`)
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()
}
}
export async function down(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(`
DROP CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
`)
await transaction.run(`
DROP CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
`)
await transaction.run(`
CALL db.index.fulltext.drop("group_fulltext_search")
`)
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

@ -1,10 +1,22 @@
import faker from 'faker'
import sample from 'lodash/sample'
import { createTestClient } from 'apollo-server-testing'
import CONFIG from '../config'
import createServer from '../server'
import faker from '@faker-js/faker'
import Factory from '../db/factories'
import { getNeode, getDriver } from '../db/neo4j'
import { gql } from '../helpers/jest'
import {
createGroupMutation,
joinGroupMutation,
changeGroupMemberRoleMutation,
} from '../graphql/groups'
import { createPostMutation } from '../graphql/posts'
import { createCommentMutation } from '../graphql/comments'
import { categories } from '../constants/categories'
if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
throw new Error(`You cannot seed the database in a non-staging and real production environment!`)
}
const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
@ -137,100 +149,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'),
@ -269,104 +274,16 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
dagobert.relateTo(louie, 'blocked'),
])
await Promise.all([
Factory.build('category', {
id: 'cat1',
name: 'Just For Fun',
slug: 'just-for-fun',
icon: 'smile',
await Promise.all(
categories.map(({ icon, name }, index) => {
Factory.build('category', {
id: `cat${index + 1}`,
slug: name,
name,
icon,
})
}),
Factory.build('category', {
id: 'cat2',
name: 'Happiness & Values',
slug: 'happiness-values',
icon: 'heart-o',
}),
Factory.build('category', {
id: 'cat3',
name: 'Health & Wellbeing',
slug: 'health-wellbeing',
icon: 'medkit',
}),
Factory.build('category', {
id: 'cat4',
name: 'Environment & Nature',
slug: 'environment-nature',
icon: 'tree',
}),
Factory.build('category', {
id: 'cat5',
name: 'Animal Protection',
slug: 'animal-protection',
icon: 'paw',
}),
Factory.build('category', {
id: 'cat6',
name: 'Human Rights & Justice',
slug: 'human-rights-justice',
icon: 'balance-scale',
}),
Factory.build('category', {
id: 'cat7',
name: 'Education & Sciences',
slug: 'education-sciences',
icon: 'graduation-cap',
}),
Factory.build('category', {
id: 'cat8',
name: 'Cooperation & Development',
slug: 'cooperation-development',
icon: 'users',
}),
Factory.build('category', {
id: 'cat9',
name: 'Democracy & Politics',
slug: 'democracy-politics',
icon: 'university',
}),
Factory.build('category', {
id: 'cat10',
name: 'Economy & Finances',
slug: 'economy-finances',
icon: 'money',
}),
Factory.build('category', {
id: 'cat11',
name: 'Energy & Technology',
slug: 'energy-technology',
icon: 'flash',
}),
Factory.build('category', {
id: 'cat12',
name: 'IT, Internet & Data Privacy',
slug: 'it-internet-data-privacy',
icon: 'mouse-pointer',
}),
Factory.build('category', {
id: 'cat13',
name: 'Art, Culture & Sport',
slug: 'art-culture-sport',
icon: 'paint-brush',
}),
Factory.build('category', {
id: 'cat14',
name: 'Freedom of Speech',
slug: 'freedom-of-speech',
icon: 'bullhorn',
}),
Factory.build('category', {
id: 'cat15',
name: 'Consumption & Sustainability',
slug: 'consumption-sustainability',
icon: 'shopping-cart',
}),
Factory.build('category', {
id: 'cat16',
name: 'Global Peace & Nonviolence',
slug: 'global-peace-nonviolence',
icon: 'angellist',
}),
])
)
const [environment, nature, democracy, freedom] = await Promise.all([
Factory.build('tag', {
@ -383,6 +300,302 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
}),
])
// Create Groups
authenticatedUser = await peterLustig.toJson()
await Promise.all([
mutate({
mutation: createGroupMutation(),
variables: {
id: 'g0',
name: 'Investigative Journalism',
about: 'Investigative journalists share ideas and insights and can collaborate.',
description: `<p class=""><em>English:</em></p><p class="">This group is hidden.</p><h3>What is our group for?</h3><p>This group was created to allow investigative journalists to share and collaborate.</p><h3>How does it work?</h3><p>Here you can internally share posts and comments about them.</p><p><br></p><p><em>Deutsch:</em></p><p class="">Diese Gruppe ist verborgen.</p><h3>Wofür ist unsere Gruppe?</h3><p class="">Diese Gruppe wurde geschaffen, um investigativen Journalisten den Austausch und die Zusammenarbeit zu ermöglichen.</p><h3>Wie funktioniert das?</h3><p class="">Hier könnt ihr euch intern über Beiträge und Kommentare zu ihnen austauschen.</p>`,
groupType: 'hidden',
actionRadius: 'global',
categoryIds: ['cat6', 'cat12', 'cat16'],
locationName: 'Hamburg, Germany',
},
}),
])
await Promise.all([
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g0',
userId: 'u2',
},
}),
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g0',
userId: 'u4',
},
}),
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g0',
userId: 'u6',
},
}),
])
await Promise.all([
mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'g0',
userId: 'u2',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'g0',
userId: 'u4',
roleInGroup: 'admin',
},
}),
])
// post into group
await Promise.all([
mutate({
mutation: createPostMutation(),
variables: {
id: 'p0-g0',
groupId: 'g0',
title: `What happend in Shanghai?`,
content: 'A sack of rise dropped in Shanghai. Should we further investigate?',
categoryIds: ['cat6'],
},
}),
])
authenticatedUser = await bobDerBaumeister.toJson()
await Promise.all([
mutate({
mutation: createPostMutation(),
variables: {
id: 'p1-g0',
groupId: 'g0',
title: `The man on the moon`,
content: 'We have to further investigate about the stories of a man living on the moon.',
categoryIds: ['cat12', 'cat16'],
},
}),
])
authenticatedUser = await jennyRostock.toJson()
await Promise.all([
mutate({
mutation: createGroupMutation(),
variables: {
id: 'g1',
name: 'School For Citizens',
about: 'Our children shall receive education for life.',
description: `<p class=""><em>English</em></p><h3>Our goal</h3><p>Only those who enjoy learning and do not lose their curiosity can obtain a good education for life and continue to learn with joy throughout their lives.</p><h3>Curiosity</h3><p>For this we need a school that takes up the curiosity of the children, the people, and satisfies it through a lot of experience.</p><p><br></p><p><em>Deutsch</em></p><h3>Unser Ziel</h3><p class="">Nur wer Spaß am Lernen hat und seine Neugier nicht verliert, kann gute Bildung für's Leben erlangen und sein ganzes Leben mit Freude weiter lernen.</p><h3>Neugier</h3><p class="">Dazu benötigen wir eine Schule, die die Neugier der Kinder, der Menschen, aufnimmt und durch viel Erfahrung befriedigt.</p>`,
groupType: 'closed',
actionRadius: 'national',
categoryIds: ['cat8', 'cat14'],
locationName: 'France',
},
}),
])
await Promise.all([
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g1',
userId: 'u1',
},
}),
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g1',
userId: 'u2',
},
}),
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g1',
userId: 'u5',
},
}),
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g1',
userId: 'u6',
},
}),
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g1',
userId: 'u7',
},
}),
])
await Promise.all([
mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'g1',
userId: 'u1',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'g1',
userId: 'u5',
roleInGroup: 'admin',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'g1',
userId: 'u6',
roleInGroup: 'owner',
},
}),
])
// post into group
await Promise.all([
mutate({
mutation: createPostMutation(),
variables: {
id: 'p0-g1',
groupId: 'g1',
title: `Can we use ocelot for education?`,
content: 'I like the concept of this school. Can we use our software in this?',
categoryIds: ['cat8'],
},
}),
])
authenticatedUser = await peterLustig.toJson()
await Promise.all([
mutate({
mutation: createPostMutation(),
variables: {
id: 'p1-g1',
groupId: 'g1',
title: `Can we push this idea out of France?`,
content: 'This idea is too inportant to have the scope only on France.',
categoryIds: ['cat14'],
},
}),
])
authenticatedUser = await bobDerBaumeister.toJson()
await Promise.all([
mutate({
mutation: createGroupMutation(),
variables: {
id: 'g2',
name: 'Yoga Practice',
about: 'We do yoga around the clock.',
description: `<h3>What Is yoga?</h3><p>Yoga is not just about practicing asanas. It's about how we do it.</p><p class="">And practicing asanas doesn't have to be yoga, it can be more athletic than yogic.</p><h3>What makes practicing asanas yogic?</h3><p class="">The important thing is:</p><ul><li><p>Use the exercises (consciously) for your personal development.</p></li></ul>`,
groupType: 'public',
actionRadius: 'interplanetary',
categoryIds: ['cat4', 'cat5', 'cat17'],
},
}),
])
await Promise.all([
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g2',
userId: 'u3',
},
}),
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g2',
userId: 'u4',
},
}),
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g2',
userId: 'u5',
},
}),
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g2',
userId: 'u6',
},
}),
mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g2',
userId: 'u7',
},
}),
])
await Promise.all([
mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'g2',
userId: 'u3',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'g2',
userId: 'u4',
roleInGroup: 'pending',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'g2',
userId: 'u5',
roleInGroup: 'admin',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'g2',
userId: 'u6',
roleInGroup: 'usual',
},
}),
])
authenticatedUser = await louie.toJson()
await Promise.all([
mutate({
mutation: createPostMutation(),
variables: {
id: 'p0-g2',
groupId: 'g2',
title: `I am a Noob`,
content: 'I am new to Yoga and did not join this group so far.',
categoryIds: ['cat4'],
},
}),
])
// Create Posts
const [p0, p1, p3, p4, p5, p6, p9, p10, p11, p13, p14, p15] = await Promise.all([
Factory.build(
'post',
@ -541,6 +754,16 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
),
])
await Factory.build(
'inviteCode',
{
code: 'ABCDEF',
},
{
generatedBy: jennyRostock,
},
)
authenticatedUser = await louie.toJson()
const mention1 =
'Hey <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
@ -550,17 +773,10 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
'See <a class="hashtag" data-hashtag-id="NaturphilosophieYoga" href="/?hashtag=NaturphilosophieYoga">#NaturphilosophieYoga</a>, it can really help you!'
const hashtagAndMention1 =
'The new physics of <a class="hashtag" data-hashtag-id="QuantenFlussTheorie" href="/?hashtag=QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" data-hashtag-id="QuantumGravity" href="/?hashtag=QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
const createPostMutation = gql`
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
id
}
}
`
await Promise.all([
mutate({
mutation: createPostMutation,
mutation: createPostMutation(),
variables: {
id: 'p2',
title: `Nature Philosophy Yoga`,
@ -569,7 +785,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
mutate({
mutation: createPostMutation,
mutation: createPostMutation(),
variables: {
id: 'p7',
title: 'This is post #7',
@ -578,7 +794,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
mutate({
mutation: createPostMutation,
mutation: createPostMutation(),
variables: {
id: 'p8',
image: faker.image.unsplash.nature(),
@ -588,7 +804,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
mutate({
mutation: createPostMutation,
mutation: createPostMutation(),
variables: {
id: 'p12',
title: 'This is post #12',
@ -607,13 +823,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
'I heard <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a> has practiced it for 3 years now.'
const mentionInComment2 =
'Did <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> tell you?'
const createCommentMutation = gql`
mutation($id: ID, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
}
}
`
await Promise.all([
mutate({
mutation: createCommentMutation,
@ -931,6 +1140,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
const additionalUsers = await Promise.all(
[...Array(30).keys()].map(() => Factory.build('user')),
)
await Promise.all(
additionalUsers.map(async (user) => {
await jennyRostock.relateTo(user, 'following')
@ -938,6 +1148,26 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
}),
)
await Promise.all(
[...Array(30).keys()].map((index) => Factory.build('user', { name: `Jenny${index}` })),
)
await Promise.all(
[...Array(30).keys()].map(() =>
Factory.build(
'post',
{ content: `Jenny ${faker.lorem.sentence()}` },
{
categoryIds: ['cat1'],
author: jennyRostock,
image: Factory.build('image', {
url: faker.image.unsplash.objects(),
}),
},
),
),
)
await Promise.all(
[...Array(30).keys()].map(() =>
Factory.build(

View File

@ -0,0 +1,108 @@
# GraphQL Playground
To use GraphQL Playground, we need to know some basics:
## How To Login?
First, we need to have a user from ocelot.social to log in as.
The user can be created by seeding the Neo4j database from the backend or by multiple GraphQL mutations.
### Seed The Neo4j Database
In your browser you can reach the GraphQL Playground under `http://localhost:4000/`, if the database and the backend are running, see [backend](../../README.md).
There you will also find instructions on how to seed the database.
### Use GraphQL Mutations To Create A User
TODO: Describe how to create a user using GraphQL mutations!
### Login Via GraphQL
You can register a user by sending the query:
```gql
mutation {
login(email: "user@example.org", password: "1234")
}
```
Or use `"moderator@example.org"` or `"admin@example.org"` for the roll you need.
If all goes well, you will receive a QGL response like:
```json
{
"data": {
"login": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUzIiwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTY2MjAyMzMwNSwiZXhwIjoxNzI1MTM4NTA1LCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.atBS-SOeS784HPeFl_5s8sRWehEAU1BkgcOZFD8d4bU"
}
}
```
You can use this response to set an HTTP header when you click `HTTP HEADERS` in the footer.
Just set it with the login token you received in response:
```json
{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUzIiwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTY2MjAyMzMwNSwiZXhwIjoxNzI1MTM4NTA1LCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.atBS-SOeS784HPeFl_5s8sRWehEAU1BkgcOZFD8d4bU"
}
```
This token is used for all other queries and mutations you send to the backend.
## Query And Mutate
When you are logged in and open a new playground tab by clicking "+", you can create a new group by sending the following mutation:
```gql
mutation {
CreateGroup(
# id: ""
name: "My Group"
# slug: ""
about: "We will save the world"
description: "<p class=\"\"><em>English:</em></p><p class=\"\">This group is hidden.</p><h3>What is our group for?</h3><p>This group was created to allow investigative journalists to share and collaborate.</p><h3>How does it work?</h3><p>Here you can internally share posts and comments about them.</p><p><br></p><p><em>Deutsch:</em></p><p class=\"\">Diese Gruppe ist verborgen.</p><h3>Wofür ist unsere Gruppe?</h3><p class=\"\">Diese Gruppe wurde geschaffen, um investigativen Journalisten den Austausch und die Zusammenarbeit zu ermöglichen.</p><h3>Wie funktioniert das?</h3><p class=\"\">Hier könnt ihr euch intern über Beiträge und Kommentare zu ihnen austauschen.</p>"
groupType: hidden
actionRadius: interplanetary
categoryIds: ["cat12"]
) {
id
name
slug
createdAt
updatedAt
disabled
deleted
about
description
groupType
actionRadius
myRole
}
}
```
You will receive the answer:
```json
{
"data": {
"CreateGroup": {
"id": "2e3bbadb-804b-4ebc-a673-2d7c7f05e827",
"name": "My Group",
"slug": "my-group",
"createdAt": "2022-09-01T09:44:47.969Z",
"updatedAt": "2022-09-01T09:44:47.969Z",
"disabled": false,
"deleted": false,
"about": "We will save the world",
"description": "<p class=\"\"><em>English:</em></p><p class=\"\">This group is hidden.</p><h3>What is our group for?</h3><p>This group was created to allow investigative journalists to share and collaborate.</p><h3>How does it work?</h3><p>Here you can internally share posts and comments about them.</p><p><br></p><p><em>Deutsch:</em></p><p class=\"\">Diese Gruppe ist verborgen.</p><h3>Wofür ist unsere Gruppe?</h3><p class=\"\">Diese Gruppe wurde geschaffen, um investigativen Journalisten den Austausch und die Zusammenarbeit zu ermöglichen.</p><h3>Wie funktioniert das?</h3><p class=\"\">Hier könnt ihr euch intern über Beiträge und Kommentare zu ihnen austauschen.</p>",
"groupType": "hidden",
"actionRadius": "interplanetary",
"myRole": "owner"
}
}
}
```
If you look into the Neo4j database with your browser and search the groups, you will now also find your new group.
For more details about our Neo4j database read [here](../../../neo4j/README.md).

View File

@ -0,0 +1,30 @@
import gql from 'graphql-tag'
// ------ mutations
export const signupVerificationMutation = gql`
mutation (
$password: String!
$email: String!
$name: String!
$slug: String
$nonce: String!
$termsAndConditionsAgreedVersion: String!
) {
SignupVerification(
email: $email
password: $password
name: $name
slug: $slug
nonce: $nonce
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
) {
id
slug
}
}
`
// ------ queries
// fill queries in here

View File

@ -0,0 +1,15 @@
import gql from 'graphql-tag'
// ------ mutations
export const createCommentMutation = gql`
mutation ($id: ID, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
}
}
`
// ------ queries
// fill queries in here

View File

@ -0,0 +1,203 @@
import gql from 'graphql-tag'
// ------ mutations
export const createGroupMutation = () => {
return gql`
mutation (
$id: ID
$name: String!
$slug: String
$about: String
$description: String!
$groupType: GroupType!
$actionRadius: GroupActionRadius!
$categoryIds: [ID]
$locationName: String # empty string '' sets it to null
) {
CreateGroup(
id: $id
name: $name
slug: $slug
about: $about
description: $description
groupType: $groupType
actionRadius: $actionRadius
categoryIds: $categoryIds
locationName: $locationName
) {
id
name
slug
createdAt
updatedAt
disabled
deleted
about
description
descriptionExcerpt
groupType
actionRadius
categories {
id
slug
name
icon
}
locationName
location {
name
nameDE
nameEN
}
myRole
}
}
`
}
export const updateGroupMutation = () => {
return gql`
mutation (
$id: ID!
$name: String
$slug: String
$about: String
$description: String
$actionRadius: GroupActionRadius
$categoryIds: [ID]
$avatar: ImageInput
$locationName: String # empty string '' sets it to null
) {
UpdateGroup(
id: $id
name: $name
slug: $slug
about: $about
description: $description
actionRadius: $actionRadius
categoryIds: $categoryIds
avatar: $avatar
locationName: $locationName
) {
id
name
slug
createdAt
updatedAt
disabled
deleted
about
description
descriptionExcerpt
groupType
actionRadius
categories {
id
slug
name
icon
}
# avatar # test this as result
locationName
location {
name
nameDE
nameEN
}
myRole
}
}
`
}
export const joinGroupMutation = () => {
return gql`
mutation ($groupId: ID!, $userId: ID!) {
JoinGroup(groupId: $groupId, userId: $userId) {
id
name
slug
myRoleInGroup
}
}
`
}
export const leaveGroupMutation = () => {
return gql`
mutation ($groupId: ID!, $userId: ID!) {
LeaveGroup(groupId: $groupId, userId: $userId) {
id
name
slug
myRoleInGroup
}
}
`
}
export const changeGroupMemberRoleMutation = () => {
return gql`
mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) {
ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) {
id
name
slug
myRoleInGroup
}
}
`
}
// ------ queries
export const groupQuery = () => {
return gql`
query ($isMember: Boolean, $id: ID, $slug: String) {
Group(isMember: $isMember, id: $id, slug: $slug) {
id
name
slug
createdAt
updatedAt
disabled
deleted
about
description
descriptionExcerpt
groupType
actionRadius
categories {
id
slug
name
icon
}
avatar {
url
}
locationName
location {
name
nameDE
nameEN
}
myRole
}
}
`
}
export const groupMembersQuery = () => {
return gql`
query ($id: ID!) {
GroupMembers(id: $id) {
id
name
slug
myRoleInGroup
}
}
`
}

View File

@ -0,0 +1,88 @@
import gql from 'graphql-tag'
// ------ mutations
export const createPostMutation = () => {
return gql`
mutation (
$id: ID
$title: String!
$slug: String
$content: String!
$categoryIds: [ID]
$groupId: ID
) {
CreatePost(
id: $id
title: $title
slug: $slug
content: $content
categoryIds: $categoryIds
groupId: $groupId
) {
id
slug
title
content
}
}
`
}
// ------ queries
export const postQuery = () => {
return gql`
query Post($id: ID!) {
Post(id: $id) {
id
title
content
}
}
`
}
export const filterPosts = () => {
return gql`
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
id
title
content
}
}
`
}
export const profilePagePosts = () => {
return gql`
query profilePagePosts(
$filter: _PostFilter
$first: Int
$offset: Int
$orderBy: [_PostOrdering]
) {
profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
id
title
content
}
}
`
}
export const searchPosts = () => {
return gql`
query ($query: String!, $firstPosts: Int, $postsOffset: Int) {
searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
postCount
posts {
id
title
content
}
}
}
`
}

View File

@ -0,0 +1,13 @@
import gql from 'graphql-tag'
// ------ mutations
export const loginMutation = gql`
mutation ($email: String!, $password: String!) {
login(email: $email, password: $password)
}
`
// ------ queries
// fill queries in here

View File

@ -1,5 +1,18 @@
// TODO: can be replaced with: (which is no a fake)
// import gql from 'graphql-tag'
// See issue: https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/5152
//* This is a fake ES2015 template string, just to benefit of syntax
// highlighting of `gql` template strings in certain editors.
export function gql(strings) {
return strings.join('')
}
// sometime we have to wait to check a db state by having a look into the db in a certain moment
// or we wait a bit to check if we missed to set an await somewhere
// see: https://www.sitepoint.com/delay-sleep-pause-wait/
export function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// usage 4 seconds for example
// await sleep(4 * 1000)

View File

@ -21,9 +21,24 @@ const neode = getNeode()
// iss: 'http://localhost:4000',
// sub: 'u3'
// }
// !!! if the token expires go into the GraphQL Playground in the browser at 'http://localhost:4000' with a running backend and a seeded Neo4j database
// now do the login mutation:
// mutation {
// login(email:"user@example.org", password:"1234")
// }
// replace this token here with the one you received as the result
export const validAuthorizationHeader =
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc'
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUzIiwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTYzNzY0NDMwMCwiZXhwIjoxNzAwNzU5NTAwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.ispIfRfgkXuYoIhKx7x2jPxgvHDJVv1ogMycLmfUnsk'
beforeAll(async () => {
await cleanDatabase()
})
afterAll(async () => {
await cleanDatabase()
})
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
afterEach(async () => {
await cleanDatabase()
})
@ -38,6 +53,7 @@ describe('decode', () => {
beforeEach(() => {
authorizationHeader = null
})
it('returns null', returnsNull)
})
@ -45,6 +61,7 @@ describe('decode', () => {
beforeEach(() => {
authorizationHeader = undefined
})
it('returns null', returnsNull)
})
@ -52,6 +69,7 @@ describe('decode', () => {
beforeEach(() => {
authorizationHeader = 'blah'
})
it('returns null', returnsNull)
})
@ -59,6 +77,7 @@ describe('decode', () => {
beforeEach(() => {
authorizationHeader = validAuthorizationHeader
})
it('returns null', returnsNull)
describe('and corresponding user in the database', () => {

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

@ -1,76 +0,0 @@
import CONFIG from '../../config'
import nodemailer from 'nodemailer'
import { htmlToText } from 'nodemailer-html-to-text'
import {
signupTemplate,
resetPasswordTemplate,
wrongAccountTemplate,
emailVerificationTemplate,
} from './templateBuilder'
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
let sendMail = () => {}
if (!hasEmailConfig) {
if (process.env.NODE_ENV !== 'test') {
// eslint-disable-next-line no-console
console.log('Warning: Email middleware will not try to send mails.')
}
} else {
sendMail = async (templateArgs) => {
const transporter = nodemailer.createTransport({
host: CONFIG.SMTP_HOST,
port: CONFIG.SMTP_PORT,
ignoreTLS: CONFIG.SMTP_IGNORE_TLS === 'true',
secure: false, // true for 465, false for other ports
auth: hasAuthData && {
user: CONFIG.SMTP_USERNAME,
pass: CONFIG.SMTP_PASSWORD,
},
})
transporter.use(
'compile',
htmlToText({
ignoreImage: true,
wordwrap: false,
}),
)
await transporter.sendMail(templateArgs)
}
}
const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
const response = await resolve(root, args, context, resolveInfo)
const { email, nonce } = response
await sendMail(signupTemplate({ email, nonce }))
delete response.nonce
return response
}
const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo) => {
const { email } = args
const { email: userFound, nonce, name } = await resolve(root, args, context, resolveInfo)
const template = userFound ? resetPasswordTemplate : wrongAccountTemplate
await sendMail(template({ email, nonce, name }))
return true
}
const sendEmailVerificationMail = async (resolve, root, args, context, resolveInfo) => {
const response = await resolve(root, args, context, resolveInfo)
const { email, nonce, name } = response
await sendMail(emailVerificationTemplate({ email, nonce, name }))
delete response.nonce
return response
}
export default {
Mutation: {
AddEmailAddress: sendEmailVerificationMail,
requestPasswordReset: sendPasswordResetMail,
Signup: sendSignupMail,
SignupByInvitation: sendSignupMail,
},
}

View File

@ -1,77 +0,0 @@
import mustache from 'mustache'
import CONFIG from '../../config'
import * as templates from './templates'
const from = '"Human Connection" <info@human-connection.org>'
const supportUrl = 'https://human-connection.org/en/contact'
export const signupTemplate = ({ email, nonce }) => {
const subject = 'Willkommen, Bienvenue, Welcome to Human Connection!'
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ actionUrl, nonce, supportUrl, subject },
{ content: templates.signup },
),
}
}
export const emailVerificationTemplate = ({ email, nonce, name }) => {
const subject = 'Neue E-Mail Adresse | New E-Mail Address'
const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ actionUrl, name, nonce, supportUrl, subject },
{ content: templates.emailVerification },
),
}
}
export const resetPasswordTemplate = ({ email, nonce, name }) => {
const subject = 'Neues Passwort | Reset Password'
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ actionUrl, name, nonce, supportUrl, subject },
{ content: templates.passwordReset },
),
}
}
export const wrongAccountTemplate = ({ email }) => {
const subject = 'Falsche Mailadresse? | Wrong E-mail?'
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ actionUrl, supportUrl },
{ content: templates.wrongAccount },
),
}
}

View File

@ -1,26 +1,32 @@
import trunc from 'trunc-html'
import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '../constants/groups'
export default {
Mutation: {
CreateGroup: async (resolve, root, args, context, info) => {
args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html
return resolve(root, args, context, info)
},
UpdateGroup: async (resolve, root, args, context, info) => {
if (args.description)
args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html
return resolve(root, args, context, info)
},
CreatePost: async (resolve, root, args, context, info) => {
args.contentExcerpt = trunc(args.content, 120).html
const result = await resolve(root, args, context, info)
return result
return resolve(root, args, context, info)
},
UpdatePost: async (resolve, root, args, context, info) => {
args.contentExcerpt = trunc(args.content, 120).html
const result = await resolve(root, args, context, info)
return result
return resolve(root, args, context, info)
},
CreateComment: async (resolve, root, args, context, info) => {
args.contentExcerpt = trunc(args.content, 180).html
const result = await resolve(root, args, context, info)
return result
return resolve(root, args, context, info)
},
UpdateComment: async (resolve, root, args, context, info) => {
args.contentExcerpt = trunc(args.content, 180).html
const result = await resolve(root, args, context, info)
return result
return resolve(root, args, context, info)
},
},
}

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
@ -30,7 +30,9 @@ const updatePostMutation = gql`
}
`
beforeAll(() => {
beforeAll(async () => {
await cleanDatabase()
const createServerResult = createServer({
context: () => {
return {
@ -46,6 +48,10 @@ beforeAll(() => {
mutate = createTestClientResult.mutate
})
afterAll(async () => {
await cleanDatabase()
})
beforeEach(async () => {
hashtagingUser = await neode.create(
'User',
@ -66,6 +72,7 @@ beforeEach(async () => {
})
})
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
afterEach(async () => {
await cleanDatabase()
})
@ -95,7 +102,7 @@ describe('hashtags', () => {
</p>
`
const postWithHastagsQuery = gql`
query($id: ID) {
query ($id: ID) {
Post(id: $id) {
tags {
id

View File

@ -0,0 +1,93 @@
import sanitizeHtml from 'sanitize-html'
import linkifyHtml from 'linkifyjs/html'
export const removeHtmlTags = (input) => {
return sanitizeHtml(input, {
allowedTags: [],
allowedAttributes: {},
})
}
const standardSanitizeHtmlOptions = {
allowedTags: [
'img',
'p',
'h3',
'h4',
'br',
'hr',
'b',
'i',
'u',
'em',
'strong',
'a',
'pre',
'ul',
'li',
'ol',
's',
'strike',
'span',
'blockquote',
],
allowedAttributes: {
a: ['href', 'class', 'target', 'data-*', 'contenteditable'],
span: ['contenteditable', 'class', 'data-*'],
img: ['src'],
},
allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'],
parser: {
lowerCaseTags: true,
},
transformTags: {
h1: 'h3',
h2: 'h3',
h3: 'h3',
h4: 'h4',
h5: 'strong',
i: 'em',
a: (tagName, attribs) => {
return {
tagName: 'a',
attribs: {
...attribs,
href: attribs.href || '',
target: '_blank',
rel: 'noopener noreferrer nofollow',
},
}
},
b: 'strong',
s: 'strike',
},
}
export function cleanHtml(dirty, _key, sanitizeHtmlOptions = standardSanitizeHtmlOptions) {
if (!dirty) {
return dirty
}
dirty = linkifyHtml(dirty)
dirty = sanitizeHtml(dirty, sanitizeHtmlOptions)
// remove empty html tags and duplicated linebreaks and returns
dirty = dirty
// remove all tags with "space only"
.replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '')
.replace(/[\n]{3,}/gim, '\n\n')
.replace(/(\r\n|\n\r|\r|\n)/g, '<br>$1')
// replace all p tags with line breaks (and spaces) only by single linebreaks
// limit linebreaks to max 2 (equivalent to html "br" linebreak)
.replace(/(<br ?\/?>\s*){2,}/gim, '<br>')
// remove additional linebreaks after p tags
.replace(/<\/(p|div|th|tr)>\s*(<br ?\/?>\s*)+\s*<(p|div|th|tr)>/gim, '</p><p>')
// remove additional linebreaks inside p tags
.replace(/<[a-z-]+>(<[a-z-]+>)*\s*(<br ?\/?>\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim, '')
// remove additional linebreaks when first child inside p tags
.replace(/<p>(\s*<br ?\/?>\s*)+/gim, '<p>')
// remove additional linebreaks when last child inside p tags
.replace(/(\s*<br ?\/?>\s*)+<\/p+>/gim, '</p>')
return dirty
}

View File

@ -0,0 +1,61 @@
import CONFIG from '../../../config'
import { cleanHtml } from '../../../middleware/helpers/cleanHtml.js'
import nodemailer from 'nodemailer'
import { htmlToText } from 'nodemailer-html-to-text'
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
let sendMailCallback = async () => {}
if (!hasEmailConfig) {
if (!CONFIG.TEST) {
// eslint-disable-next-line no-console
console.log('Warning: Middlewares will not try to send mails.')
// TODO: disable e-mail logging on database seeding?
// TODO: implement general logging like 'log4js', see Gradido project: https://github.com/gradido/gradido/blob/master/backend/log4js-config.json
sendMailCallback = async (templateArgs) => {
// eslint-disable-next-line no-console
console.log('--- Log Unsend E-Mail ---')
// eslint-disable-next-line no-console
console.log('To: ' + templateArgs.to)
// eslint-disable-next-line no-console
console.log('From: ' + templateArgs.from)
// eslint-disable-next-line no-console
console.log('Subject: ' + templateArgs.subject)
// eslint-disable-next-line no-console
console.log('Content:')
// eslint-disable-next-line no-console
console.log(
cleanHtml(templateArgs.html, 'dummyKey', {
allowedTags: ['a'],
allowedAttributes: { a: ['href'] },
}).replace(/&amp;/g, '&'),
)
}
}
} else {
sendMailCallback = async (templateArgs) => {
const transporter = nodemailer.createTransport({
host: CONFIG.SMTP_HOST,
port: CONFIG.SMTP_PORT,
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,
},
})
transporter.use(
'compile',
htmlToText({
ignoreImage: true,
wordwrap: false,
}),
)
await transporter.sendMail(templateArgs)
}
}
export const sendMail = sendMailCallback

View File

@ -0,0 +1,113 @@
import mustache from 'mustache'
import CONFIG from '../../../config'
import metadata from '../../../config/metadata.js'
import logosWebapp from '../../../config/logos.js'
import * as templates from './templates'
import * as templatesEN from './templates/en'
import * as templatesDE from './templates/de'
const from = CONFIG.EMAIL_DEFAULT_SENDER
const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI)
const defaultParams = {
welcomeImageUrl,
APPLICATION_NAME: CONFIG.APPLICATION_NAME,
ORGANIZATION_NAME: metadata.ORGANIZATION_NAME,
ORGANIZATION_URL: CONFIG.ORGANIZATION_URL,
supportUrl: CONFIG.SUPPORT_URL,
}
const englishHint = 'English version below!'
export const signupTemplate = ({ email, variables: { nonce, inviteCode = null } }) => {
const subject = `Willkommen, Bienvenue, Welcome to ${CONFIG.APPLICATION_NAME}!`
// dev format example: http://localhost:3000/registration?method=invite-mail&email=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')
}
const renderParams = { ...defaultParams, englishHint, actionUrl, nonce, subject }
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content: templates.signup }),
}
}
export const emailVerificationTemplate = ({ email, variables: { 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('email', email)
actionUrl.searchParams.set('nonce', nonce)
const renderParams = { ...defaultParams, englishHint, actionUrl, name, nonce, subject }
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content: templates.emailVerification }),
}
}
export const resetPasswordTemplate = ({ email, variables: { nonce, name } }) => {
const subject = 'Neues Passwort | Reset Password'
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
const renderParams = { ...defaultParams, englishHint, actionUrl, name, nonce, subject }
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content: templates.passwordReset }),
}
}
export const wrongAccountTemplate = ({ email, _variables = {} }) => {
const subject = 'Falsche Mailadresse? | Wrong E-mail?'
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
const renderParams = { ...defaultParams, englishHint, actionUrl }
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content: templates.wrongAccount }),
}
}
export const notificationTemplate = ({ email, variables: { notification } }) => {
const actionUrl = new URL('/notifications', CONFIG.CLIENT_URI)
const settingsUrl = new URL('/settings/notifications', CONFIG.CLIENT_URI)
const renderParams = { ...defaultParams, name: notification.to.name, settingsUrl, actionUrl }
let content
switch (notification.to.locale) {
case 'de':
content = templatesDE.notification
break
case 'en':
content = templatesEN.notification
break
default:
content = templatesEN.notification
break
}
const subjectUnrendered = content.split('\n')[0].split('"')[1]
const subject = mustache.render(subjectUnrendered, renderParams, {})
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content }),
}
}

View File

@ -0,0 +1,246 @@
import CONFIG from '../../../config'
import logosWebapp from '../../../config/logos.js'
import {
signupTemplate,
emailVerificationTemplate,
resetPasswordTemplate,
wrongAccountTemplate,
notificationTemplate,
} from './templateBuilder'
const englishHint = 'English version below!'
const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI)
const supportUrl = CONFIG.SUPPORT_URL.toString()
let actionUrl, name, settingsUrl
const signupTemplateData = () => ({
email: 'test@example.org',
variables: {
nonce: '12345',
inviteCode: 'AAAAAA',
},
})
const emailVerificationTemplateData = () => ({
email: 'test@example.org',
variables: {
nonce: '12345',
name: 'Mr Example',
},
})
const resetPasswordTemplateData = () => ({
email: 'test@example.org',
variables: {
nonce: '12345',
name: 'Mr Example',
},
})
const wrongAccountTemplateData = () => ({
email: 'test@example.org',
variables: {},
})
const notificationTemplateData = (locale) => ({
email: 'test@example.org',
variables: {
notification: {
to: { name: 'Mr Example', locale },
},
},
})
const textsStandard = [
{
templPropName: 'from',
isContaining: false,
text: CONFIG.EMAIL_DEFAULT_SENDER,
},
{
templPropName: 'to',
isContaining: false,
text: 'test@example.org',
},
// is contained in html
welcomeImageUrl.toString(),
CONFIG.ORGANIZATION_URL,
CONFIG.APPLICATION_NAME,
]
const testEmailData = (emailTemplate, templateBuilder, templateData, texts) => {
if (!emailTemplate) {
emailTemplate = templateBuilder(templateData)
}
texts.forEach((element) => {
if (typeof element === 'object') {
if (element.isContaining) {
expect(emailTemplate[element.templPropName]).toEqual(expect.stringContaining(element.text))
} else {
expect(emailTemplate[element.templPropName]).toEqual(element.text)
}
} else {
expect(emailTemplate.html).toEqual(expect.stringContaining(element))
}
})
return emailTemplate
}
// beforeAll(async () => {
// await cleanDatabase()
// })
// afterAll(async () => {
// await cleanDatabase()
// })
describe('templateBuilder', () => {
describe('signupTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {
const subject = `Willkommen, Bienvenue, Welcome to ${CONFIG.APPLICATION_NAME}!`
const actionUrl = new URL('/registration', CONFIG.CLIENT_URI).toString()
const theSignupTemplateData = signupTemplateData()
const enContent = "Thank you for joining our cause it's awesome to have you on board."
const deContent =
'Danke, dass Du dich angemeldet hast wir freuen uns, Dich dabei zu haben.'
testEmailData(null, signupTemplate, theSignupTemplateData, [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
englishHint,
actionUrl,
theSignupTemplateData.variables.nonce,
theSignupTemplateData.variables.inviteCode,
enContent,
deContent,
supportUrl,
])
})
})
})
describe('emailVerificationTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {
const subject = 'Neue E-Mail Adresse | New E-Mail Address'
const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI).toString()
const theEmailVerificationTemplateData = emailVerificationTemplateData()
const enContent = 'So, you want to change your e-mail? No problem!'
const deContent = 'Du möchtest also deine E-Mail ändern? Kein Problem!'
testEmailData(null, emailVerificationTemplate, theEmailVerificationTemplateData, [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
englishHint,
actionUrl,
theEmailVerificationTemplateData.variables.nonce,
theEmailVerificationTemplateData.variables.name,
enContent,
deContent,
supportUrl,
])
})
})
})
describe('resetPasswordTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {
const subject = 'Neues Passwort | Reset Password'
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI).toString()
const theResetPasswordTemplateData = resetPasswordTemplateData()
const enContent = 'So, you forgot your password? No problem!'
const deContent = 'Du hast also dein Passwort vergessen? Kein Problem!'
testEmailData(null, resetPasswordTemplate, theResetPasswordTemplateData, [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
englishHint,
actionUrl,
theResetPasswordTemplateData.variables.nonce,
theResetPasswordTemplateData.variables.name,
enContent,
deContent,
supportUrl,
])
})
})
})
describe('wrongAccountTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {
const subject = 'Falsche Mailadresse? | Wrong E-mail?'
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI).toString()
const theWrongAccountTemplateData = wrongAccountTemplateData()
const enContent =
"You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address."
const deContent =
'Du hast bei uns ein neues Passwort angefordert leider haben wir aber keinen Account mit Deiner E-Mailadresse gefunden.'
testEmailData(null, wrongAccountTemplate, theWrongAccountTemplateData, [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
englishHint,
actionUrl,
enContent,
deContent,
supportUrl,
])
})
})
})
describe('notificationTemplate', () => {
beforeEach(() => {
actionUrl = new URL('/notifications', CONFIG.CLIENT_URI).toString()
name = notificationTemplateData('en').variables.notification.to.name
settingsUrl = new URL('/settings/notifications', CONFIG.CLIENT_URI)
})
describe('en', () => {
it('e-mail is build with all data', () => {
const subject = `${CONFIG.APPLICATION_NAME} Notification`
const content = 'You received at least one notification. Click on this button to view them:'
testEmailData(null, notificationTemplate, notificationTemplateData('en'), [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
actionUrl,
name,
content,
settingsUrl,
])
})
})
describe('de', () => {
it('e-mail is build with all data', async () => {
const subject = `${CONFIG.APPLICATION_NAME} Benachrichtigung`
const content = `Du hast mindestens eine Benachrichtigung erhalten. Klick auf diesen Button, um sie anzusehen:`
testEmailData(null, notificationTemplate, notificationTemplateData('de'), [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
actionUrl,
name,
content,
settingsUrl,
])
})
})
})
})

View File

@ -0,0 +1,6 @@
import fs from 'fs'
import path from 'path'
const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')
export const notification = readFile('./notification.html')

View File

@ -0,0 +1,83 @@
<!-- emailSubject: "{{APPLICATION_NAME}} Benachrichtigung" -->
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
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>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hallo {{ name }},</h1>
<p style="margin: 0;">Du hast mindestens eine Benachrichtigung erhalten. Klick auf diesen Button, um sie anzusehen:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Benachrichtigungen
ansehen</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0; margin-top: 10px;">PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine <a href="{{{ settingsUrl }}}"
style="color: #17b53e;">Benachrichtigungseinstellungen</a>.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body German : END -->

View File

@ -6,9 +6,9 @@
<tr>
<td style="background-color: #ffffff;">
<img
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
width="600" height="" alt="Human Connection community logo" border="0"
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
src="{{{ welcomeImageUrl }}}"
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>
@ -74,9 +74,9 @@
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein Human Connection Team</p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{{APPLICATION_NAME}}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein {{APPLICATION_NAME}} Team</p>
</td>
</tr>
<tr>
@ -104,9 +104,9 @@
<tr>
<td style="background-color: #ffffff;">
<img
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
width="600" height="" alt="Human Connection community logo" border="0"
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
src="{{{ welcomeImageUrl }}}"
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>
@ -172,9 +172,9 @@
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If the above button doesn't work you can also copy the following code into your
browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The Human Connection Team</p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{{APPLICATION_NAME}}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>

View File

@ -0,0 +1,6 @@
import fs from 'fs'
import path from 'path'
const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')
export const notification = readFile('./notification.html')

View File

@ -0,0 +1,83 @@
<!-- emailSubject: "{{APPLICATION_NAME}} Notification" -->
<!-- Email Body English : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
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>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hello {{ name }},</h1>
<p style="margin: 0;">You received at least one notification. Click on this button to view them:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">View
notifications</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0; margin-top: 10px;">PS: If you don't want to receive e-mails anymore, change your <a href="{{{ settingsUrl }}}"
style="color: #17b53e;">notification settings</a>.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body English : END -->

View File

@ -159,7 +159,7 @@
<td>
<![endif]-->
<p style="color:#19c243; font-style: italic; font-family: Lato, sans-serif; font-size: 16px; padding-top: 20px;">English version below!</p>
<p style="color:#19c243; font-style: italic; font-family: Lato, sans-serif; font-size: 16px; padding-top: 20px;">{{englishHint}}</p>
{{> content}}
@ -169,10 +169,11 @@
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
<br><br>
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
Teck<br>Germany</span>
<br><br>
<br>
<a href="{{{ ORGANIZATION_URL }}}" target="_blank" style="color: #17b53e;">{{ORGANIZATION_NAME}}</a>
<br>
<br>
<br>
</td>
</tr>
</table>

View File

@ -6,9 +6,9 @@
<tr>
<td style="background-color: #ffffff;">
<img
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
width="600" height="" alt="Human Connection community logo" border="0"
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
src="{{{ welcomeImageUrl }}}"
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>
@ -74,9 +74,9 @@
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein Human Connection Team</p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein {{APPLICATION_NAME}} Team</p>
</td>
</tr>
<tr>
@ -104,9 +104,9 @@
<tr>
<td style="background-color: #ffffff;">
<img
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
width="600" height="" alt="Human Connection community logo" border="0"
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
src="{{{ welcomeImageUrl }}}"
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>
@ -171,9 +171,9 @@
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If the above button doesn't work you can also copy the following code into your
browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The Human Connection Team</p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>

View File

@ -6,9 +6,9 @@
<tr>
<td style="background-color: #ffffff;">
<img
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
width="600" height="" alt="Human Connection community logo" border="0"
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
src="{{{ welcomeImageUrl }}}"
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>
@ -23,7 +23,7 @@
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Willkommen bei Human Connection!</h1>
Willkommen bei {{APPLICATION_NAME}}!</h1>
<p style="margin: 0;">Danke, dass Du dich angemeldet hast wir freuen uns, Dich dabei zu haben. Jetzt
fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können ... Bitte bestätige
Deine E-Mail Adresse:</p>
@ -62,8 +62,8 @@
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0;">Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.</p>
<p style="margin: 0; margin-top: 10px;">Falls Du Dich nicht selbst bei <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a> angemeldet hast, schau doch mal vorbei!
<p style="margin: 0; margin-top: 10px;">Falls Du Dich nicht selbst bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a> angemeldet hast, schau doch mal vorbei!
Wir sind ein gemeinnütziges Aktionsnetzwerk von Menschen für Menschen.</p>
<p style="margin: 0; margin-top: 10px;">PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese
E-Mail einfach ignorieren. ;)</p>
@ -87,9 +87,9 @@
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Melde Dich gerne <a href="{{{ supportUrl }}}" style="color: #17b53e;">bei
unserem Support Team</a>, wenn Du Fragen hast.</p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein Human Connection Team</p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein {{APPLICATION_NAME}} Team</p>
</td>
</tr>
<tr>
@ -117,9 +117,9 @@
<tr>
<td style="background-color: #ffffff;">
<img
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
width="600" height="" alt="Human Connection community logo" border="0"
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
src="{{{ welcomeImageUrl }}}"
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>
@ -134,7 +134,7 @@
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Welcome to Human Connection!</h1>
Welcome to {{APPLICATION_NAME}}!</h1>
<p style="margin: 0;">Thank you for joining our cause it's awesome to have you on board. There's
just one tiny step missing before we can start shaping the world together ... Please confirm your
e-mail address by clicking the button below:</p>
@ -173,8 +173,8 @@
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If the above button doesn't work, you can also copy the following code into your browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0;">However, this only works if you have registered through our website.</p>
<p style="margin: 0; margin-top: 10px;">If you didn't sign up for <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a> we recommend you to check it out!
<p style="margin: 0; margin-top: 10px;">If you didn't sign up for <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a> we recommend you to check it out!
It's a social network from people for people who want to connect and change the world together.</p>
<p style="margin: 0; margin-top: 10px;">PS: If you ignore this e-mail we will not create an account
for
@ -200,9 +200,9 @@
<p style="margin: 0;">Feel free to <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our
support team</a> with any
questions you have.</p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The Human Connection Team</p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>

View File

@ -6,9 +6,9 @@
<tr>
<td style="background-color: #ffffff;">
<img
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
width="600" height="" alt="Human Connection community logo" border="0"
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
src="{{{ welcomeImageUrl }}}"
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>
@ -24,8 +24,8 @@
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hallo!</h1>
<p style="margin: 0;">Du hast bei uns ein neues Passwort angefordert leider haben wir aber keinen
Account mit Deiner E-Mailadresse gefunden. Kann es sein, dass Du mit einer anderen Adresse bei uns
<p style="margin: 0;">Du hast bei uns ein neues Passwort angefordert leider haben wir aber keinen Account mit Deiner E-Mailadresse gefunden.
Kann es sein, dass Du mit einer anderen Adresse bei uns
angemeldet bist?</p>
</td>
</tr>
@ -55,8 +55,8 @@
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Wenn Du noch keinen Account bei <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a> hast oder Dein Password gar nicht ändern willst,
<p style="margin: 0;">Wenn Du noch keinen Account bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a> hast oder Dein Password gar nicht ändern willst,
kannst Du diese E-Mail einfach ignorieren!</p>
</td>
</tr>
@ -74,9 +74,9 @@
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Ansonsten hilft Dir <a href="{{{ supportUrl }}}" style="color: #17b53e;">unser
Support Team</a> gerne weiter.</p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein Human Connection Team</p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein {{APPLICATION_NAME}} Team</p>
</td>
</tr>
<tr>
@ -104,9 +104,9 @@
<tr>
<td style="background-color: #ffffff;">
<img
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
width="600" height="" alt="Human Connection community logo" border="0"
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
src="{{{ welcomeImageUrl }}}"
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>
@ -122,8 +122,8 @@
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hello!</h1>
<p style="margin: 0;">You requested a password reset but unfortunately we couldn't find an account
associated with your e-mail address. Did you maybe use another one when you signed up?</p>
<p style="margin: 0;">You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address.
Did you maybe use another one when you signed up?</p>
</td>
</tr>
<tr>
@ -152,8 +152,8 @@
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If you don't have an account at <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a> yet or if you didn't want to reset your password,
<p style="margin: 0;">If you don't have an account at <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a> yet or if you didn't want to reset your password,
please ignore this e-mail.</p>
</td>
</tr>
@ -171,9 +171,9 @@
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Otherwise <a href="{{{ supportUrl }}}" style="color: #17b53e;">our
support team</a> will be happy to help you out.</p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The Human Connection Team</p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>

View File

@ -12,8 +12,10 @@ import orderBy from './orderByMiddleware'
import validation from './validation/validationMiddleware'
import notifications from './notifications/notificationsMiddleware'
import hashtags from './hashtags/hashtagsMiddleware'
import email from './email/emailMiddleware'
import login from './login/loginMiddleware'
import sentry from './sentryMiddleware'
import languages from './languages/languages'
import userInteractions from './userInteractions'
export default (schema) => {
const middlewares = {
@ -24,12 +26,14 @@ export default (schema) => {
validation,
sluggify,
excerpt,
email,
login,
notifications,
hashtags,
softDelete,
includedFields,
orderBy,
languages,
userInteractions,
}
let order = [
@ -38,9 +42,11 @@ export default (schema) => {
'xss',
// 'activityPub', disabled temporarily
'validation',
'userInteractions',
'sluggify',
'languages',
'excerpt',
'email',
'login',
'notifications',
'hashtags',
'softDelete',

View File

@ -0,0 +1,21 @@
import LanguageDetect from 'languagedetect'
import { removeHtmlTags } from '../helpers/cleanHtml.js'
const setPostLanguage = (text) => {
const lngDetector = new LanguageDetect()
lngDetector.setLanguageType('iso2')
return lngDetector.detect(removeHtmlTags(text), 1)[0][0]
}
export default {
Mutation: {
CreatePost: async (resolve, root, args, context, info) => {
args.language = await setPostLanguage(args.content)
return resolve(root, args, context, info)
},
UpdatePost: async (resolve, root, args, context, info) => {
args.language = await setPostLanguage(args.content)
return resolve(root, args, context, info)
},
},
}

View File

@ -0,0 +1,133 @@
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 mutate
let authenticatedUser
let variables
const driver = getDriver()
const neode = getNeode()
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
mutate = createTestClient(server).mutate
})
afterAll(async () => {
await cleanDatabase()
})
const createPostMutation = gql`
mutation ($title: String!, $content: String!, $categoryIds: [ID]) {
CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
language
}
}
`
describe('languagesMiddleware', () => {
variables = {
title: 'Test post languages',
categoryIds: ['cat9'],
}
beforeAll(async () => {
const user = await Factory.build('user')
authenticatedUser = await user.toJson()
await Factory.build('category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
})
})
it('detects German', async () => {
variables = {
...variables,
content: 'Jeder sollte vor seiner eigenen Tür kehren.',
}
await expect(
mutate({
mutation: createPostMutation,
variables,
}),
).resolves.toMatchObject({
data: {
CreatePost: {
language: 'de',
},
},
})
})
it('detects English', async () => {
variables = {
...variables,
content: 'A journey of a thousand miles begins with a single step.',
}
await expect(
mutate({
mutation: createPostMutation,
variables,
}),
).resolves.toMatchObject({
data: {
CreatePost: {
language: 'en',
},
},
})
})
it('detects Spanish', async () => {
variables = {
...variables,
content: 'A caballo regalado, no le mires el diente.',
}
await expect(
mutate({
mutation: createPostMutation,
variables,
}),
).resolves.toMatchObject({
data: {
CreatePost: {
language: 'es',
},
},
})
})
it('detects German in between lots of html tags', async () => {
variables = {
...variables,
content:
'<strong>Jeder</strong> <strike>sollte</strike> <strong>vor</strong> <span>seiner</span> eigenen <blockquote>Tür</blockquote> kehren.',
}
await expect(
mutate({
mutation: createPostMutation,
variables,
}),
).resolves.toMatchObject({
data: {
CreatePost: {
language: 'de',
},
},
})
})
})

View File

@ -0,0 +1,44 @@
import { sendMail } from '../helpers/email/sendMail'
import {
signupTemplate,
resetPasswordTemplate,
wrongAccountTemplate,
emailVerificationTemplate,
} from '../helpers/email/templateBuilder'
const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
const { inviteCode } = args
const response = await resolve(root, args, context, resolveInfo)
const { email, nonce } = response
if (inviteCode) {
await sendMail(signupTemplate({ email, variables: { nonce, inviteCode } }))
} else {
await sendMail(signupTemplate({ email, variables: { nonce } }))
}
delete response.nonce
return response
}
const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo) => {
const { email } = args
const { email: userFound, nonce, name } = await resolve(root, args, context, resolveInfo)
const template = userFound ? resetPasswordTemplate : wrongAccountTemplate
await sendMail(template({ email, variables: { nonce, name } }))
return true
}
const sendEmailVerificationMail = async (resolve, root, args, context, resolveInfo) => {
const response = await resolve(root, args, context, resolveInfo)
const { email, nonce, name } = response
await sendMail(emailVerificationTemplate({ email, variables: { nonce, name } }))
delete response.nonce
return response
}
export default {
Mutation: {
AddEmailAddress: sendEmailVerificationMail,
requestPasswordReset: sendPasswordResetMail,
Signup: sendSignupMail,
},
}

View File

@ -1,21 +1,63 @@
import { pubsub, NOTIFICATION_ADDED } from '../../server'
import extractMentionedUsers from './mentions/extractMentionedUsers'
import { validateNotifyUsers } from '../validation/validationMiddleware'
import { pubsub, NOTIFICATION_ADDED } from '../../server'
import { sendMail } from '../helpers/email/sendMail'
import { notificationTemplate } from '../helpers/email/templateBuilder'
const publishNotifications = async (...promises) => {
const notifications = await Promise.all(promises)
notifications
.flat()
.forEach((notificationAdded) => pubsub.publish(NOTIFICATION_ADDED, { notificationAdded }))
const queryNotificationEmails = async (context, notificationUserIds) => {
if (!(notificationUserIds && notificationUserIds.length)) return []
const userEmailCypher = `
MATCH (user: User)
// blocked users are filtered out from notifications already
WHERE user.id in $notificationUserIds
WITH user
MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress)
RETURN emailAddress {.email}
`
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const emailAddressTransactionResponse = await transaction.run(userEmailCypher, {
notificationUserIds,
})
return emailAddressTransactionResponse.records.map((record) => record.get('emailAddress'))
})
try {
const emailAddresses = await writeTxResultPromise
return emailAddresses
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
}
const publishNotifications = async (context, promises) => {
let notifications = await Promise.all(promises)
notifications = notifications.flat()
const notificationsEmailAddresses = await queryNotificationEmails(
context,
notifications.map((notification) => notification.to.id),
)
notifications.forEach((notificationAdded, index) => {
pubsub.publish(NOTIFICATION_ADDED, { notificationAdded })
if (notificationAdded.to.sendNotificationEmails) {
sendMail(
notificationTemplate({
email: notificationsEmailAddresses[index].email,
variables: { notification: notificationAdded },
}),
)
}
})
}
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
const idsOfUsers = extractMentionedUsers(args.content)
const post = await resolve(root, args, context, resolveInfo)
if (post) {
await publishNotifications(
await publishNotifications(context, [
notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context),
)
])
}
return post
}
@ -26,10 +68,10 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI
const comment = await resolve(root, args, context, resolveInfo)
const [postAuthor] = await postAuthorOfComment(comment.id, { context })
idsOfUsers = idsOfUsers.filter((id) => id !== postAuthor.id)
await publishNotifications(
await publishNotifications(context, [
notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context),
notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context),
)
])
return comment
}

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
@ -37,6 +37,7 @@ const createCommentMutation = gql`
beforeAll(async () => {
await cleanDatabase()
publishSpy = jest.spyOn(pubsub, 'publish')
const createServerResult = createServer({
context: () => {
@ -53,6 +54,10 @@ beforeAll(async () => {
mutate = createTestClientResult.mutate
})
afterAll(async () => {
await cleanDatabase()
})
beforeEach(async () => {
publishSpy.mockClear()
notifiedUser = await neode.create(
@ -74,13 +79,14 @@ beforeEach(async () => {
})
})
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
afterEach(async () => {
await cleanDatabase()
})
describe('notifications', () => {
const notificationQuery = gql`
query($read: Boolean) {
query ($read: Boolean) {
notifications(read: $read, orderBy: updatedAt_desc) {
read
reason
@ -367,7 +373,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

@ -18,6 +18,14 @@ const { server } = createServer({
})
const { query } = createTestClient(server)
beforeAll(async () => {
await cleanDatabase()
})
afterAll(async () => {
await cleanDatabase()
})
beforeEach(async () => {
await neode.create('Post', { title: 'first' })
await neode.create('Post', { title: 'second' })
@ -25,6 +33,7 @@ beforeEach(async () => {
await neode.create('Post', { title: 'last' })
})
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
afterEach(async () => {
await cleanDatabase()
})

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