Merge branch 'master' into 2019/kw15/change_password_strength

This commit is contained in:
Ulf Gebhardt 2019-05-06 13:59:03 +02:00
commit 9fcddfbd97
No known key found for this signature in database
GPG Key ID: 44C888923CC8E7F3
196 changed files with 5401 additions and 2196 deletions

3
.gitbook.yaml Normal file
View File

@ -0,0 +1,3 @@
structure:
readme: README.md
summary: SUMMARY.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
.gitbook/assets/grafik.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

View File

@ -28,7 +28,7 @@ script:
- docker-compose exec webapp yarn run lint - docker-compose exec webapp yarn run lint
- docker-compose exec webapp yarn run test --ci --verbose=false - docker-compose exec webapp yarn run test --ci --verbose=false
- docker-compose exec -d backend yarn run test:before:seeder - docker-compose exec -d backend yarn run test:before:seeder
- yarn run cypress:run --record --key $CYPRESS_TOKEN - yarn run cypress:run
after_success: after_success:
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh

View File

@ -40,7 +40,5 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.4, available at [http://contributor-covenant.org/version/1/4](http://contributor-covenant.org/version/1/4/)
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -1,8 +1,10 @@
Thanks so much for thinking of contributing to the Human Connection project, we really appreciate it! :-) # CONTRIBUTING
### Getting Set Up Thanks so much for thinking of contributing to the Human Connection project, we really appreciate it! :-\)
Instructions for how to install all the necessary software can be found in our [documentation](https://docs.human-connection.org/nitro/) ## Getting Set Up
Instructions for how to install all the necessary software can be found in our [documentation](https://docs.human-connection.org/human-connection/)
We recommend that new folks should ideally work together with an existing developer. Please join our discord instance to chat with developers or just ask them in tickets in [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f/boards?repos=152252353): We recommend that new folks should ideally work together with an existing developer. Please join our discord instance to chat with developers or just ask them in tickets in [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f/boards?repos=152252353):
@ -10,31 +12,28 @@ We recommend that new folks should ideally work together with an existing develo
Here are some general notes on our development flow: Here are some general notes on our development flow:
### Development ## Development
* Currently operating in two week sprints * Currently operating in two week sprints
* We are using ZenHub to coordinate * We are using ZenHub to coordinate
- estimating time per issue is the crucial feature of [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f) that Github does not have * estimating time per issue is the crucial feature of [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f) that Github does not have
- "up-for-grabs" links to [Github project](https://github.com/orgs/Human-Connection/projects/10?card_filter_query=label%3A%22good+first+issue) * "up-for-grabs" links to [Github project](https://github.com/orgs/Human-Connection/projects/10?card_filter_query=label%3A"good+first+issue)
- ordering on ZenHub not necessarily reflected on github projects * ordering on ZenHub not necessarily reflected on github projects
* AgileVentures run open pairing sessions at 10:30am UTC each week on Tuesdays and Thursdays * AgileVentures run open pairing sessions at 10:30am UTC each week on Tuesdays and Thursdays
* Core team * Core team
- all the people who are hired by HC non-profit corporation * all the people who are hired by HC non-profit corporation
- you can Meet-the-team [every two weeks in German](https://human-connection.org/veranstaltungen/) and [every month in English](https://human-connection.org/en/events/). * you can Meet-the-team [every two weeks in German](https://human-connection.org/veranstaltungen/) and [every month in English](https://human-connection.org/en/events/).
- 9 people * 9 people
- 2 core developers (Robert [@roschaefer](https://github.com/roschaefer) and Greg [@appinteractive](https://github.com/appinteractive)) * 2 core developers \(Robert [@roschaefer](https://github.com/roschaefer) and Greg [@appinteractive](https://github.com/appinteractive)\)
- 3 marketeers Jasi, Dennis and Sensi * 3 marketeers Jasi, Dennis and Sensi
- Hardy doing business development * Hardy doing business development
- Martin head of IT and previously data protection officer * Martin head of IT and previously data protection officer
- Victor doing accounting and controlling * Victor doing accounting and controlling
- Nicolas is the community manager (reviews content in the network) reflects community opinion back to the core team * Nicolas is the community manager \(reviews content in the network\) reflects community opinion back to the core team
* when can folks pair with Robert * when can folks pair with Robert
- 10am UTC until 5pm UTC every working day * 10am UTC until 5pm UTC every working day
### Philosophy ## Philosophy
We practise [collective code ownership](http://www.extremeprogramming.org/rules/collective.html) rather than strong code ownership, which means that: We practise [collective code ownership](http://www.extremeprogramming.org/rules/collective.html) rather than strong code ownership, which means that:
@ -45,28 +44,25 @@ We practise [collective code ownership](http://www.extremeprogramming.org/rules/
Everyone feel free to request merges or answers to issues from the project managers Everyone feel free to request merges or answers to issues from the project managers
But what do we do when waiting for merge into master (wanting to keep PRs small) But what do we do when waiting for merge into master \(wanting to keep PRs small\) --> Robert recommends creating a pull request for each step
--> Robert recommends creating a pull request for each step
- programming is also about thinking about other people - empathy for your co-workers
- but what about when you are waiting for merge?
- solutions
- 1) put 2nd PR into branch that the first PR is hitting - but requires update after merging
- 2) prefer to leave exiting PR until it can be reviewed, and instead go and work on some other part of the codebase that is not impacted by the first PR
### Notes * programming is also about thinking about other people - empathy for your co-workers
* but what about when you are waiting for merge?
* solutions
* 1\) put 2nd PR into branch that the first PR is hitting - but requires update after merging
* 2\) prefer to leave exiting PR until it can be reviewed, and instead go and work on some other part of the codebase that is not impacted by the first PR
question: when you want to pick a task - (find out priority) - is it in discord? is it in AV slack? --> Robert says you can always ask in discord - group channels are the best ## Notes
Robert shares: [Zenhub board](https://app.zenhub.com/workspaces/nitro-embed-5c0154ecc699f60fc92cf11f/boards?repos=112590397,152252353,152252578,157710732,163305928) question: when you want to pick a task - \(find out priority\) - is it in discord? is it in AV slack? --> Robert says you can always ask in discord - group channels are the best
Robert says the order of tickets are preserved in ZenHub and reflect their priority (most important at the top) and so check out the current milestones
Matt - question about who can work on [ticket 100](https://app.zenhub.com/workspaces/nitro-embed-5c0154ecc699f60fc92cf11f/issues/human-connection/human-connection/100) --> Robert - in rare occasions it might be exclusive to someone with admin permissions Robert shares: [Zenhub board](https://app.zenhub.com/workspaces/nitro-embed-5c0154ecc699f60fc92cf11f/boards?repos=112590397,152252353,152252578,157710732,163305928) Robert says the order of tickets are preserved in ZenHub and reflect their priority \(most important at the top\) and so check out the current milestones
Robert: notes greg just pushed this today: https://github.com/Human-Connection/Nitro-Deployment
Matt - question about who can work on [ticket 100](https://app.zenhub.com/workspaces/nitro-embed-5c0154ecc699f60fc92cf11f/issues/human-connection/human-connection/100) --> Robert - in rare occasions it might be exclusive to someone with admin permissions Robert: notes greg just pushed this today: [https://github.com/Human-Connection/Nitro-Deployment](https://github.com/Human-Connection/Nitro-Deployment)
Matt makes point that new stories will have to be taken off the "New Issues" and Robert says that's fine, if you don't like the first one, then you can take the next one. Volunteeers have no commitment except their own self development and their awesomeness by contributing to free and open-source software projects. Matt makes point that new stories will have to be taken off the "New Issues" and Robert says that's fine, if you don't like the first one, then you can take the next one. Volunteeers have no commitment except their own self development and their awesomeness by contributing to free and open-source software projects.
Robert notes that everyone is invited to join the kickoff meetings Robert notes that everyone is invited to join the kickoff meetings
Robert - difference between "important" (creates a lot of value) and "beginner friendly" (easy to implement) Robert - difference between "important" \(creates a lot of value\) and "beginner friendly" \(easy to implement\)

View File

@ -1,21 +1,12 @@
# LICENSE
MIT License MIT License
Copyright (c) 2018 Human-Connection gGmbH Copyright \(c\) 2018 Human-Connection gGmbH
Permission is hereby granted, free of charge, to any person obtaining a copy 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:
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 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
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.

View File

@ -1,20 +1,25 @@
<p align="center">
<a href="https://human-connection.org"><img align="center" src="https://github.com/Human-Connection/Human-Connection/blob/master/lets_get_together_2.png" alt="Human-Connection" /></a>
</p>
# Human-Connection # Human-Connection
[![Build Status](https://travis-ci.com/Human-Connection/Human-Connection.svg?branch=master)](https://travis-ci.com/Human-Connection/Human-Connection) [![Build Status](https://travis-ci.com/Human-Connection/Human-Connection.svg?branch=master)](https://travis-ci.com/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://discord.gg/6ub73U3)
Human Connection is a free and open-source social network for active citizenship. 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.
* **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**.
[![Human-Connection](.gitbook/assets/lets_get_together.png)](https://human-connection.org)
**Technology Stack** **Technology Stack**
- [VueJS](https://vuejs.org/)
- [NuxtJS](https://nuxtjs.org/) * [VueJS](https://vuejs.org/)
- [GraphQL](https://graphql.org/) * [NuxtJS](https://nuxtjs.org/)
- [NodeJS](https://nodejs.org/en/) * [GraphQL](https://graphql.org/)
- [Neo4J](https://neo4j.com/) * [NodeJS](https://nodejs.org/en/)
* [Neo4J](https://neo4j.com/)
## Live demo ## Live demo
@ -23,17 +28,30 @@ Try out our deployed [staging environment](https://nitro-staging.human-connectio
Logins: Logins:
| email | password | role | | email | password | role |
| --- | --- | --- | | :--- | :--- | :--- |
| `user@example.org` | 1234 | user | | `user@example.org` | 1234 | user |
| `moderator@example.org` | 1234 | moderator | | `moderator@example.org` | 1234 | moderator |
| `admin@example.org` | 1234 | admin | | `admin@example.org` | 1234 | admin |
## Documentation ## Documentation
Learn how to set up a local development environment in our [Docs](https://docs.human-connection.org/nitro).
Learn how to set up a local development environment in our [Docs](https://docs.human-connection.org/human-connection/) :mag_right:
## Translations ## Translations
Contributre translations on [lokalise.co](https://lokalise.co/public/556252725c18dd752dd546.13222042/).
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:.
## Developer Chat ## Developer Chat
Join the open-source community on [Discord](https://discord.gg/6ub73U3).
Join our friendly open-source community on [Discord](https://discord.gg/6ub73U3) :heart_eyes_cat:
Just introduce yourself at `#user-presentation` and mention `@@Mentor` to get you onboard :neckbeard:
Check out the [contribution guideline](./CONTRIBUTING.md), too!
## Attributions
Locale Icons made by [Freepik](http://www.freepik.com/) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/)
## License
See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT).

35
SUMMARY.md Normal file
View File

@ -0,0 +1,35 @@
# Table of contents
* [Introduction](README.md)
* [Edit this Documentation](edit-this-documentation.md)
* [Installation](installation.md)
* [Backend](backend/README.md)
* [GraphQL](backend/graphql.md)
* [Webapp](webapp/README.md)
* [COMPONENTS](webapp/components.md)
* [PLUGINS](webapp/plugins.md)
* [STORE](webapp/store.md)
* [PAGES](webapp/pages.md)
* [ASSETS](webapp/assets.md)
* [LAYOUTS](webapp/layouts.md)
* [Styleguide](webapp/styleguide.md)
* [STATIC](webapp/static.md)
* [MIDDLEWARE](webapp/middleware.md)
* [Testing Guide](testing.md)
* [End-to-end tests](cypress/README.md)
* [Frontend tests](webapp/testing.md)
* [Backend tests](backend/testing.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)
* [Volumes](deployment/volumes/README.md)
* [Neo4J DB Backup](deployment/backup.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

@ -15,7 +15,7 @@ node_modules/
scripts/ scripts/
dist/ dist/
db-migration-worker/ maintenance-worker/
neo4j/ neo4j/
public/uploads/* public/uploads/*

View File

@ -1,196 +1,152 @@
<p align="center"> # Backend
<a href="https://human-connection.org"><img align="center" src="humanconnection.png" height="200" alt="Human Connection" /></a>
</p>
# NITRO Backend ## Installation
[![Build Status](https://img.shields.io/travis/com/Human-Connection/Nitro-Backend/master.svg)](https://travis-ci.com/Human-Connection/Nitro-Backend) {% tabs %}
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitro-Backend/blob/backend/LICENSE.md) {% tab title="Docker" %}
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_shield)
[![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/6ub73U3)
> This Prototype tries to resolve the biggest hurdle of connecting Run the following command to install everything through docker.
> our services together. This is not possible in a sane way using
> our current approach.
>
> With this Prototype we can explore using the combination of
> GraphQL and the Neo4j Graph Database for achieving the connected
> nature of a social graph with better development experience as we
> do not need to connect data by our own any more through weird table
> structures etc.
> The installation takes a bit longer on the first pass or on rebuild ...
> #### Advantages:
> - easer data structure
> - better connected data
> - easy to achieve "recommendations" based on actions (relations)
> - more performant and better to understand API
> - better API client that uses caching
>
> We still need to evaluate the drawbacks and estimate the development
> cost of such an approach
## How to get in touch ```bash
Connect with other developers over [Discord](https://discord.gg/6ub73U3) $ docker-compose up
## Quick Start # rebuild the containers for a cleanup
$ docker-compose up --build
### Requirements
Node >= `v10.12.0`
``` ```
node --version Open another terminal and create unique indices with:
```bash
$ docker-compose exec neo4j migrate
``` ```
### Forking the repository {% endtab %}
Before you start, fork the repository using the fork button above, then clone it to your local machine using `git clone https://github.com/your-username/Nitro-Backend.git`
### Installation with Docker {% tab title="Without Docker" %}
Run: For the local installation you need a recent version of [node](https://nodejs.org/en/)
```sh (&gt;= `v10.12.0`) and [Neo4J](https://neo4j.com/) along with
docker-compose up [Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures) plugin installed
on your system.
# create indices etc.
docker-compose exec neo4j migrate
# if you want seed data
# open another terminal and run
docker-compose exec backend yarn run db:seed
```
App is [running on port 4000](http://localhost:4000/)
To wipe out your neo4j database run:
```sh
docker-compose down -v
```
### Installation without Docker
Install dependencies:
Download [Neo4j Community Edition](https://neo4j.com/download-center/#releases) and unpack the files. Download [Neo4j Community Edition](https://neo4j.com/download-center/#releases) and unpack the files.
Download [Neo4j Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases) and drop the file into the `plugins` folder of the just extracted Neo4j-Server Download [Neo4j Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases) and drop the file into the `plugins` folder of the just extracted Neo4j-Server
Note that grand-stack-starter does not currently bundle a distribution of Neo4j. You can download [Neo4j Desktop](https://neo4j.com/download/) and run locally for development, spin up a [hosted Neo4j Sandbox instance](https://neo4j.com/download/), run Neo4j in one of the [many cloud options](https://neo4j.com/developer/guide-cloud-deployment/), [spin up Neo4j in a Docker container](https://neo4j.com/developer/docker/) or on Debian-based systems install [Neo4j from the Debian Repository](http://debian.neo4j.org/). Just be sure to update the Neo4j connection string and credentials accordingly in `.env`.
Start Neo4J and confirm the database is running at [http://localhost:7474](http://localhost:7474).
Start Neo4j Now install node dependencies with [yarn](https://yarnpkg.com/en/):
```bash
$ cd backend
$ yarn install
``` ```
neo4j\bin\neo4j start
```
and confirm it's running [here](http://localhost:7474)
Copy Environment Variables:
```bash
# in backend/
$ cp .env.template .env
```
Configure the new files according to your needs and your local setup.
Create unique indices with:
```bash ```bash
yarn install $ ./neo4j/migrate.sh
# -or-
npm install
``` ```
Copy: Start the backend for development with:
```
cp .env.template .env
```
Configure the file `.env` according to your needs and your local setup.
Start the GraphQL service:
```bash ```bash
yarn dev $ yarn run dev
# -or-
npm dev
``` ```
And on the production machine run following: or start the backend in production environment with:
```bash ```bash
yarn start yarn run start
# -or-
npm start
``` ```
This will start the GraphQL service (by default on localhost:4000) {% endtab %}
where you can issue GraphQL requests or access GraphQL Playground in the browser: {% endtabs %}
![GraphQL Playground](graphql-playground.png) 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.
## Configure ![GraphQL Playground](../.gitbook/assets/graphql-playground.png)
Set your Neo4j connection string and credentials in `.env`. You can access Neo4J through [http://localhost:7474/](http://localhost:7474/)
For example: for an interactive `cypher` shell and a visualization of the graph.
_.env_
```yaml #### Seed Database
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=letmein
```
> You need to install APOC as a plugin for the graph you create in the neo4j desktop app! If you want your backend to return anything else than an empty response, you
need to seed your database:
Note that grand-stack-starter does not currently bundle a distribution {% tabs %}
of Neo4j. You can download [Neo4j Desktop](https://neo4j.com/download/) {% tab title="Docker" %}
and run locally for development, spin up a [hosted Neo4j Sandbox instance](https://neo4j.com/download/),
run Neo4j in one of the [many cloud options](https://neo4j.com/developer/guide-cloud-deployment/),
[spin up Neo4j in a Docker container](https://neo4j.com/developer/docker/) or on Debian-based systems install [Neo4j from the Debian Repository](http://debian.neo4j.org/).
Just be sure to update the Neo4j connection string and credentials accordingly in `.env`.
## Mock API Results
Alternatively you can just mock all responses from the api which let
you build a frontend application without running a neo4j instance.
Just set `MOCK=true` inside `.env` or pass it on application start.
## Seed and Reset the Database
Optionally you can seed the GraphQL service by executing mutations that
will write sample data to the database:
In another terminal run:
```bash ```bash
yarn run db:seed $ docker-compose exec backend yarn run db:seed
# -or-
npm run db:seed
``` ```
For a reset you can use the reset script: To reset the database run:
```bash ```bash
yarn db:reset $ docker-compose exec backend yarn run db:reset
# -or- # you could also wipe out your neo4j database and delete all volumes with:
npm run db:reset $ docker-compose down -v
```
{% endtab %}
{% tab title="Without Docker" %}
Run:
```bash
$ yarn run db:seed
``` ```
## Run Tests To reset the database run:
```bash
$ yarn run db:reset
```
{% endtab %}
{% endtabs %}
# 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! **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!
Run the **_jest_** tests:
{% tabs %}
{% tab title="Docker" %}
Run the _**jest**_ tests:
```bash ```bash
yarn run test $ docker-compose exec backend yarn run test:jest
# -or-
npm run test
```
Run the **_cucumber_** features:
```bash
yarn run test:cucumber
# -or-
npm run test:cucumber
``` ```
When some tests fail, try `yarn db:reset` and after that `yarn db:seed`. Then run the tests again Run the _**cucumber**_ features:
## Todo`s
- [x] add jwt authentication ```bash
- [ ] get directives working correctly (@toLower, @auth, @role, etc.) $ docker-compose exec backend yarn run test:cucumber
- [x] check if search is working ```
- [x] check if sorting is working
- [x] check if pagination is working
- [ ] check if upload is working (using graphql-yoga?)
- [x] evaluate middleware
- [ ] ignore Posts and Comments by blacklisted Users
{% endtab %}
## License {% tab title="Without Docker" %}
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_large)
Run the _**jest**_ tests:
```bash
$ yarn run test:jest
```
Run the _**cucumber**_ features:
```bash
$ yarn run test:cucumber
```
{% endtab %}
{% endtabs %}

View File

@ -1,13 +0,0 @@
FROM mongo:4
RUN apt-get update && apt-get -y install --no-install-recommends wget apt-transport-https \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN wget -O - https://debian.neo4j.org/neotechnology.gpg.key | apt-key add -
RUN echo 'deb https://debian.neo4j.org/repo stable/' | tee /etc/apt/sources.list.d/neo4j.list
RUN apt-get update && apt-get -y install --no-install-recommends openjdk-8-jre openssh-client neo4j rsync \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY migration ./migration
COPY migrate.sh /usr/local/bin/migrate
COPY sync_uploads.sh /usr/local/bin/sync_uploads

View File

@ -1,18 +0,0 @@
version: "3.7"
services:
neo4j:
environment:
- NEO4J_AUTH=none
ports:
- 7687:7687
- 7474:7474
backend:
ports:
- 4001:4001
- 4123:4123
image: humanconnection/nitro-backend:builder
build:
context: .
target: builder
command: yarn run test:cypress

View File

@ -1,36 +0,0 @@
version: "3.7"
services:
backend:
volumes:
- uploads:/nitro-backend/public/uploads
neo4j:
volumes:
- mongo-export:/mongo-export
environment:
- NEO4J_apoc_import_file_enabled=true
db-migration-worker:
build:
context: db-migration-worker
volumes:
- mongo-export:/mongo-export
- uploads:/uploads
- ./db-migration-worker/migration/:/migration
- ./db-migration-worker/.ssh/:/root/.ssh/
networks:
- hc-network
depends_on:
- backend
environment:
- NEO4J_URI=bolt://neo4j:7687
- "SSH_USERNAME=${SSH_USERNAME}"
- "SSH_HOST=${SSH_HOST}"
- "MONGODB_USERNAME=${MONGODB_USERNAME}"
- "MONGODB_PASSWORD=${MONGODB_PASSWORD}"
- "MONGODB_AUTH_DB=${MONGODB_AUTH_DB}"
- "MONGODB_DATABASE=${MONGODB_DATABASE}"
- "UPLOADS_DIRECTORY=${UPLOADS_DIRECTORY}"
volumes:
mongo-export:
uploads:

View File

@ -1,23 +0,0 @@
version: "3.7"
services:
backend:
image: humanconnection/nitro-backend:builder
build:
context: .
target: builder
volumes:
- .:/nitro-backend
- /nitro-backend/node_modules
command: yarn run dev
neo4j:
environment:
- NEO4J_AUTH=none
ports:
- 7687:7687
- 7474:7474
volumes:
- neo4j-data:/data
volumes:
neo4j-data:

View File

@ -1,14 +0,0 @@
version: "3.7"
services:
neo4j:
environment:
- NEO4J_AUTH=none
ports:
- 7687:7687
- 7474:7474
backend:
image: humanconnection/nitro-backend:builder
build:
context: .
target: builder

View File

@ -1,34 +0,0 @@
version: "3.7"
services:
backend:
image: humanconnection/nitro-backend:latest
build:
context: .
target: production
networks:
- hc-network
depends_on:
- neo4j
ports:
- 4000:4000
environment:
- NEO4J_URI=bolt://neo4j:7687
- GRAPHQL_PORT=4000
- GRAPHQL_URI=http://localhost:4000
- CLIENT_URI=http://localhost:3000
- JWT_SECRET=b/&&7b78BF&fv/Vd
- MOCK=false
- MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ
- PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78
neo4j:
image: humanconnection/neo4j:latest
build:
context: neo4j
networks:
- hc-network
networks:
hc-network:
name: hc-network

18
backend/graphql.md Normal file
View File

@ -0,0 +1,18 @@
# GraphQL with Apollo
GraphQL is a data query language which provides an alternative to REST and ad-hoc web service architectures. It allows clients to define the structure of the data required, and exactly the same structure of the data is returned from the server.
![GraphQL Playground](../../../.gitbook/assets/graphql-playground%20%281%29.png)
## Middleware keeps resolvers clean
![](../.gitbook/assets/grafik-4.png)
A well-organized codebase is key for the ability to maintain and easily introduce changes into an app. Figuring out the right structure for your code remains a continuous challenge - especially as an application grows and more developers are joining a project.
A common problem in GraphQL servers is that resolvers often get cluttered with business logic, making the entire resolver system harder to understand and maintain.
GraphQL Middleware uses the [_middleware pattern_](https://dzone.com/articles/understanding-middleware-pattern-in-expressjs) \(well-known from Express.js\) to pull out repetitive code from resolvers and execute it before or after one of your resolvers is invoked. This improves code modularity and keeps your resolvers clean and simple.

View File

@ -11,7 +11,7 @@
"lint": "eslint src --config .eslintrc.js", "lint": "eslint src --config .eslintrc.js",
"test": "run-s test:jest test:cucumber", "test": "run-s test:jest test:cucumber",
"test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 yarn run dev 2> /dev/null", "test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 yarn run dev 2> /dev/null",
"test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions,activityPub yarn run dev", "test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions,activityPub yarn run dev 2> /dev/null",
"test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand", "test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand",
"test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/", "test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/",
"test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand", "test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand",
@ -50,7 +50,7 @@
"graphql-custom-directives": "~0.2.14", "graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1", "graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.2", "graphql-middleware": "~3.0.2",
"graphql-shield": "~5.3.2", "graphql-shield": "~5.3.4",
"graphql-tag": "~2.10.1", "graphql-tag": "~2.10.1",
"graphql-yoga": "~1.17.4", "graphql-yoga": "~1.17.4",
"helmet": "~3.16.0", "helmet": "~3.16.0",
@ -60,22 +60,22 @@
"ms": "~2.1.1", "ms": "~2.1.1",
"neo4j-driver": "~1.7.3", "neo4j-driver": "~1.7.3",
"neo4j-graphql-js": "~2.4.2", "neo4j-graphql-js": "~2.4.2",
"node-fetch": "~2.3.0", "node-fetch": "~2.4.1",
"npm-run-all": "~4.1.5", "npm-run-all": "~4.1.5",
"request": "~2.88.0", "request": "~2.88.0",
"sanitize-html": "~1.20.0", "sanitize-html": "~1.20.1",
"slug": "~1.1.0", "slug": "~1.1.0",
"trunc-html": "~1.1.2", "trunc-html": "~1.1.2",
"uuid": "~3.3.2", "uuid": "~3.3.2",
"wait-on": "~3.2.0" "wait-on": "~3.2.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "~7.2.3", "@babel/cli": "~7.4.4",
"@babel/core": "~7.4.3", "@babel/core": "~7.4.4",
"@babel/node": "~7.2.2", "@babel/node": "~7.2.2",
"@babel/plugin-proposal-throw-expressions": "^7.2.0", "@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.4.3", "@babel/preset-env": "~7.4.4",
"@babel/register": "~7.4.0", "@babel/register": "~7.4.4",
"apollo-server-testing": "~2.4.8", "apollo-server-testing": "~2.4.8",
"babel-core": "~7.0.0-0", "babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.1", "babel-eslint": "~10.0.1",
@ -84,8 +84,8 @@
"cucumber": "~5.1.0", "cucumber": "~5.1.0",
"eslint": "~5.16.0", "eslint": "~5.16.0",
"eslint-config-standard": "~12.0.0", "eslint-config-standard": "~12.0.0",
"eslint-plugin-import": "~2.16.0", "eslint-plugin-import": "~2.17.2",
"eslint-plugin-jest": "~22.4.1", "eslint-plugin-jest": "~22.5.1",
"eslint-plugin-node": "~8.0.1", "eslint-plugin-node": "~8.0.1",
"eslint-plugin-promise": "~4.1.1", "eslint-plugin-promise": "~4.1.1",
"eslint-plugin-standard": "~4.0.0", "eslint-plugin-standard": "~4.0.0",

View File

@ -6,8 +6,12 @@ import statistics from './resolvers/statistics.js'
import reports from './resolvers/reports.js' import reports from './resolvers/reports.js'
import posts from './resolvers/posts.js' import posts from './resolvers/posts.js'
import moderation from './resolvers/moderation.js' import moderation from './resolvers/moderation.js'
import follow from './resolvers/follow.js'
import shout from './resolvers/shout.js'
import rewards from './resolvers/rewards.js' import rewards from './resolvers/rewards.js'
import socialMedia from './resolvers/socialMedia.js'
import notifications from './resolvers/notifications' import notifications from './resolvers/notifications'
import comments from './resolvers/comments'
export const typeDefs = fs export const typeDefs = fs
.readFileSync( .readFileSync(
@ -19,14 +23,19 @@ export const resolvers = {
Query: { Query: {
...statistics.Query, ...statistics.Query,
...userManagement.Query, ...userManagement.Query,
...notifications.Query ...notifications.Query,
...comments.Query
}, },
Mutation: { Mutation: {
...userManagement.Mutation, ...userManagement.Mutation,
...reports.Mutation, ...reports.Mutation,
...posts.Mutation, ...posts.Mutation,
...moderation.Mutation, ...moderation.Mutation,
...follow.Mutation,
...shout.Mutation,
...rewards.Mutation, ...rewards.Mutation,
...notifications.Mutation ...socialMedia.Mutation,
...notifications.Mutation,
...comments.Mutation
} }
} }

View File

@ -10,17 +10,19 @@ import permissionsMiddleware from './permissionsMiddleware'
import userMiddleware from './userMiddleware' import userMiddleware from './userMiddleware'
import includedFieldsMiddleware from './includedFieldsMiddleware' import includedFieldsMiddleware from './includedFieldsMiddleware'
import orderByMiddleware from './orderByMiddleware' import orderByMiddleware from './orderByMiddleware'
import notificationsMiddleware from './notificationsMiddleware' import validUrlMiddleware from './validUrlMiddleware'
import notificationsMiddleware from './notifications'
export default schema => { export default schema => {
let middleware = [ let middleware = [
passwordMiddleware, passwordMiddleware,
dateTimeMiddleware, dateTimeMiddleware,
validUrlMiddleware,
sluggifyMiddleware, sluggifyMiddleware,
excerptMiddleware, excerptMiddleware,
notificationsMiddleware,
xssMiddleware, xssMiddleware,
fixImageUrlsMiddleware, fixImageUrlsMiddleware,
notificationsMiddleware,
softDeleteMiddleware, softDeleteMiddleware,
userMiddleware, userMiddleware,
includedFieldsMiddleware, includedFieldsMiddleware,

View File

@ -0,0 +1,17 @@
import cheerio from 'cheerio'
const ID_REGEX = /\/profile\/([\w\-.!~*'"(),]+)/g
export default function (content) {
const $ = cheerio.load(content)
const urls = $('.mention').map((_, el) => {
return $(el).attr('href')
}).get()
const ids = []
urls.forEach((url) => {
let match
while ((match = ID_REGEX.exec(url)) != null) {
ids.push(match[1])
}
})
return ids
}

View File

@ -0,0 +1,46 @@
import extractIds from './extractMentions'
describe('extract', () => {
describe('searches through links', () => {
it('ignores links without .mention class', () => {
const content = '<p>Something inspirational about <a href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual([])
})
describe('given a link with .mention class', () => {
it('extracts ids', () => {
const content = '<p>Something inspirational about <a href="/profile/u2" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual(['u2', 'u3'])
})
describe('handles links', () => {
it('with slug and id', () => {
const content = '<p>Something inspirational about <a href="/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual(['u2', 'u3'])
})
it('with domains', () => {
const content = '<p>Something inspirational about <a href="http://localhost:3000/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual(['u2', 'u3'])
})
it('special characters', () => {
const content = '<p>Something inspirational about <a href="http://localhost:3000/profile/u!*(),2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u.~-3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual(['u!*(),2', 'u.~-3'])
})
})
describe('does not crash if', () => {
it('`href` contains no user id', () => {
const content = '<p>Something inspirational about <a href="/profile" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual([])
})
it('`href` is empty or invalid', () => {
const content = '<p>Something inspirational about <a href="" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="not-a-url" class="mention" target="_blank">@jenny-rostock</a>.</p>'
expect(extractIds(content)).toEqual([])
})
})
})
})
})

View File

@ -1,20 +1,22 @@
import { extractSlugs } from './notifications/mentions' import extractIds from './extractMentions'
const notify = async (resolve, root, args, context, resolveInfo) => { const notify = async (resolve, root, args, context, resolveInfo) => {
// extract user ids before xss-middleware removes link classes
const ids = extractIds(args.content)
const post = await resolve(root, args, context, resolveInfo) const post = await resolve(root, args, context, resolveInfo)
const session = context.driver.session() const session = context.driver.session()
const { content, id: postId } = post const { id: postId } = post
const slugs = extractSlugs(content)
const createdAt = (new Date()).toISOString() const createdAt = (new Date()).toISOString()
const cypher = ` const cypher = `
match(u:User) where u.slug in $slugs match(u:User) where u.id in $ids
match(p:Post) where p.id = $postId match(p:Post) where p.id = $postId
create(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt}) create(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt})
merge (n)-[:NOTIFIED]->(u) merge (n)-[:NOTIFIED]->(u)
merge (p)-[:NOTIFIED]->(n) merge (p)-[:NOTIFIED]->(n)
` `
await session.run(cypher, { slugs, createdAt, postId }) await session.run(cypher, { ids, createdAt, postId })
session.close() session.close()
return post return post
@ -22,6 +24,7 @@ const notify = async (resolve, root, args, context, resolveInfo) => {
export default { export default {
Mutation: { Mutation: {
CreatePost: notify CreatePost: notify,
UpdatePost: notify
} }
} }

View File

@ -1,10 +0,0 @@
const MENTION_REGEX = /\s@([\w_-]+)/g
export function extractSlugs (content) {
let slugs = []
let match
while ((match = MENTION_REGEX.exec(content)) != null) {
slugs.push(match[1])
}
return slugs
}

View File

@ -1,30 +0,0 @@
import { extractSlugs } from './mentions'
describe('extract', () => {
describe('finds mentions in the form of', () => {
it('@user', () => {
const content = 'Hello @user'
expect(extractSlugs(content)).toEqual(['user'])
})
it('@user-with-dash', () => {
const content = 'Hello @user-with-dash'
expect(extractSlugs(content)).toEqual(['user-with-dash'])
})
it('@user.', () => {
const content = 'Hello @user.'
expect(extractSlugs(content)).toEqual(['user'])
})
it('@user-With-Capital-LETTERS', () => {
const content = 'Hello @user-With-Capital-LETTERS'
expect(extractSlugs(content)).toEqual(['user-With-Capital-LETTERS'])
})
})
it('ignores email addresses', () => {
const content = 'Hello somebody@example.org'
expect(extractSlugs(content)).toEqual([])
})
})

View File

@ -0,0 +1,124 @@
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../../jest/helpers'
import Factory from '../../seed/factories'
const factory = Factory()
let client
beforeEach(async () => {
await factory.create('User', {
id: 'you',
name: 'Al Capone',
slug: 'al-capone',
email: 'test@example.org',
password: '1234'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('currentUser { notifications }', () => {
const query = `query($read: Boolean) {
currentUser {
notifications(read: $read, orderBy: createdAt_desc) {
read
post {
content
}
}
}
}`
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
describe('given another user', () => {
let authorClient
let authorParams
let authorHeaders
beforeEach(async () => {
authorParams = {
email: 'author@example.org',
password: '1234',
id: 'author'
}
await factory.create('User', authorParams)
authorHeaders = await login(authorParams)
})
describe('who mentions me in a post', () => {
let post
const title = 'Mentioning Al Capone'
const content = 'Hey <a class="mention" href="/profile/you/al-capone">@al-capone</a> how do you do?'
beforeEach(async () => {
const createPostMutation = `
mutation($title: String!, $content: String!) {
CreatePost(title: $title, content: $content) {
id
title
content
}
}
`
authorClient = new GraphQLClient(host, { headers: authorHeaders })
const { CreatePost } = await authorClient.request(createPostMutation, { title, content })
post = CreatePost
})
it('sends you a notification', async () => {
const expectedContent = 'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
const expected = {
currentUser: {
notifications: [
{ read: false, post: { content: expectedContent } }
]
}
}
await expect(client.request(query, { read: false })).resolves.toEqual(expected)
})
describe('who mentions me again', () => {
beforeEach(async () => {
const updatedContent = `${post.content} One more mention to <a href="/profile/you" class="mention">@al-capone</a>`
// The response `post.content` contains a link but the XSSmiddleware
// should have the `mention` CSS class removed. I discovered this
// during development and thought: A feature not a bug! This way we
// can encode a re-mentioning of users when you edit your post or
// comment.
const createPostMutation = `
mutation($id: ID!, $content: String!) {
UpdatePost(id: $id, content: $content) {
title
content
}
}
`
authorClient = new GraphQLClient(host, { headers: authorHeaders })
await authorClient.request(createPostMutation, { id: post.id, content: updatedContent })
})
it('creates exactly one more notification', async () => {
const expectedContent = 'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do? One more mention to <a href="/profile/you" target="_blank">@al-capone</a>'
const expected = {
currentUser: {
notifications: [
{ read: false, post: { content: expectedContent } },
{ read: false, post: { content: expectedContent } }
]
}
}
await expect(client.request(query, { read: false })).resolves.toEqual(expected)
})
})
})
})
})
})

View File

@ -1,85 +0,0 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
let client
beforeEach(async () => {
await factory.create('User', {
id: 'you',
name: 'Al Capone',
slug: 'al-capone',
email: 'test@example.org',
password: '1234'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('currentUser { notifications }', () => {
const query = `query($read: Boolean) {
currentUser {
notifications(read: $read, orderBy: createdAt_desc) {
read
post {
content
}
}
}
}`
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
describe('given another user', () => {
let authorClient
let authorParams
let authorHeaders
beforeEach(async () => {
authorParams = {
email: 'author@example.org',
password: '1234',
id: 'author'
}
await factory.create('User', authorParams)
authorHeaders = await login(authorParams)
})
describe('who mentions me in a post', () => {
beforeEach(async () => {
const content = 'Hey @al-capone how do you do?'
const title = 'Mentioning Al Capone'
const createPostMutation = `
mutation($title: String!, $content: String!) {
CreatePost(title: $title, content: $content) {
title
content
}
}
`
authorClient = new GraphQLClient(host, { headers: authorHeaders })
await authorClient.request(createPostMutation, { title, content })
})
it('sends you a notification', async () => {
const expected = {
currentUser: {
notifications: [
{ read: false, post: { content: 'Hey @al-capone how do you do?' } }
]
}
}
await expect(client.request(query, { read: false })).resolves.toEqual(expected)
})
})
})
})
})

View File

@ -11,10 +11,11 @@ export default {
} }
}, },
Query: async (resolve, root, args, context, info) => { Query: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info) let result = await resolve(root, args, context, info)
return walkRecursive(result, ['password'], () => { result = walkRecursive(result, ['password', 'privateKey'], () => {
// replace password with asterisk // replace password with asterisk
return '*****' return '*****'
}) })
return result
} }
} }

View File

@ -74,6 +74,7 @@ const permissions = shield({
UpdateBadge: isAdmin, UpdateBadge: isAdmin,
DeleteBadge: isAdmin, DeleteBadge: isAdmin,
AddUserBadges: isAdmin, AddUserBadges: isAdmin,
CreateSocialMedia: isAuthenticated,
// AddBadgeRewarded: isAdmin, // AddBadgeRewarded: isAdmin,
// RemoveBadgeRewarded: isAdmin, // RemoveBadgeRewarded: isAdmin,
reward: isAdmin, reward: isAdmin,
@ -85,12 +86,14 @@ const permissions = shield({
unshout: isAuthenticated, unshout: isAuthenticated,
changePassword: isAuthenticated, changePassword: isAuthenticated,
enable: isModerator, enable: isModerator,
disable: isModerator disable: isModerator,
CreateComment: isAuthenticated
// CreateUser: allow, // CreateUser: allow,
}, },
User: { User: {
email: isMyOwn, email: isMyOwn,
password: isMyOwn password: isMyOwn,
privateKey: isMyOwn
} }
}) })

View File

@ -23,21 +23,19 @@ beforeAll(async () => {
]) ])
await Promise.all([ await Promise.all([
factory.create('Comment', { id: 'c2', content: 'Enabled comment on public post' }) factory.create('Comment', { id: 'c2', postId: 'p3', content: 'Enabled comment on public post' })
]) ])
await Promise.all([ await Promise.all([
factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' }), factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' })
factory.relate('Comment', 'Post', { from: 'c2', to: 'p3' })
]) ])
const asTroll = Factory() const asTroll = Factory()
await asTroll.authenticateAs({ email: 'troll@example.org', password: '1234' }) await asTroll.authenticateAs({ email: 'troll@example.org', password: '1234' })
await asTroll.create('Post', { id: 'p2', title: 'Disabled post', content: 'This is an offensive post content', image: '/some/offensive/image.jpg', deleted: false }) await asTroll.create('Post', { id: 'p2', title: 'Disabled post', content: 'This is an offensive post content', image: '/some/offensive/image.jpg', deleted: false })
await asTroll.create('Comment', { id: 'c1', content: 'Disabled comment' }) await asTroll.create('Comment', { id: 'c1', postId: 'p3', content: 'Disabled comment' })
await Promise.all([ await Promise.all([
asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' }), asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' })
asTroll.relate('Comment', 'Post', { from: 'c1', to: 'p3' })
]) ])
const asModerator = Factory() const asModerator = Factory()

View File

@ -0,0 +1,18 @@
const validURL = str => {
const isValid = str.match(/^(?:https?:\/\/)(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g)
return !!isValid
}
export default {
Mutation: {
CreateSocialMedia: async (resolve, root, args, context, info) => {
let socialMedia
if (validURL(args.url)) {
socialMedia = await resolve(root, args, context, info)
} else {
throw Error('Input is not a URL')
}
return socialMedia
}
}
}

View File

@ -0,0 +1,52 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
import { UserInputError } from 'apollo-server'
const COMMENT_MIN_LENGTH = 1
export default {
Query: {
CommentByPost: async (object, params, context, resolveInfo) => {
const { postId } = params
const session = context.driver.session()
const transactionRes = await session.run(`
MATCH (comment:Comment)-[:COMMENTS]->(post:Post {id: $postId})
RETURN comment {.id, .contentExcerpt, .createdAt} ORDER BY comment.createdAt ASC`, {
postId
})
session.close()
let comments = []
transactionRes.records.map(record => {
comments.push(record.get('comment'))
})
return comments
}
},
Mutation: {
CreateComment: async (object, params, context, resolveInfo) => {
const content = params.content.replace(/<(?:.|\n)*?>/gm, '').trim()
if (!params.content || content.length < COMMENT_MIN_LENGTH) {
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
}
const { postId } = params
delete params.postId
const comment = await neo4jgraphql(object, params, context, resolveInfo, false)
const session = context.driver.session()
await session.run(`
MATCH (post:Post {id: $postId}), (comment:Comment {id: $commentId})
MERGE (post)<-[:COMMENTS]-(comment)
RETURN comment {.id, .content}`, {
postId,
commentId: comment.id
}
)
session.close()
return comment
}
}
}

View File

@ -0,0 +1,81 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
let client
let variables
beforeEach(async () => {
await factory.create('User', {
email: 'test@example.org',
password: '1234'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('CreateComment', () => {
const mutation = `
mutation($postId: ID, $content: String!) {
CreateComment(postId: $postId, content: $content) {
id
content
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
variables = {
postId: 'p1',
content: 'I\'m not authorised to comment'
}
client = new GraphQLClient(host)
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('creates a comment', async () => {
variables = {
postId: 'p1',
content: 'I\'m authorised to comment'
}
const expected = {
CreateComment: {
content: 'I\'m authorised to comment'
}
}
await expect(client.request(mutation, variables)).resolves.toMatchObject(expected)
})
it('throw an error if an empty string is sent as content', async () => {
variables = {
postId: 'p1',
content: '<p></p>'
}
await expect(client.request(mutation, variables))
.rejects.toThrow('Comment must be at least 1 character long!')
})
it('throws an error if a comment does not contain a single character', async () => {
variables = {
postId: 'p1',
content: '<p> </p>'
}
await expect(client.request(mutation, variables))
.rejects.toThrow('Comment must be at least 1 character long!')
})
})
})

View File

@ -0,0 +1,51 @@
export default {
Mutation: {
follow: async (_object, params, context, _resolveInfo) => {
const { id, type } = params
const session = context.driver.session()
let transactionRes = await session.run(
`MATCH (node {id: $id}), (user:User {id: $userId})
WHERE $type IN labels(node) AND NOT $id = $userId
MERGE (user)-[relation:FOLLOWS]->(node)
RETURN COUNT(relation) > 0 as isFollowed`,
{
id,
type,
userId: context.user.id
}
)
const [isFollowed] = transactionRes.records.map(record => {
return record.get('isFollowed')
})
session.close()
return isFollowed
},
unfollow: async (_object, params, context, _resolveInfo) => {
const { id, type } = params
const session = context.driver.session()
let transactionRes = await session.run(
`MATCH (user:User {id: $userId})-[relation:FOLLOWS]->(node {id: $id})
WHERE $type IN labels(node)
DELETE relation
RETURN COUNT(relation) > 0 as isFollowed`,
{
id,
type,
userId: context.user.id
}
)
const [isFollowed] = transactionRes.records.map(record => {
return record.get('isFollowed')
})
session.close()
return isFollowed
}
}
}

View File

@ -109,11 +109,11 @@ describe('disable', () => {
await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' }) await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' })
await Promise.all([ await Promise.all([
factory.create('Post', { id: 'p3' }), factory.create('Post', { id: 'p3' }),
factory.create('Comment', { id: 'c47' }) factory.create('Comment', { id: 'c47', postId: 'p3', content: 'this comment was created for this post' })
]) ])
await Promise.all([ await Promise.all([
factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' }), factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' })
factory.relate('Comment', 'Post', { from: 'c47', to: 'p3' })
]) ])
} }
}) })
@ -286,8 +286,7 @@ describe('enable', () => {
factory.create('Comment', { id: 'c456' }) factory.create('Comment', { id: 'c456' })
]) ])
await Promise.all([ await Promise.all([
factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' }), factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' })
factory.relate('Comment', 'Post', { from: 'c456', to: 'p9' })
]) ])
const disableMutation = ` const disableMutation = `

View File

@ -4,7 +4,7 @@ export default {
const { fromBadgeId, toUserId } = params const { fromBadgeId, toUserId } = params
const session = context.driver.session() const session = context.driver.session()
let sessionRes = await session.run( let transactionRes = await session.run(
`MATCH (badge:Badge {id: $badgeId}), (rewardedUser:User {id: $rewardedUserId}) `MATCH (badge:Badge {id: $badgeId}), (rewardedUser:User {id: $rewardedUserId})
MERGE (badge)-[:REWARDED]->(rewardedUser) MERGE (badge)-[:REWARDED]->(rewardedUser)
RETURN rewardedUser {.id}`, RETURN rewardedUser {.id}`,
@ -14,7 +14,7 @@ export default {
} }
) )
const [rewardedUser] = sessionRes.records.map(record => { const [rewardedUser] = transactionRes.records.map(record => {
return record.get('rewardedUser') return record.get('rewardedUser')
}) })
@ -27,7 +27,7 @@ export default {
const { fromBadgeId, toUserId } = params const { fromBadgeId, toUserId } = params
const session = context.driver.session() const session = context.driver.session()
let sessionRes = await session.run( let transactionRes = await session.run(
`MATCH (badge:Badge {id: $badgeId})-[reward:REWARDED]->(rewardedUser:User {id: $rewardedUserId}) `MATCH (badge:Badge {id: $badgeId})-[reward:REWARDED]->(rewardedUser:User {id: $rewardedUserId})
DELETE reward DELETE reward
RETURN rewardedUser {.id}`, RETURN rewardedUser {.id}`,
@ -36,7 +36,7 @@ export default {
rewardedUserId: toUserId rewardedUserId: toUserId
} }
) )
const [rewardedUser] = sessionRes.records.map(record => { const [rewardedUser] = transactionRes.records.map(record => {
return record.get('rewardedUser') return record.get('rewardedUser')
}) })
session.close() session.close()

View File

@ -0,0 +1,51 @@
export default {
Mutation: {
shout: async (_object, params, context, _resolveInfo) => {
const { id, type } = params
const session = context.driver.session()
let transactionRes = await session.run(
`MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId})
WHERE $type IN labels(node) AND NOT userWritten.id = $userId
MERGE (user)-[relation:SHOUTED]->(node)
RETURN COUNT(relation) > 0 as isShouted`,
{
id,
type,
userId: context.user.id
}
)
const [isShouted] = transactionRes.records.map(record => {
return record.get('isShouted')
})
session.close()
return isShouted
},
unshout: async (_object, params, context, _resolveInfo) => {
const { id, type } = params
const session = context.driver.session()
let transactionRes = await session.run(
`MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id})
WHERE $type IN labels(node)
DELETE relation
RETURN COUNT(relation) > 0 as isShouted`,
{
id,
type,
userId: context.user.id
}
)
const [isShouted] = transactionRes.records.map(record => {
return record.get('isShouted')
})
session.close()
return isShouted
}
}
}

View File

@ -0,0 +1,21 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
export default {
Mutation: {
CreateSocialMedia: async (object, params, context, resolveInfo) => {
const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
const session = context.driver.session()
await session.run(
`MATCH (owner:User {id: $userId}), (socialMedia:SocialMedia {id: $socialMediaId})
MERGE (socialMedia)<-[:OWNED]-(owner)
RETURN owner`, {
userId: context.user.id,
socialMediaId: socialMedia.id
}
)
session.close()
return socialMedia
}
}
}

View File

@ -0,0 +1,49 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
describe('CreateSocialMedia', () => {
let client
let headers
const mutation = `
mutation($url: String!) {
CreateSocialMedia(url: $url) {
url
}
}
`
beforeEach(async () => {
await factory.create('User', {
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
name: 'Matilde Hermiston',
slug: 'matilde-hermiston',
role: 'user',
email: 'test@example.org',
password: '1234'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('authenticated', () => {
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('rejects empty string', async () => {
const variables = { url: '' }
await expect(client.request(mutation, variables)).rejects.toThrow('Input is not a URL')
})
it('validates URLs', async () => {
const variables = { url: 'not-a-url' }
await expect(client.request(mutation, variables)).rejects.toThrow('Input is not a URL')
})
})
})

View File

@ -1,3 +1,4 @@
import gql from 'graphql-tag'
import Factory from '../seed/factories' import Factory from '../seed/factories'
import { GraphQLClient, request } from 'graphql-request' import { GraphQLClient, request } from 'graphql-request'
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
@ -254,7 +255,7 @@ describe('change password', () => {
} }
describe('should be authenticated before changing password', () => { describe('should be authenticated before changing password', () => {
it('throws not "Not Authorised!', async () => { it('throws "Not Authorised!"', async () => {
await expect( await expect(
request( request(
host, host,
@ -309,3 +310,97 @@ describe('change password', () => {
}) })
}) })
}) })
describe('do not expose private RSA key', () => {
let headers
let client
const queryUserPuplicKey = gql`
query($queriedUserSlug: String) {
User(slug: $queriedUserSlug) {
id
publicKey
}
}`
const queryUserPrivateKey = gql`
query($queriedUserSlug: String) {
User(slug: $queriedUserSlug) {
id
privateKey
}
}`
const actionGenUserWithKeys = async () => {
// Generate user with "privateKey" via 'CreateUser' mutation instead of using the factories "factory.create('User', {...})", see above.
const variables = {
id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
password: 'xYz',
slug: 'apfel-strudel',
name: 'Apfel Strudel',
email: 'apfel-strudel@test.org'
}
await client.request(gql`
mutation($id: ID, $password: String!, $slug: String, $name: String, $email: String) {
CreateUser(id: $id, password: $password, slug: $slug, name: $name, email: $email) {
id
}
}`, variables
)
}
// not authenticate
beforeEach(async () => {
client = new GraphQLClient(host)
})
describe('unauthenticated query of "publicKey" (does the RSA key pair get generated at all?)', () => {
it('returns publicKey', async () => {
await actionGenUserWithKeys()
await expect(
await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' })
).toEqual(expect.objectContaining({
User: [{
id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
publicKey: expect.any(String)
}]
}))
})
})
describe('unauthenticated query of "privateKey"', () => {
it('throws "Not Authorised!"', async () => {
await actionGenUserWithKeys()
await expect(
client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' })
).rejects.toThrow('Not Authorised')
})
})
// authenticate
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
describe('authenticated query of "publicKey"', () => {
it('returns publicKey', async () => {
await actionGenUserWithKeys()
await expect(
await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' })
).toEqual(expect.objectContaining({
User: [{
id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
publicKey: expect.any(String)
}]
}))
})
})
describe('authenticated query of "privateKey"', () => {
it('throws "Not Authorised!"', async () => {
await actionGenUserWithKeys()
await expect(
client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' })
).rejects.toThrow('Not Authorised')
})
})
})

View File

@ -1,8 +1,8 @@
type Query { type Query {
isLoggedIn: Boolean! isLoggedIn: Boolean!
"Get the currently logged in User based on the given JWT Token" # Get the currently logged in User based on the given JWT Token
currentUser: User currentUser: User
"Get the latest Network Statistics" # Get the latest Network Statistics
statistics: Statistics! statistics: Statistics!
findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( findPosts(filter: String!, limit: Int = 10): [Post]! @cypher(
statement: """ statement: """
@ -16,9 +16,10 @@ type Query {
LIMIT $limit LIMIT $limit
""" """
) )
CommentByPost(postId: ID!): [Comment]!
} }
type Mutation { type Mutation {
"Get a JWT Token for the given Email and password" # Get a JWT Token for the given Email and password
login(email: String!, password: String!): String! login(email: String!, password: String!): String!
signup(email: String!, password: String!): Boolean! signup(email: String!, password: String!): Boolean!
changePassword(oldPassword:String!, newPassword: String!): String! changePassword(oldPassword:String!, newPassword: String!): String!
@ -27,34 +28,14 @@ type Mutation {
enable(id: ID!): ID enable(id: ID!): ID
reward(fromBadgeId: ID!, toUserId: ID!): ID reward(fromBadgeId: ID!, toUserId: ID!): ID
unreward(fromBadgeId: ID!, toUserId: ID!): ID unreward(fromBadgeId: ID!, toUserId: ID!): ID
"Shout the given Type and ID" # Shout the given Type and ID
shout(id: ID!, type: ShoutTypeEnum): Boolean! @cypher(statement: """ shout(id: ID!, type: ShoutTypeEnum): Boolean!
MATCH (n {id: $id})<-[:WROTE]-(wu:User), (u:User {id: $cypherParams.currentUserId}) # Unshout the given Type and ID
WHERE $type IN labels(n) AND NOT wu.id = $cypherParams.currentUserId unshout(id: ID!, type: ShoutTypeEnum): Boolean!
MERGE (u)-[r:SHOUTED]->(n) # Follow the given Type and ID
RETURN COUNT(r) > 0 follow(id: ID!, type: FollowTypeEnum): Boolean!
""") # Unfollow the given Type and ID
"Unshout the given Type and ID" unfollow(id: ID!, type: FollowTypeEnum): Boolean!
unshout(id: ID!, type: ShoutTypeEnum): Boolean! @cypher(statement: """
MATCH (:User {id: $cypherParams.currentUserId})-[r:SHOUTED]->(n {id: $id})
WHERE $type IN labels(n)
DELETE r
RETURN COUNT(r) > 0
""")
"Follow the given Type and ID"
follow(id: ID!, type: FollowTypeEnum): Boolean! @cypher(statement: """
MATCH (n {id: $id}), (u:User {id: $cypherParams.currentUserId})
WHERE $type IN labels(n) AND NOT $id = $cypherParams.currentUserId
MERGE (u)-[r:FOLLOWS]->(n)
RETURN COUNT(r) > 0
""")
"Unfollow the given Type and ID"
unfollow(id: ID!, type: FollowTypeEnum): Boolean! @cypher(statement: """
MATCH (:User {id: $cypherParams.currentUserId})-[r:FOLLOWS]->(n {id: $id})
WHERE $type IN labels(n)
DELETE r
RETURN COUNT(r) > 0
""")
} }
type Statistics { type Statistics {
@ -128,6 +109,7 @@ type User {
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
locationName: String locationName: String
about: String about: String
socialMedia: [SocialMedia]! @relation(name: "OWNED", direction: "OUT")
createdAt: String createdAt: String
updatedAt: String updatedAt: String
@ -143,11 +125,13 @@ type User {
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
"Is the currently logged in user following that user?" # Is the currently logged in user following that user?
followedByCurrentUser: Boolean! @cypher(statement: """ followedByCurrentUser: Boolean! @cypher(
statement: """
MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1 RETURN COUNT(u) >= 1
""") """
)
#contributions: [WrittenPost]! #contributions: [WrittenPost]!
#contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! #contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
@ -155,11 +139,13 @@ type User {
# statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp" # statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp"
# ) # )
contributions: [Post]! @relation(name: "WROTE", direction: "OUT") contributions: [Post]! @relation(name: "WROTE", direction: "OUT")
contributionsCount: Int! @cypher(statement: """ contributionsCount: Int! @cypher(
statement: """
MATCH (this)-[:WROTE]->(r:Post) MATCH (this)-[:WROTE]->(r:Post)
WHERE (NOT exists(r.deleted) OR r.deleted = false) WHERE (NOT exists(r.deleted) OR r.deleted = false)
AND (NOT exists(r.disabled) OR r.disabled = false) AND (NOT exists(r.disabled) OR r.disabled = false)
RETURN COUNT(r)""" RETURN COUNT(r)
"""
) )
comments: [Comment]! @relation(name: "WROTE", direction: "OUT") comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
@ -196,11 +182,13 @@ type Post {
createdAt: String createdAt: String
updatedAt: String updatedAt: String
relatedContributions: [Post]! @cypher(statement: """ relatedContributions: [Post]! @cypher(
statement: """
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post) MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
RETURN DISTINCT post RETURN DISTINCT post
LIMIT 10 LIMIT 10
""") """
)
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT") tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
@ -211,16 +199,19 @@ type Post {
shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN") shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
"Has the currently logged in user shouted that post?" # Has the currently logged in user shouted that post?
shoutedByCurrentUser: Boolean! @cypher(statement: """ shoutedByCurrentUser: Boolean! @cypher(
statement: """
MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1 RETURN COUNT(u) >= 1
""") """
)
} }
type Comment { type Comment {
id: ID! id: ID!
activityId: String activityId: String
postId: ID
author: User @relation(name: "WROTE", direction: "IN") author: User @relation(name: "WROTE", direction: "IN")
content: String! content: String!
contentExcerpt: String contentExcerpt: String
@ -314,7 +305,15 @@ type Tag {
deleted: Boolean deleted: Boolean
disabled: Boolean disabled: Boolean
} }
type SharedInboxEndpoint { type SharedInboxEndpoint {
id: ID! id: ID!
uri: String uri: String
} }
type SocialMedia {
id: ID!
url: String
ownedBy: [User]! @relation(name: "OWNED", direction: "IN")
}

View File

@ -3,21 +3,26 @@ import uuid from 'uuid/v4'
export default function (params) { export default function (params) {
const { const {
id = uuid(), id = uuid(),
key, key = '',
type = 'crowdfunding', type = 'crowdfunding',
status = 'permanent', status = 'permanent',
icon icon = '/img/badges/indiegogo_en_panda.svg'
} = params } = params
return ` return {
mutation { mutation: `
CreateBadge( mutation(
id: "${id}", $id: ID
key: "${key}", $key: String!
type: ${type}, $type: BadgeTypeEnum!
status: ${status}, $status: BadgeStatusEnum!
icon: "${icon}" $icon: String!
) { id } ) {
CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
id
}
}
`,
variables: { id, key, type, status, icon }
} }
`
} }

View File

@ -8,14 +8,15 @@ export default function (params) {
icon icon
} = params } = params
return ` return {
mutation { mutation: `
CreateCategory( mutation($id: ID, $name: String!, $slug: String, $icon: String!) {
id: "${id}", CreateCategory(id: $id, name: $name, slug: $slug, icon: $icon) {
name: "${name}", id
slug: "${slug}", name
icon: "${icon}" }
) { id, name } }
`,
variables: { id, name, slug, icon }
} }
`
} }

View File

@ -4,22 +4,21 @@ import uuid from 'uuid/v4'
export default function (params) { export default function (params) {
const { const {
id = uuid(), id = uuid(),
postId = 'p6',
content = [ content = [
faker.lorem.sentence(), faker.lorem.sentence(),
faker.lorem.sentence() faker.lorem.sentence()
].join('. '), ].join('. ')
disabled = false,
deleted = false
} = params } = params
return ` return {
mutation { mutation: `
CreateComment( mutation($id: ID!, $postId: ID, $content: String!) {
id: "${id}", CreateComment(id: $id, postId: $postId, content: $content) {
content: "${content}", id
disabled: ${disabled}, }
deleted: ${deleted} }
) { id } `,
variables: { id, postId, content }
} }
`
} }

View File

@ -71,8 +71,8 @@ export default function Factory (options = {}) {
return this return this
}, },
async create (node, properties) { async create (node, properties) {
const mutation = this.factories[node](properties) const { mutation, variables } = this.factories[node](properties)
this.lastResponse = await this.graphQLClient.request(mutation) this.lastResponse = await this.graphQLClient.request(mutation, variables)
return this return this
}, },
async relate (node, relationship, properties) { async relate (node, relationship, properties) {

View File

@ -6,12 +6,15 @@ export default function (params) {
read = false read = false
} = params } = params
return ` return {
mutation { mutation: `
CreateNotification( mutation($id: ID, $read: Boolean) {
id: "${id}", CreateNotification(id: $id, read: $read) {
read: ${read}, id
) { id, read } read
}
}
`,
variables: { id, read }
} }
`
} }

View File

@ -5,20 +5,17 @@ export default function create (params) {
const { const {
id = uuid(), id = uuid(),
name = faker.company.companyName(), name = faker.company.companyName(),
description = faker.company.catchPhrase(), description = faker.company.catchPhrase()
disabled = false,
deleted = false
} = params } = params
return ` return {
mutation { mutation: `
CreateOrganization( mutation($id: ID!, $name: String!, $description: String!) {
id: "${id}", CreateOrganization(id: $id, name: $name, description: $description) {
name: "${name}", name
description: "${description}", }
disabled: ${disabled}, }
deleted: ${deleted} `,
) { name } variables: { id, name, description }
} }
`
} }

View File

@ -18,17 +18,31 @@ export default function (params) {
deleted = false deleted = false
} = params } = params
return ` return {
mutation { mutation: `
mutation(
$id: ID!
$slug: String
$title: String!
$content: String!
$image: String
$visibility: VisibilityEnum
$deleted: Boolean
) {
CreatePost( CreatePost(
id: "${id}", id: $id
slug: "${slug}", slug: $slug
title: "${title}", title: $title
content: "${content}", content: $content
image: "${image}", image: $image
visibility: ${visibility}, visibility: $visibility
deleted: ${deleted} deleted: $deleted
) { title, content } ) {
title
content
}
}
`,
variables: { id, slug, title, content, image, visibility, deleted }
} }
`
} }

View File

@ -6,15 +6,15 @@ export default function create (params) {
id id
} = params } = params
return ` return {
mutation { mutation: `
report( mutation($id: ID!, $description: String!) {
description: "${description}", report(description: $description, id: $id) {
id: "${id}", id
) {
id,
createdAt createdAt
} }
} }
` `,
variables: { id, description }
}
} }

View File

@ -3,15 +3,17 @@ import uuid from 'uuid/v4'
export default function (params) { export default function (params) {
const { const {
id = uuid(), id = uuid(),
name name = '#human-connection'
} = params } = params
return ` return {
mutation { mutation: `
CreateTag( mutation($id: ID!, $name: String!) {
id: "${id}", CreateTag(id: $id, name: $name) {
name: "${name}", name
) { name } }
}
`,
variables: { id, name }
} }
`
} }

View File

@ -10,24 +10,30 @@ export default function create (params) {
password = '1234', password = '1234',
role = 'user', role = 'user',
avatar = faker.internet.avatar(), avatar = faker.internet.avatar(),
about = faker.lorem.paragraph(), about = faker.lorem.paragraph()
disabled = false,
deleted = false
} = params } = params
return ` return {
mutation { mutation: `
mutation(
$id: ID!
$name: String
$slug: String
$password: String!
$email: String
$avatar: String
$about: String
$role: UserGroupEnum
) {
CreateUser( CreateUser(
id: "${id}", id: $id
name: "${name}", name: $name
slug: "${slug}", slug: $slug
password: "${password}", password: $password
email: "${email}", email: $email
avatar: "${avatar}", avatar: $avatar
about: "${about}", about: $about
role: ${role}, role: $role
disabled: ${disabled},
deleted: ${deleted}
) { ) {
id id
name name
@ -39,5 +45,7 @@ export default function create (params) {
disabled disabled
} }
} }
` `,
variables: { id, name, slug, password, email, avatar, about, role }
}
} }

View File

@ -1,3 +1,4 @@
import faker from 'faker'
import Factory from './factories' import Factory from './factories'
/* eslint-disable no-multi-spaces */ /* eslint-disable no-multi-spaces */
@ -88,20 +89,23 @@ import Factory from './factories'
f.create('Tag', { id: 't4', name: 'Freiheit' }) f.create('Tag', { id: 't4', name: 'Freiheit' })
]) ])
const mention1 = 'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
const mention2 = 'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, here is another notification for you!'
await Promise.all([ await Promise.all([
asAdmin.create('Post', { id: 'p0' }), asAdmin.create('Post', { id: 'p0' }),
asModerator.create('Post', { id: 'p1' }), asModerator.create('Post', { id: 'p1' }),
asUser.create('Post', { id: 'p2', deleted: true }), asUser.create('Post', { id: 'p2' }),
asTick.create('Post', { id: 'p3' }), asTick.create('Post', { id: 'p3' }),
asTrick.create('Post', { id: 'p4' }), asTrick.create('Post', { id: 'p4' }),
asTrack.create('Post', { id: 'p5' }), asTrack.create('Post', { id: 'p5' }),
asAdmin.create('Post', { id: 'p6' }), asAdmin.create('Post', { id: 'p6' }),
asModerator.create('Post', { id: 'p7' }), asModerator.create('Post', { id: 'p7', content: `${mention1} ${faker.lorem.paragraph()}` }),
asUser.create('Post', { id: 'p8' }), asUser.create('Post', { id: 'p8' }),
asTick.create('Post', { id: 'p9' }), asTick.create('Post', { id: 'p9' }),
asTrick.create('Post', { id: 'p10' }), asTrick.create('Post', { id: 'p10' }),
asTrack.create('Post', { id: 'p11' }), asTrack.create('Post', { id: 'p11' }),
asAdmin.create('Post', { id: 'p12' }), asAdmin.create('Post', { id: 'p12', content: `${mention2} ${faker.lorem.paragraph()}` }),
asModerator.create('Post', { id: 'p13' }), asModerator.create('Post', { id: 'p13' }),
asUser.create('Post', { id: 'p14' }), asUser.create('Post', { id: 'p14' }),
asTick.create('Post', { id: 'p15' }) asTick.create('Post', { id: 'p15' })
@ -185,45 +189,33 @@ import Factory from './factories'
]) ])
await Promise.all([ await Promise.all([
f.create('Comment', { id: 'c1' }), f.create('Comment', { id: 'c1', postId: 'p1' }),
f.create('Comment', { id: 'c2' }), f.create('Comment', { id: 'c2', postId: 'p1' }),
f.create('Comment', { id: 'c3' }), f.create('Comment', { id: 'c3', postId: 'p3' }),
f.create('Comment', { id: 'c4' }), f.create('Comment', { id: 'c4', postId: 'p2' }),
f.create('Comment', { id: 'c5' }), f.create('Comment', { id: 'c5', postId: 'p3' }),
f.create('Comment', { id: 'c6' }), f.create('Comment', { id: 'c6', postId: 'p4' }),
f.create('Comment', { id: 'c7' }), f.create('Comment', { id: 'c7', postId: 'p2' }),
f.create('Comment', { id: 'c8' }), f.create('Comment', { id: 'c8', postId: 'p15' }),
f.create('Comment', { id: 'c9' }), f.create('Comment', { id: 'c9', postId: 'p15' }),
f.create('Comment', { id: 'c10' }), f.create('Comment', { id: 'c10', postId: 'p15' }),
f.create('Comment', { id: 'c11' }), f.create('Comment', { id: 'c11', postId: 'p15' }),
f.create('Comment', { id: 'c12' }) f.create('Comment', { id: 'c12', postId: 'p15' })
]) ])
await Promise.all([ await Promise.all([
f.relate('Comment', 'Author', { from: 'u3', to: 'c1' }), f.relate('Comment', 'Author', { from: 'u3', to: 'c1' }),
f.relate('Comment', 'Post', { from: 'c1', to: 'p1' }),
f.relate('Comment', 'Author', { from: 'u1', to: 'c2' }), f.relate('Comment', 'Author', { from: 'u1', to: 'c2' }),
f.relate('Comment', 'Post', { from: 'c2', to: 'p1' }),
f.relate('Comment', 'Author', { from: 'u1', to: 'c3' }), f.relate('Comment', 'Author', { from: 'u1', to: 'c3' }),
f.relate('Comment', 'Post', { from: 'c3', to: 'p3' }),
f.relate('Comment', 'Author', { from: 'u4', to: 'c4' }), f.relate('Comment', 'Author', { from: 'u4', to: 'c4' }),
f.relate('Comment', 'Post', { from: 'c4', to: 'p2' }),
f.relate('Comment', 'Author', { from: 'u4', to: 'c5' }), f.relate('Comment', 'Author', { from: 'u4', to: 'c5' }),
f.relate('Comment', 'Post', { from: 'c5', to: 'p3' }),
f.relate('Comment', 'Author', { from: 'u3', to: 'c6' }), f.relate('Comment', 'Author', { from: 'u3', to: 'c6' }),
f.relate('Comment', 'Post', { from: 'c6', to: 'p4' }),
f.relate('Comment', 'Author', { from: 'u2', to: 'c7' }), f.relate('Comment', 'Author', { from: 'u2', to: 'c7' }),
f.relate('Comment', 'Post', { from: 'c7', to: 'p2' }),
f.relate('Comment', 'Author', { from: 'u5', to: 'c8' }), f.relate('Comment', 'Author', { from: 'u5', to: 'c8' }),
f.relate('Comment', 'Post', { from: 'c8', to: 'p15' }),
f.relate('Comment', 'Author', { from: 'u6', to: 'c9' }), f.relate('Comment', 'Author', { from: 'u6', to: 'c9' }),
f.relate('Comment', 'Post', { from: 'c9', to: 'p15' }),
f.relate('Comment', 'Author', { from: 'u7', to: 'c10' }), f.relate('Comment', 'Author', { from: 'u7', to: 'c10' }),
f.relate('Comment', 'Post', { from: 'c10', to: 'p15' }),
f.relate('Comment', 'Author', { from: 'u5', to: 'c11' }), f.relate('Comment', 'Author', { from: 'u5', to: 'c11' }),
f.relate('Comment', 'Post', { from: 'c11', to: 'p15' }), f.relate('Comment', 'Author', { from: 'u6', to: 'c12' })
f.relate('Comment', 'Author', { from: 'u6', to: 'c12' }),
f.relate('Comment', 'Post', { from: 'c12', to: 'p15' })
]) ])
const disableMutation = 'mutation($id: ID!) { disable(id: $id) }' const disableMutation = 'mutation($id: ID!) { disable(id: $id) }'

2
backend/testing.md Normal file
View File

@ -0,0 +1,2 @@
# Unit Testing

View File

@ -14,22 +14,22 @@
resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.6.tgz#022209e28a2b547dcde15b219f0c50f47aa5beb3" resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.6.tgz#022209e28a2b547dcde15b219f0c50f47aa5beb3"
integrity sha512-lqK94b+caNtmKFs5oUVXlSpN3sm5IXZ+KfhMxOtr0LR2SqErzkoJilitjDvJ1WbjHlxLI7WtCjRmOLdOGJqtMQ== integrity sha512-lqK94b+caNtmKFs5oUVXlSpN3sm5IXZ+KfhMxOtr0LR2SqErzkoJilitjDvJ1WbjHlxLI7WtCjRmOLdOGJqtMQ==
"@babel/cli@~7.2.3": "@babel/cli@~7.4.4":
version "7.2.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.2.3.tgz#1b262e42a3e959d28ab3d205ba2718e1923cfee6" resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.4.4.tgz#5454bb7112f29026a4069d8e6f0e1794e651966c"
integrity sha512-bfna97nmJV6nDJhXNPeEfxyMjWnt6+IjUAaDPiYRTBlm8L41n8nvw6UAqUCbvpFfU246gHPxW7sfWwqtF4FcYA== integrity sha512-XGr5YjQSjgTa6OzQZY57FAJsdeVSAKR/u/KA5exWIz66IKtv/zXtHy+fIZcMry/EgYegwuHE7vzGnrFhjdIAsQ==
dependencies: dependencies:
commander "^2.8.1" commander "^2.8.1"
convert-source-map "^1.1.0" convert-source-map "^1.1.0"
fs-readdir-recursive "^1.1.0" fs-readdir-recursive "^1.1.0"
glob "^7.0.0" glob "^7.0.0"
lodash "^4.17.10" lodash "^4.17.11"
mkdirp "^0.5.1" mkdirp "^0.5.1"
output-file-sync "^2.0.0" output-file-sync "^2.0.0"
slash "^2.0.0" slash "^2.0.0"
source-map "^0.5.0" source-map "^0.5.0"
optionalDependencies: optionalDependencies:
chokidar "^2.0.3" chokidar "^2.0.4"
"@babel/code-frame@^7.0.0": "@babel/code-frame@^7.0.0":
version "7.0.0" version "7.0.0"
@ -38,18 +38,18 @@
dependencies: dependencies:
"@babel/highlight" "^7.0.0" "@babel/highlight" "^7.0.0"
"@babel/core@^7.1.0", "@babel/core@~7.4.3": "@babel/core@^7.1.0", "@babel/core@~7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.3.tgz#198d6d3af4567be3989550d97e068de94503074f" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.4.tgz#84055750b05fcd50f9915a826b44fa347a825250"
integrity sha512-oDpASqKFlbspQfzAE7yaeTmdljSH2ADIvBlb0RwbStltTuWa0+7CCI1fYVINNv9saHPa1W7oaKeuNuKj+RQCvA== integrity sha512-lQgGX3FPRgbz2SKmhMtYgJvVzGZrmjaF4apZ2bLwofAKiSjxU0drPh4S/VasyYXwaTs+A1gvQ45BN8SQJzHsQQ==
dependencies: dependencies:
"@babel/code-frame" "^7.0.0" "@babel/code-frame" "^7.0.0"
"@babel/generator" "^7.4.0" "@babel/generator" "^7.4.4"
"@babel/helpers" "^7.4.3" "@babel/helpers" "^7.4.4"
"@babel/parser" "^7.4.3" "@babel/parser" "^7.4.4"
"@babel/template" "^7.4.0" "@babel/template" "^7.4.4"
"@babel/traverse" "^7.4.3" "@babel/traverse" "^7.4.4"
"@babel/types" "^7.4.0" "@babel/types" "^7.4.4"
convert-source-map "^1.1.0" convert-source-map "^1.1.0"
debug "^4.1.0" debug "^4.1.0"
json5 "^2.1.0" json5 "^2.1.0"
@ -58,12 +58,12 @@
semver "^5.4.1" semver "^5.4.1"
source-map "^0.5.0" source-map "^0.5.0"
"@babel/generator@^7.0.0", "@babel/generator@^7.4.0": "@babel/generator@^7.0.0", "@babel/generator@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.0.tgz#c230e79589ae7a729fd4631b9ded4dc220418196" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041"
integrity sha512-/v5I+a1jhGSKLgZDcmAUZ4K/VePi43eRkUs3yePW1HB1iANOD5tqJXwGSG4BZhSksP8J9ejSlwGeTiiOFZOrXQ== integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==
dependencies: dependencies:
"@babel/types" "^7.4.0" "@babel/types" "^7.4.4"
jsesc "^2.5.1" jsesc "^2.5.1"
lodash "^4.17.11" lodash "^4.17.11"
source-map "^0.5.0" source-map "^0.5.0"
@ -84,22 +84,22 @@
"@babel/helper-explode-assignable-expression" "^7.1.0" "@babel/helper-explode-assignable-expression" "^7.1.0"
"@babel/types" "^7.0.0" "@babel/types" "^7.0.0"
"@babel/helper-call-delegate@^7.4.0": "@babel/helper-call-delegate@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.4.0.tgz#f308eabe0d44f451217853aedf4dea5f6fe3294f" resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz#87c1f8ca19ad552a736a7a27b1c1fcf8b1ff1f43"
integrity sha512-SdqDfbVdNQCBp3WhK2mNdDvHd3BD6qbmIc43CAyjnsfCmgHMeqgDcM3BzY2lchi7HBJGJ2CVdynLWbezaE4mmQ== integrity sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ==
dependencies: dependencies:
"@babel/helper-hoist-variables" "^7.4.0" "@babel/helper-hoist-variables" "^7.4.4"
"@babel/traverse" "^7.4.0" "@babel/traverse" "^7.4.4"
"@babel/types" "^7.4.0" "@babel/types" "^7.4.4"
"@babel/helper-define-map@^7.4.0": "@babel/helper-define-map@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.0.tgz#cbfd8c1b2f12708e262c26f600cd16ed6a3bc6c9" resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz#6969d1f570b46bdc900d1eba8e5d59c48ba2c12a"
integrity sha512-wAhQ9HdnLIywERVcSvX40CEJwKdAa1ID4neI9NXQPDOHwwA+57DqwLiPEVy2AIyWzAk0CQ8qx4awO0VUURwLtA== integrity sha512-IX3Ln8gLhZpSuqHJSnTNBWGDE9kdkTEWl21A/K7PQ00tseBwbqCHTvNLHSBd9M0R5rER4h5Rsvj9vw0R5SieBg==
dependencies: dependencies:
"@babel/helper-function-name" "^7.1.0" "@babel/helper-function-name" "^7.1.0"
"@babel/types" "^7.4.0" "@babel/types" "^7.4.4"
lodash "^4.17.11" lodash "^4.17.11"
"@babel/helper-explode-assignable-expression@^7.1.0": "@babel/helper-explode-assignable-expression@^7.1.0":
@ -126,12 +126,12 @@
dependencies: dependencies:
"@babel/types" "^7.0.0" "@babel/types" "^7.0.0"
"@babel/helper-hoist-variables@^7.4.0": "@babel/helper-hoist-variables@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.0.tgz#25b621399ae229869329730a62015bbeb0a6fbd6" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz#0298b5f25c8c09c53102d52ac4a98f773eb2850a"
integrity sha512-/NErCuoe/et17IlAQFKWM24qtyYYie7sFIrW/tIQXpck6vAu2hhtYYsKLBWQV+BQZMbcIYPU/QMYuTufrY4aQw== integrity sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w==
dependencies: dependencies:
"@babel/types" "^7.4.0" "@babel/types" "^7.4.4"
"@babel/helper-member-expression-to-functions@^7.0.0": "@babel/helper-member-expression-to-functions@^7.0.0":
version "7.0.0" version "7.0.0"
@ -159,16 +159,16 @@
"@babel/types" "^7.0.0" "@babel/types" "^7.0.0"
lodash "^4.17.10" lodash "^4.17.10"
"@babel/helper-module-transforms@^7.4.3": "@babel/helper-module-transforms@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.4.3.tgz#b1e357a1c49e58a47211a6853abb8e2aaefeb064" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz#96115ea42a2f139e619e98ed46df6019b94414b8"
integrity sha512-H88T9IySZW25anu5uqyaC1DaQre7ofM+joZtAaO2F8NBdFfupH0SZ4gKjgSFVcvtx/aAirqA9L9Clio2heYbZA== integrity sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w==
dependencies: dependencies:
"@babel/helper-module-imports" "^7.0.0" "@babel/helper-module-imports" "^7.0.0"
"@babel/helper-simple-access" "^7.1.0" "@babel/helper-simple-access" "^7.1.0"
"@babel/helper-split-export-declaration" "^7.0.0" "@babel/helper-split-export-declaration" "^7.4.4"
"@babel/template" "^7.2.2" "@babel/template" "^7.4.4"
"@babel/types" "^7.2.2" "@babel/types" "^7.4.4"
lodash "^4.17.11" lodash "^4.17.11"
"@babel/helper-optimise-call-expression@^7.0.0": "@babel/helper-optimise-call-expression@^7.0.0":
@ -190,10 +190,10 @@
dependencies: dependencies:
lodash "^4.17.10" lodash "^4.17.10"
"@babel/helper-regex@^7.4.3": "@babel/helper-regex@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.4.3.tgz#9d6e5428bfd638ab53b37ae4ec8caf0477495147" resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.4.4.tgz#a47e02bc91fb259d2e6727c2a30013e3ac13c4a2"
integrity sha512-hnoq5u96pLCfgjXuj8ZLX3QQ+6nAulS+zSgi6HulUwFbEruRAKwbGLU5OvXkE14L8XW6XsQEKsIDfgthKLRAyA== integrity sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q==
dependencies: dependencies:
lodash "^4.17.11" lodash "^4.17.11"
@ -218,15 +218,15 @@
"@babel/traverse" "^7.1.0" "@babel/traverse" "^7.1.0"
"@babel/types" "^7.0.0" "@babel/types" "^7.0.0"
"@babel/helper-replace-supers@^7.4.0": "@babel/helper-replace-supers@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.0.tgz#4f56adb6aedcd449d2da9399c2dcf0545463b64c" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz#aee41783ebe4f2d3ab3ae775e1cc6f1a90cefa27"
integrity sha512-PVwCVnWWAgnal+kJ+ZSAphzyl58XrFeSKSAJRiqg5QToTsjL+Xu1f9+RJ+d+Q0aPhPfBGaYfkox66k86thxNSg== integrity sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg==
dependencies: dependencies:
"@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-member-expression-to-functions" "^7.0.0"
"@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0"
"@babel/traverse" "^7.4.0" "@babel/traverse" "^7.4.4"
"@babel/types" "^7.4.0" "@babel/types" "^7.4.4"
"@babel/helper-simple-access@^7.1.0": "@babel/helper-simple-access@^7.1.0":
version "7.1.0" version "7.1.0"
@ -243,12 +243,12 @@
dependencies: dependencies:
"@babel/types" "^7.0.0" "@babel/types" "^7.0.0"
"@babel/helper-split-export-declaration@^7.4.0": "@babel/helper-split-export-declaration@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.0.tgz#571bfd52701f492920d63b7f735030e9a3e10b55" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677"
integrity sha512-7Cuc6JZiYShaZnybDmfwhY4UYHzI6rlqhWjaIqbsJGsIqPimEYy5uh3akSRLMg65LSdSEnJ8a8/bWQN6u2oMGw== integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==
dependencies: dependencies:
"@babel/types" "^7.4.0" "@babel/types" "^7.4.4"
"@babel/helper-wrap-function@^7.1.0": "@babel/helper-wrap-function@^7.1.0":
version "7.2.0" version "7.2.0"
@ -260,14 +260,14 @@
"@babel/traverse" "^7.1.0" "@babel/traverse" "^7.1.0"
"@babel/types" "^7.2.0" "@babel/types" "^7.2.0"
"@babel/helpers@^7.4.3": "@babel/helpers@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.3.tgz#7b1d354363494b31cb9a2417ae86af32b7853a3b" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5"
integrity sha512-BMh7X0oZqb36CfyhvtbSmcWc3GXocfxv3yNsAEuM0l+fAqSO22rQrUpijr3oE/10jCTrB6/0b9kzmG4VetCj8Q== integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A==
dependencies: dependencies:
"@babel/template" "^7.4.0" "@babel/template" "^7.4.4"
"@babel/traverse" "^7.4.3" "@babel/traverse" "^7.4.4"
"@babel/types" "^7.4.0" "@babel/types" "^7.4.4"
"@babel/highlight@^7.0.0": "@babel/highlight@^7.0.0":
version "7.0.0" version "7.0.0"
@ -289,10 +289,10 @@
lodash "^4.17.10" lodash "^4.17.10"
v8flags "^3.1.1" v8flags "^3.1.1"
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.0", "@babel/parser@^7.4.3": "@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.3.tgz#eb3ac80f64aa101c907d4ce5406360fe75b7895b" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.4.tgz#5977129431b8fe33471730d255ce8654ae1250b6"
integrity sha512-gxpEUhTS1sGA63EGQGuA+WESPR/6tz6ng7tSHFCmaTJK/cGK8y37cBTspX+U2xCAue2IQVvF6Z0oigmjwD8YGQ== integrity sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w==
"@babel/plugin-proposal-async-generator-functions@^7.2.0": "@babel/plugin-proposal-async-generator-functions@^7.2.0":
version "7.2.0" version "7.2.0"
@ -311,10 +311,10 @@
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-json-strings" "^7.2.0" "@babel/plugin-syntax-json-strings" "^7.2.0"
"@babel/plugin-proposal-object-rest-spread@^7.4.3": "@babel/plugin-proposal-object-rest-spread@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.4.3.tgz#be27cd416eceeba84141305b93c282f5de23bbb4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.4.4.tgz#1ef173fcf24b3e2df92a678f027673b55e7e3005"
integrity sha512-xC//6DNSSHVjq8O2ge0dyYlhshsH4T7XdCVoxbi5HzLYWfsC5ooFlJjrXk8RcAT+hjHAK9UjBXdylzSoDK3t4g== integrity sha512-dMBG6cSPBbHeEBdFXeQ2QLc5gUpg4Vkaz8octD4aoW/ISO+jBOcsuxYL7bsb5WSu8RLP6boxrBIALEHgoHtO9g==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-object-rest-spread" "^7.2.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
@ -335,13 +335,13 @@
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-throw-expressions" "^7.2.0" "@babel/plugin-syntax-throw-expressions" "^7.2.0"
"@babel/plugin-proposal-unicode-property-regex@^7.4.0": "@babel/plugin-proposal-unicode-property-regex@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.0.tgz#202d91ee977d760ef83f4f416b280d568be84623" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz#501ffd9826c0b91da22690720722ac7cb1ca9c78"
integrity sha512-h/KjEZ3nK9wv1P1FSNb9G079jXrNYR0Ko+7XkOx85+gM24iZbPn0rh4vCftk+5QKY7y1uByFataBTmX7irEF1w== integrity sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/helper-regex" "^7.0.0" "@babel/helper-regex" "^7.4.4"
regexpu-core "^4.5.4" regexpu-core "^4.5.4"
"@babel/plugin-syntax-async-generators@^7.2.0": "@babel/plugin-syntax-async-generators@^7.2.0":
@ -386,10 +386,10 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-async-to-generator@^7.4.0": "@babel/plugin-transform-async-to-generator@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.0.tgz#234fe3e458dce95865c0d152d256119b237834b0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.4.tgz#a3f1d01f2f21cadab20b33a82133116f14fb5894"
integrity sha512-EeaFdCeUULM+GPFEsf7pFcNSxM7hYjoj5fiYbyuiXobW4JhFnjAv9OWzNwHyHcKoPNpAfeRDuW6VyaXEDUBa7g== integrity sha512-YiqW2Li8TXmzgbXw+STsSqPBPFnGviiaSp6CYOq55X8GQ2SGVLrXB6pNid8HkqkZAzOH6knbai3snhP7v0fNwA==
dependencies: dependencies:
"@babel/helper-module-imports" "^7.0.0" "@babel/helper-module-imports" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
@ -402,26 +402,26 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-block-scoping@^7.4.0": "@babel/plugin-transform-block-scoping@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.0.tgz#164df3bb41e3deb954c4ca32ffa9fcaa56d30bcb" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.4.tgz#c13279fabf6b916661531841a23c4b7dae29646d"
integrity sha512-AWyt3k+fBXQqt2qb9r97tn3iBwFpiv9xdAiG+Gr2HpAZpuayvbL55yWrsV3MyHvXk/4vmSiedhDRl1YI2Iy5nQ== integrity sha512-jkTUyWZcTrwxu5DD4rWz6rDB5Cjdmgz6z7M7RLXOJyCUkFBawssDGcGh8M/0FTSB87avyJI1HsTwUXp9nKA1PA==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
lodash "^4.17.11" lodash "^4.17.11"
"@babel/plugin-transform-classes@^7.4.3": "@babel/plugin-transform-classes@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.3.tgz#adc7a1137ab4287a555d429cc56ecde8f40c062c" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.4.tgz#0ce4094cdafd709721076d3b9c38ad31ca715eb6"
integrity sha512-PUaIKyFUDtG6jF5DUJOfkBdwAS/kFFV3XFk7Nn0a6vR7ZT8jYw5cGtIlat77wcnd0C6ViGqo/wyNf4ZHytF/nQ== integrity sha512-/e44eFLImEGIpL9qPxSRat13I5QNRgBLu2hOQJCF7VLy/otSM/sypV1+XaIw5+502RX/+6YaSAPmldk+nhHDPw==
dependencies: dependencies:
"@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-annotate-as-pure" "^7.0.0"
"@babel/helper-define-map" "^7.4.0" "@babel/helper-define-map" "^7.4.4"
"@babel/helper-function-name" "^7.1.0" "@babel/helper-function-name" "^7.1.0"
"@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/helper-replace-supers" "^7.4.0" "@babel/helper-replace-supers" "^7.4.4"
"@babel/helper-split-export-declaration" "^7.4.0" "@babel/helper-split-export-declaration" "^7.4.4"
globals "^11.1.0" globals "^11.1.0"
"@babel/plugin-transform-computed-properties@^7.2.0": "@babel/plugin-transform-computed-properties@^7.2.0":
@ -431,20 +431,20 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-destructuring@^7.4.3": "@babel/plugin-transform-destructuring@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.4.3.tgz#1a95f5ca2bf2f91ef0648d5de38a8d472da4350f" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.4.4.tgz#9d964717829cc9e4b601fc82a26a71a4d8faf20f"
integrity sha512-rVTLLZpydDFDyN4qnXdzwoVpk1oaXHIvPEOkOLyr88o7oHxVc/LyrnDx+amuBWGOwUb7D1s/uLsKBNTx08htZg== integrity sha512-/aOx+nW0w8eHiEHm+BTERB2oJn5D127iye/SUQl7NjHy0lf+j7h4MKMMSOwdazGq9OxgiNADncE+SRJkCxjZpQ==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-dotall-regex@^7.4.3": "@babel/plugin-transform-dotall-regex@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.3.tgz#fceff1c16d00c53d32d980448606f812cd6d02bf" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3"
integrity sha512-9Arc2I0AGynzXRR/oPdSALv3k0rM38IMFyto7kOCwb5F9sLUt2Ykdo3V9yUPR+Bgr4kb6bVEyLkPEiBhzcTeoA== integrity sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/helper-regex" "^7.4.3" "@babel/helper-regex" "^7.4.4"
regexpu-core "^4.5.4" regexpu-core "^4.5.4"
"@babel/plugin-transform-duplicate-keys@^7.2.0": "@babel/plugin-transform-duplicate-keys@^7.2.0":
@ -462,17 +462,17 @@
"@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0" "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-for-of@^7.4.3": "@babel/plugin-transform-for-of@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.3.tgz#c36ff40d893f2b8352202a2558824f70cd75e9fe" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz#0267fc735e24c808ba173866c6c4d1440fc3c556"
integrity sha512-UselcZPwVWNSURnqcfpnxtMehrb8wjXYOimlYQPBnup/Zld426YzIhNEvuRsEWVHfESIECGrxoI6L5QqzuLH5Q== integrity sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-function-name@^7.4.3": "@babel/plugin-transform-function-name@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.3.tgz#130c27ec7fb4f0cba30e958989449e5ec8d22bbd" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz#e1436116abb0610c2259094848754ac5230922ad"
integrity sha512-uT5J/3qI/8vACBR9I1GlAuU/JqBtWdfCrynuOkrWG6nCDieZd5przB1vfP59FRHBZQ9DC2IUfqr/xKqzOD5x0A== integrity sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA==
dependencies: dependencies:
"@babel/helper-function-name" "^7.1.0" "@babel/helper-function-name" "^7.1.0"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
@ -499,21 +499,21 @@
"@babel/helper-module-transforms" "^7.1.0" "@babel/helper-module-transforms" "^7.1.0"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-modules-commonjs@^7.4.3": "@babel/plugin-transform-modules-commonjs@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.3.tgz#3917f260463ac08f8896aa5bd54403f6e1fed165" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.4.tgz#0bef4713d30f1d78c2e59b3d6db40e60192cac1e"
integrity sha512-sMP4JqOTbMJMimqsSZwYWsMjppD+KRyDIUVW91pd7td0dZKAvPmhCaxhOzkzLParKwgQc7bdL9UNv+rpJB0HfA== integrity sha512-4sfBOJt58sEo9a2BQXnZq+Q3ZTSAUXyK3E30o36BOGnJ+tvJ6YSxF0PG6kERvbeISgProodWuI9UVG3/FMY6iw==
dependencies: dependencies:
"@babel/helper-module-transforms" "^7.4.3" "@babel/helper-module-transforms" "^7.4.4"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/helper-simple-access" "^7.1.0" "@babel/helper-simple-access" "^7.1.0"
"@babel/plugin-transform-modules-systemjs@^7.4.0": "@babel/plugin-transform-modules-systemjs@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.0.tgz#c2495e55528135797bc816f5d50f851698c586a1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.4.tgz#dc83c5665b07d6c2a7b224c00ac63659ea36a405"
integrity sha512-gjPdHmqiNhVoBqus5qK60mWPp1CmYWp/tkh11mvb0rrys01HycEGD7NvvSoKXlWEfSM9TcL36CpsK8ElsADptQ== integrity sha512-MSiModfILQc3/oqnG7NrP1jHaSPryO6tA2kOMmAQApz5dayPxWiHqmq4sWH2xF5LcQK56LlbKByCd8Aah/OIkQ==
dependencies: dependencies:
"@babel/helper-hoist-variables" "^7.4.0" "@babel/helper-hoist-variables" "^7.4.4"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-modules-umd@^7.2.0": "@babel/plugin-transform-modules-umd@^7.2.0":
@ -524,17 +524,17 @@
"@babel/helper-module-transforms" "^7.1.0" "@babel/helper-module-transforms" "^7.1.0"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-named-capturing-groups-regex@^7.4.2": "@babel/plugin-transform-named-capturing-groups-regex@^7.4.4":
version "7.4.2" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.2.tgz#800391136d6cbcc80728dbdba3c1c6e46f86c12e" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.4.tgz#5611d96d987dfc4a3a81c4383bb173361037d68d"
integrity sha512-NsAuliSwkL3WO2dzWTOL1oZJHm0TM8ZY8ZSxk2ANyKkt5SQlToGA4pzctmq1BEjoacurdwZ3xp2dCQWJkME0gQ== integrity sha512-Ki+Y9nXBlKfhD+LXaRS7v95TtTGYRAf9Y1rTDiE75zf8YQz4GDaWRXosMfJBXxnk88mGFjWdCRIeqDbon7spYA==
dependencies: dependencies:
regexp-tree "^0.1.0" regexp-tree "^0.1.0"
"@babel/plugin-transform-new-target@^7.4.0": "@babel/plugin-transform-new-target@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.0.tgz#67658a1d944edb53c8d4fa3004473a0dd7838150" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz#18d120438b0cc9ee95a47f2c72bc9768fbed60a5"
integrity sha512-6ZKNgMQmQmrEX/ncuCwnnw1yVGoaOW5KpxNhoWI7pCQdA0uZ0HqHGqenCUIENAnxRjy2WwNQ30gfGdIgqJXXqw== integrity sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
@ -546,12 +546,12 @@
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/helper-replace-supers" "^7.1.0" "@babel/helper-replace-supers" "^7.1.0"
"@babel/plugin-transform-parameters@^7.4.3": "@babel/plugin-transform-parameters@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.3.tgz#e5ff62929fdf4cf93e58badb5e2430303003800d" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz#7556cf03f318bd2719fe4c922d2d808be5571e16"
integrity sha512-ULJYC2Vnw96/zdotCZkMGr2QVfKpIT/4/K+xWWY0MbOJyMZuk660BGkr3bEKWQrrciwz6xpmft39nA4BF7hJuA== integrity sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==
dependencies: dependencies:
"@babel/helper-call-delegate" "^7.4.0" "@babel/helper-call-delegate" "^7.4.4"
"@babel/helper-get-function-arity" "^7.0.0" "@babel/helper-get-function-arity" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
@ -562,10 +562,10 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-regenerator@^7.4.3": "@babel/plugin-transform-regenerator@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.3.tgz#2a697af96887e2bbf5d303ab0221d139de5e739c" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.4.tgz#5b4da4df79391895fca9e28f99e87e22cfc02072"
integrity sha512-kEzotPuOpv6/iSlHroCDydPkKYw7tiJGKlmYp6iJn4a6C/+b2FdttlJsLKYxolYHgotTJ5G5UY5h0qey5ka3+A== integrity sha512-Zz3w+pX1SI0KMIiqshFZkwnVGUhDZzpX2vtPzfJBKQQq8WsP/Xy9DNdELWivxcKOCX/Pywge4SiEaPaLtoDT4g==
dependencies: dependencies:
regenerator-transform "^0.13.4" regenerator-transform "^0.13.4"
@ -598,10 +598,10 @@
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/helper-regex" "^7.0.0" "@babel/helper-regex" "^7.0.0"
"@babel/plugin-transform-template-literals@^7.2.0": "@babel/plugin-transform-template-literals@^7.4.4":
version "7.2.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.2.0.tgz#d87ed01b8eaac7a92473f608c97c089de2ba1e5b" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz#9d28fea7bbce637fb7612a0750989d8321d4bcb0"
integrity sha512-FkPix00J9A/XWXv4VoKJBMeSkyY9x/TqIh76wzcdfl57RJJcf8CehQ08uwfhCDNtRQYtHQKBTwKZDEyjE13Lwg== integrity sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==
dependencies: dependencies:
"@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-annotate-as-pure" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
@ -613,13 +613,13 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-unicode-regex@^7.4.3": "@babel/plugin-transform-unicode-regex@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.3.tgz#3868703fc0e8f443dda65654b298df576f7b863b" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz#ab4634bb4f14d36728bf5978322b35587787970f"
integrity sha512-lnSNgkVjL8EMtnE8eSS7t2ku8qvKH3eqNf/IwIfnSPUqzgqYmRwzdsQWv4mNQAN9Nuo6Gz1Y0a4CSmdpu1Pp6g== integrity sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/helper-regex" "^7.4.3" "@babel/helper-regex" "^7.4.4"
regexpu-core "^4.5.4" regexpu-core "^4.5.4"
"@babel/polyfill@^7.0.0": "@babel/polyfill@^7.0.0":
@ -638,64 +638,64 @@
core-js "^2.5.7" core-js "^2.5.7"
regenerator-runtime "^0.12.0" regenerator-runtime "^0.12.0"
"@babel/preset-env@~7.4.3": "@babel/preset-env@~7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.3.tgz#e71e16e123dc0fbf65a52cbcbcefd072fbd02880" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.4.tgz#b6f6825bfb27b3e1394ca3de4f926482722c1d6f"
integrity sha512-FYbZdV12yHdJU5Z70cEg0f6lvtpZ8jFSDakTm7WXeJbLXh4R0ztGEu/SW7G1nJ2ZvKwDhz8YrbA84eYyprmGqw== integrity sha512-FU1H+ACWqZZqfw1x2G1tgtSSYSfxJLkpaUQL37CenULFARDo+h4xJoVHzRoHbK+85ViLciuI7ME4WTIhFRBBlw==
dependencies: dependencies:
"@babel/helper-module-imports" "^7.0.0" "@babel/helper-module-imports" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-proposal-async-generator-functions" "^7.2.0" "@babel/plugin-proposal-async-generator-functions" "^7.2.0"
"@babel/plugin-proposal-json-strings" "^7.2.0" "@babel/plugin-proposal-json-strings" "^7.2.0"
"@babel/plugin-proposal-object-rest-spread" "^7.4.3" "@babel/plugin-proposal-object-rest-spread" "^7.4.4"
"@babel/plugin-proposal-optional-catch-binding" "^7.2.0" "@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
"@babel/plugin-proposal-unicode-property-regex" "^7.4.0" "@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
"@babel/plugin-syntax-async-generators" "^7.2.0" "@babel/plugin-syntax-async-generators" "^7.2.0"
"@babel/plugin-syntax-json-strings" "^7.2.0" "@babel/plugin-syntax-json-strings" "^7.2.0"
"@babel/plugin-syntax-object-rest-spread" "^7.2.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
"@babel/plugin-syntax-optional-catch-binding" "^7.2.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
"@babel/plugin-transform-arrow-functions" "^7.2.0" "@babel/plugin-transform-arrow-functions" "^7.2.0"
"@babel/plugin-transform-async-to-generator" "^7.4.0" "@babel/plugin-transform-async-to-generator" "^7.4.4"
"@babel/plugin-transform-block-scoped-functions" "^7.2.0" "@babel/plugin-transform-block-scoped-functions" "^7.2.0"
"@babel/plugin-transform-block-scoping" "^7.4.0" "@babel/plugin-transform-block-scoping" "^7.4.4"
"@babel/plugin-transform-classes" "^7.4.3" "@babel/plugin-transform-classes" "^7.4.4"
"@babel/plugin-transform-computed-properties" "^7.2.0" "@babel/plugin-transform-computed-properties" "^7.2.0"
"@babel/plugin-transform-destructuring" "^7.4.3" "@babel/plugin-transform-destructuring" "^7.4.4"
"@babel/plugin-transform-dotall-regex" "^7.4.3" "@babel/plugin-transform-dotall-regex" "^7.4.4"
"@babel/plugin-transform-duplicate-keys" "^7.2.0" "@babel/plugin-transform-duplicate-keys" "^7.2.0"
"@babel/plugin-transform-exponentiation-operator" "^7.2.0" "@babel/plugin-transform-exponentiation-operator" "^7.2.0"
"@babel/plugin-transform-for-of" "^7.4.3" "@babel/plugin-transform-for-of" "^7.4.4"
"@babel/plugin-transform-function-name" "^7.4.3" "@babel/plugin-transform-function-name" "^7.4.4"
"@babel/plugin-transform-literals" "^7.2.0" "@babel/plugin-transform-literals" "^7.2.0"
"@babel/plugin-transform-member-expression-literals" "^7.2.0" "@babel/plugin-transform-member-expression-literals" "^7.2.0"
"@babel/plugin-transform-modules-amd" "^7.2.0" "@babel/plugin-transform-modules-amd" "^7.2.0"
"@babel/plugin-transform-modules-commonjs" "^7.4.3" "@babel/plugin-transform-modules-commonjs" "^7.4.4"
"@babel/plugin-transform-modules-systemjs" "^7.4.0" "@babel/plugin-transform-modules-systemjs" "^7.4.4"
"@babel/plugin-transform-modules-umd" "^7.2.0" "@babel/plugin-transform-modules-umd" "^7.2.0"
"@babel/plugin-transform-named-capturing-groups-regex" "^7.4.2" "@babel/plugin-transform-named-capturing-groups-regex" "^7.4.4"
"@babel/plugin-transform-new-target" "^7.4.0" "@babel/plugin-transform-new-target" "^7.4.4"
"@babel/plugin-transform-object-super" "^7.2.0" "@babel/plugin-transform-object-super" "^7.2.0"
"@babel/plugin-transform-parameters" "^7.4.3" "@babel/plugin-transform-parameters" "^7.4.4"
"@babel/plugin-transform-property-literals" "^7.2.0" "@babel/plugin-transform-property-literals" "^7.2.0"
"@babel/plugin-transform-regenerator" "^7.4.3" "@babel/plugin-transform-regenerator" "^7.4.4"
"@babel/plugin-transform-reserved-words" "^7.2.0" "@babel/plugin-transform-reserved-words" "^7.2.0"
"@babel/plugin-transform-shorthand-properties" "^7.2.0" "@babel/plugin-transform-shorthand-properties" "^7.2.0"
"@babel/plugin-transform-spread" "^7.2.0" "@babel/plugin-transform-spread" "^7.2.0"
"@babel/plugin-transform-sticky-regex" "^7.2.0" "@babel/plugin-transform-sticky-regex" "^7.2.0"
"@babel/plugin-transform-template-literals" "^7.2.0" "@babel/plugin-transform-template-literals" "^7.4.4"
"@babel/plugin-transform-typeof-symbol" "^7.2.0" "@babel/plugin-transform-typeof-symbol" "^7.2.0"
"@babel/plugin-transform-unicode-regex" "^7.4.3" "@babel/plugin-transform-unicode-regex" "^7.4.4"
"@babel/types" "^7.4.0" "@babel/types" "^7.4.4"
browserslist "^4.5.2" browserslist "^4.5.2"
core-js-compat "^3.0.0" core-js-compat "^3.0.0"
invariant "^2.2.2" invariant "^2.2.2"
js-levenshtein "^1.1.3" js-levenshtein "^1.1.3"
semver "^5.5.0" semver "^5.5.0"
"@babel/register@^7.0.0", "@babel/register@~7.4.0": "@babel/register@^7.0.0", "@babel/register@~7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.4.0.tgz#d9d0a621db268fb14200f2685a4f8924c822404c" resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.4.4.tgz#370a68ba36f08f015a8b35d4864176c6b65d7a23"
integrity sha512-ekziebXBnS/7V6xk8sBfLSSD6YZuy6P29igBtR6OL/tswKdxOV+Yqq0nzICMguVYtGRZYUCGpfGV8J9Za2iBdw== integrity sha512-sn51H88GRa00+ZoMqCVgOphmswG4b7mhf9VOB0LUBAieykq2GnRFerlN+JQkO/ntT7wz4jaHNSRPg9IdMPEUkA==
dependencies: dependencies:
core-js "^3.0.0" core-js "^3.0.0"
find-cache-dir "^2.0.0" find-cache-dir "^2.0.0"
@ -711,34 +711,34 @@
dependencies: dependencies:
regenerator-runtime "^0.13.2" regenerator-runtime "^0.13.2"
"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.2.2", "@babel/template@^7.4.0": "@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.0.tgz#12474e9c077bae585c5d835a95c0b0b790c25c8b" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237"
integrity sha512-SOWwxxClTTh5NdbbYZ0BmaBVzxzTh2tO/TeLTbF6MO6EzVhHTnff8CdBXx3mEtazFBoysmEM6GU/wF+SuSx4Fw== integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==
dependencies: dependencies:
"@babel/code-frame" "^7.0.0" "@babel/code-frame" "^7.0.0"
"@babel/parser" "^7.4.0" "@babel/parser" "^7.4.4"
"@babel/types" "^7.4.0" "@babel/types" "^7.4.4"
"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.0", "@babel/traverse@^7.4.3": "@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.4":
version "7.4.3" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.3.tgz#1a01f078fc575d589ff30c0f71bf3c3d9ccbad84" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.4.tgz#0776f038f6d78361860b6823887d4f3937133fe8"
integrity sha512-HmA01qrtaCwwJWpSKpA948cBvU5BrmviAief/b3AVw936DtcdsTexlbyzNuDnthwhOQ37xshn7hvQaEQk7ISYQ== integrity sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A==
dependencies: dependencies:
"@babel/code-frame" "^7.0.0" "@babel/code-frame" "^7.0.0"
"@babel/generator" "^7.4.0" "@babel/generator" "^7.4.4"
"@babel/helper-function-name" "^7.1.0" "@babel/helper-function-name" "^7.1.0"
"@babel/helper-split-export-declaration" "^7.4.0" "@babel/helper-split-export-declaration" "^7.4.4"
"@babel/parser" "^7.4.3" "@babel/parser" "^7.4.4"
"@babel/types" "^7.4.0" "@babel/types" "^7.4.4"
debug "^4.1.0" debug "^4.1.0"
globals "^11.1.0" globals "^11.1.0"
lodash "^4.17.11" lodash "^4.17.11"
"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.4.0": "@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4":
version "7.4.0" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.0.tgz#670724f77d24cce6cc7d8cf64599d511d164894c" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0"
integrity sha512-aPvkXyU2SPOnztlgo8n9cEiXW755mgyvueUPcpStqdzoSPm0fjO0vQBjLkt3JKJW7ufikfcnMTTPsN1xaTsBPA== integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
lodash "^4.17.11" lodash "^4.17.11"
@ -1104,10 +1104,10 @@
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0"
integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA== integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==
"@types/yup@0.26.9": "@types/yup@0.26.12":
version "0.26.9" version "0.26.12"
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.9.tgz#8a619ac4d2b8dcacb0d81345746018303b479919" resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.12.tgz#60fc1a485923a929699d2107fac46e6769707c4a"
integrity sha512-C7HdLLs1ZNPbYeNsSX++fMosxWAwzVeUs9wc76XlKJrKvLEyNwXMDUjag75EVAPxlZ36YiRJ6iTy4zc5Dbtndw== integrity sha512-lWCsvLer6G84Gj7yh+oFGRuGHsqZd1Dwu47CVVL0ATw+bOnGDgMNHbTn80p1onT66fvLfN8FnRA3eRANsnnbbQ==
"@types/zen-observable@^0.5.3": "@types/zen-observable@^0.5.3":
version "0.5.4" version "0.5.4"
@ -1609,6 +1609,14 @@ array-flatten@1.1.1:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
array-includes@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d"
integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=
dependencies:
define-properties "^1.1.2"
es-abstract "^1.7.0"
array-map@~0.0.0: array-map@~0.0.0:
version "0.0.0" version "0.0.0"
resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
@ -2149,7 +2157,7 @@ cheerio@~1.0.0-rc.3:
lodash "^4.15.0" lodash "^4.15.0"
parse5 "^3.0.1" parse5 "^3.0.1"
chokidar@^2.0.3, chokidar@^2.1.5: chokidar@^2.0.4, chokidar@^2.1.5:
version "2.1.5" version "2.1.5"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.5.tgz#0ae8434d962281a5f56c72869e79cb6d9d86ad4d" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.5.tgz#0ae8434d962281a5f56c72869e79cb6d9d86ad4d"
integrity sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A== integrity sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==
@ -2892,7 +2900,7 @@ es-abstract@^1.4.3:
is-callable "^1.1.3" is-callable "^1.1.3"
is-regex "^1.0.4" is-regex "^1.0.4"
es-abstract@^1.5.1: es-abstract@^1.5.1, es-abstract@^1.7.0:
version "1.13.0" version "1.13.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==
@ -2989,10 +2997,10 @@ eslint-import-resolver-node@^0.3.2:
debug "^2.6.9" debug "^2.6.9"
resolve "^1.5.0" resolve "^1.5.0"
eslint-module-utils@^2.3.0: eslint-module-utils@^2.4.0:
version "2.3.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz#546178dab5e046c8b562bbb50705e2456d7bda49" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz#8b93499e9b00eab80ccb6614e69f03678e84e09a"
integrity sha512-lmDJgeOOjk8hObTysjqH7wyMi+nsHwwvfBykwfhjR1LNdd7C2uFJBvx4OpWYpXOw4df1yE1cDEVd1yLHitk34w== integrity sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw==
dependencies: dependencies:
debug "^2.6.8" debug "^2.6.8"
pkg-dir "^2.0.0" pkg-dir "^2.0.0"
@ -3005,26 +3013,27 @@ eslint-plugin-es@^1.3.1:
eslint-utils "^1.3.0" eslint-utils "^1.3.0"
regexpp "^2.0.1" regexpp "^2.0.1"
eslint-plugin-import@~2.16.0: eslint-plugin-import@~2.17.2:
version "2.16.0" version "2.17.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz#97ac3e75d0791c4fac0e15ef388510217be7f66f" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.17.2.tgz#d227d5c6dc67eca71eb590d2bb62fb38d86e9fcb"
integrity sha512-z6oqWlf1x5GkHIFgrSvtmudnqM6Q60KM4KvpWi5ubonMjycLjndvd5+8VAZIsTlHC03djdgJuyKG6XO577px6A== integrity sha512-m+cSVxM7oLsIpmwNn2WXTJoReOF9f/CtLMo7qOVmKd1KntBy0hEcuNZ3erTmWjx+DxRO0Zcrm5KwAvI9wHcV5g==
dependencies: dependencies:
array-includes "^3.0.3"
contains-path "^0.1.0" contains-path "^0.1.0"
debug "^2.6.9" debug "^2.6.9"
doctrine "1.5.0" doctrine "1.5.0"
eslint-import-resolver-node "^0.3.2" eslint-import-resolver-node "^0.3.2"
eslint-module-utils "^2.3.0" eslint-module-utils "^2.4.0"
has "^1.0.3" has "^1.0.3"
lodash "^4.17.11" lodash "^4.17.11"
minimatch "^3.0.4" minimatch "^3.0.4"
read-pkg-up "^2.0.0" read-pkg-up "^2.0.0"
resolve "^1.9.0" resolve "^1.10.0"
eslint-plugin-jest@~22.4.1: eslint-plugin-jest@~22.5.1:
version "22.4.1" version "22.5.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.4.1.tgz#a5fd6f7a2a41388d16f527073b778013c5189a9c" resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.5.1.tgz#a31dfe9f9513c6af7c17ece4c65535a1370f060b"
integrity sha512-gcLfn6P2PrFAVx3AobaOzlIEevpAEf9chTpFZz7bYfc7pz8XRv7vuKTIE4hxPKZSha6XWKKplDQ0x9Pq8xX2mg== integrity sha512-c3WjZR/HBoi4GedJRwo2OGHa8Pzo1EbSVwQ2HFzJ+4t2OoYM7Alx646EH/aaxZ+9eGcPiq0FT0UGkRuFFx2FHg==
eslint-plugin-node@~8.0.1: eslint-plugin-node@~8.0.1:
version "8.0.1" version "8.0.1"
@ -3743,13 +3752,13 @@ graphql-request@~1.8.2:
dependencies: dependencies:
cross-fetch "2.2.2" cross-fetch "2.2.2"
graphql-shield@~5.3.2: graphql-shield@~5.3.4:
version "5.3.2" version "5.3.4"
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.3.2.tgz#2d47907ed9882a0636cb8ade6087123309d215ef" resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.3.4.tgz#bd126d7d39adc6ae5b91d93ab5f65ae25f93ce80"
integrity sha512-fib7rSr5aS/WHL3+Aa5LXhcCuPGEIDXmzfGtFjUXkUiZ6E5u+bDSL+9KRXo/p14A28GkJF+1Vu1hlg9H/QFG1w== integrity sha512-YasNfKk7d0hiSU9eh0zvJmRmUMDLZrfVTwSke/4y46cBRXFiI9fv6OA12Ux+1DB4TyDAjGGnqx8d92ptL7ZN3w==
dependencies: dependencies:
"@types/yup" "0.26.9" "@types/yup" "0.26.12"
lightercollective "^0.2.0" lightercollective "^0.3.0"
object-hash "^1.3.1" object-hash "^1.3.1"
yup "^0.27.0" yup "^0.27.0"
@ -4917,9 +4926,9 @@ joi@^13.0.0:
topo "3.x.x" topo "3.x.x"
jquery@^3.3.1: jquery@^3.3.1:
version "3.3.1" version "3.4.0"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.0.tgz#8de513fa0fa4b2c7d2e48a530e26f0596936efdf"
integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg== integrity sha512-ggRCXln9zEqv6OqAGXFEcshF5dSBvCkzj6Gm2gzuR5fWawaX8t7cxKVkkygKODrDAzKdoYw3l/e3pm3vlT4IbQ==
js-levenshtein@^1.1.3: js-levenshtein@^1.1.3:
version "1.1.4" version "1.1.4"
@ -5175,10 +5184,10 @@ libphonenumber-js@^1.6.4:
semver-compare "^1.0.0" semver-compare "^1.0.0"
xml2js "^0.4.17" xml2js "^0.4.17"
lightercollective@^0.2.0: lightercollective@^0.3.0:
version "0.2.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/lightercollective/-/lightercollective-0.2.0.tgz#4f10cd53ec50405d7da03ee81233067993ca8e67" resolved "https://registry.yarnpkg.com/lightercollective/-/lightercollective-0.3.0.tgz#1f07638642ec645d70bdb69ab2777676f35a28f0"
integrity sha512-zgFCDiUQQOjislj+1tX7zDxZbgVB6Qi9BSmos41oZcxHdkTveVDzHW0Y3TisNZCWuBN1h0e0xrjkNoLtPkLsUg== integrity sha512-RFOLSUVvwdK3xA0P8o6G7QGXLIyy1L2qv5caEI7zXN5ciaEjbAriRF182kbsoJ1S1TgvpyGcN485fMky6qxOPw==
linkifyjs@~2.1.8: linkifyjs@~2.1.8:
version "2.1.8" version "2.1.8"
@ -5625,10 +5634,10 @@ node-fetch@2.1.2:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U= integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=
node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@~2.3.0: node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@~2.4.1:
version "2.3.0" version "2.4.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.4.1.tgz#b2e38f1117b8acbedbe0524f041fb3177188255d"
integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA== integrity sha512-P9UbpFK87NyqBZzUuDBDz4f6Yiys8xm8j7ACDbi6usvFm6KItklQUKjeoqTrYS/S1k6I8oaOC2YLLDr/gg26Mw==
node-forge@~0.6.45: node-forge@~0.6.45:
version "0.6.49" version "0.6.49"
@ -6677,7 +6686,7 @@ resolve@1.1.7:
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
resolve@^1.3.2, resolve@^1.3.3, resolve@^1.5.0, resolve@^1.8.1, resolve@^1.9.0: resolve@^1.10.0, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.5.0, resolve@^1.8.1:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==
@ -6770,10 +6779,10 @@ sane@^4.0.3:
minimist "^1.1.1" minimist "^1.1.1"
walker "~1.0.5" walker "~1.0.5"
sanitize-html@~1.20.0: sanitize-html@~1.20.1:
version "1.20.0" version "1.20.1"
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.20.0.tgz#9a602beb1c9faf960fb31f9890f61911cc4d9156" resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.20.1.tgz#f6effdf55dd398807171215a62bfc21811bacf85"
integrity sha512-BpxXkBoAG+uKCHjoXFmox6kCSYpnulABoGcZ/R3QyY9ndXbIM5S94eOr1IqnzTG8TnbmXaxWoDDzKC5eJv7fEQ== integrity sha512-txnH8TQjaQvg2Q0HY06G6CDJLVYCpbnxrdO0WN8gjCKaU5J0KbyGYhZxx5QJg3WLZ1lB7XU9kDkfrCXUozqptA==
dependencies: dependencies:
chalk "^2.4.1" chalk "^2.4.1"
htmlparser2 "^3.10.0" htmlparser2 "^3.10.0"

50
cypress/README.md Normal file
View File

@ -0,0 +1,50 @@
# End-to-End Testing
## Configure cypress
First, you have to tell cypress how to connect to your local neo4j database
among other things. You can copy our template configuration and change the new
file according to your needs.
Make sure you are at the root level of the project. Then:
```bash
# in the top level folder Human-Connection/
$ cp cypress.env.template.json cypress.env.json
```
## Run Tests
To run the tests, do this:
```bash
# in the top level folder Human-Connection/
$ yarn cypress:setup
```
After verifying that there are no errors with the servers starting, open another tab in your terminal and run the following command:
```bash
$ yarn cypress:run
```
![Console output after running cypress test](../.gitbook/assets/grafik%20%281%29.png)
After the test runs, you will also get some video footage of the test run which you can then analyse in more detail.
## Open Interactive Test Console
If you are like me, you might want to see some visual output. The interactive cypress environment also helps at debugging your tests, you can even time travel between individual steps and see the exact state of the app.
To use this feature, you will still run the `yarn cypress:setup` above, but instead of `yarn cypress:run` open another tab in your terminal and run the following command:
```bash
$ yarn cypress:open
```
![Interactive Cypress Environment](../.gitbook/assets/grafik-1%20%281%29.png)
## Write some Tests
Check out the Cypress documentation for further information on how to write tests:
[https://docs.cypress.io/guides/getting-started/writing-your-first-test.html\#Write-a-simple-test](https://docs.cypress.io/guides/getting-started/writing-your-first-test.html#Write-a-simple-test)

279
cypress/features.md Normal file
View File

@ -0,0 +1,279 @@
# Network Specification
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.
* **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**.
## Features
The following features will be implemented. This gets done in three steps:
1. First we will implement a basic feature set and provide a test system to test the basic network functionality.
2. In a second step we will make our prototype publicly available with an advanced feature set including the technology and organizational structure to drive a bigger public social network.
3. In a third step all the remaining features will be implemented to build the full product.
### User Account
[Cucumber Features](./integration/user_account)
* Sign-up
* Agree to Data Privacy Statement
* Agree to Terms of Service
* Login
* Logoff
* Change User Name
* Change Email Address
* Change Password
* Delete Account
* Download User's Content
* GDPR-Information about stored Content
* Choosing Interface Language \(e.g. German / English / French\)
* Persistent Links
### User Profile
[Cucumber Features](./integration/user_profile)
* Upload and Change Avatar
* Upload and Change Profile Picture
* Edit Social Media Accounts
* Edit Locale information
* Show and delete Bookmarks \(later\)
* Show Posts of a specific User
* Show Comments of a specific User
### Dashboard
[Clickdummy](https://preview.uxpin.com/24a2ab8adcd84f9a763d87ed27251351225e0ecd#/pages/99768919/simulate/sitemap?mode=i)
* Show Link to own Profile
* Show Friends Widget
* Show Favorites Widget
* Show Get Friends Widget
* Show popular Hashtags Widget
* Show Mini-Statistics Widget \(all time\)
* Show Chatrooms Widget
* Show List of Let's Talk requests with online status of requesting people
### Posts
[Cucumber Features](./integration/post/)
* Creating Posts
* Persistent Links
* Upload Teaser Picture for Post
* Upload additional Pictures
* Editing Title and Content
* Allow embedded Conten \(Videos, Sound, ...\)
* Choosing a Category
* Adding Tags
* Choosing Language \(e.g. German / English / French\)
* Choosing Visibility \(Public / Friends / Private\)
* Shout Button for Posts
* Bookmark Posts \(later\)
* Optionally provide Let's Talk Feature
* Optionally provide Commenting Feature
### Comments
* Creating Comments
* Deleting Comments
* Editing Comments
* Upvote comments of others
### Notifications
[Cucumber features](./integration/notifications)
* User @-mentionings
* Notify authors for comments
* Administrative notifications to all users
### Contribution List
* Show Posts by Tiles
* Show Posts as List
* Filter by Category \(Health and Wellbeing, Global Peace & Non-Violence, ...\)
* Filter by Mood \(Funny, Happy, Surprised, Cry, Angry, ...\)
* Filter by Source \(Connections, Following, Individuals, Non-Profits, ...\)
* Filter by Posts & Tools \(Post, Events, CanDos, ...\)
* Filter by Format Type \(Text, Pictures, Video, ...\)
* Extended Filter \(Continent, Country, Language, ...\)
* Sort Posts by Date
* Sort Posts by Shouts
* Sort Posts by most Comments
* Sort Posts by Emoji-Count \(all Types\)
### Blacklist
[Video](https://www.youtube.com/watch?v=-uDvvmN8hLQ)
* Blacklist Users
* Blacklist specific Terms
* Blacklist Tags
* Switch on/off Adult Content
### Search
[Cucumber Features](./integration/search)
* Search for Categories
* Search for Tags
* Fulltext Search
### CanDos
* Creating CanDos
* Editing Title and Content
* Choosing a Category
* Adding Tags
* Choosing Language \(e.g. German / English / French\)
* Choosing Visibility \(Public / Friends / Private\)
* Choosing Difficulty
* Editing Why - why should you do this
* Editing Usefulness - what is it good for
### Versus \(interaction on existing Post\)
* Create / edit / delete Versus
### Jobs
* Create, edit and delete Jobs by an User
* Handle Jobs as Part of Projects
* Handle Jobs done by Organizations
### Projects
* Create, edit and delete Projects
* Edit Title and Description for the Project
* Set Project Type
* Set and Edit Timeline for the Project
* Add Media to the Project
* Chat about the Project
### Pro & Contra
* Create Pro and Con \(2-row\)
* Add Arguments on Pro or Con Side
* Rate up Arguments
* Add Tags
* Attach Media
### Votes
* Create Votes \(Surveys with two or more Choices\)
* Add Title and Description
* Let Users vote
* Add Tags
### Bestlist
* Create Bestlist
* Create Votes \(Surveys\)
* Add Title and Description
* Add Tags
* Let Users vote for Best Item
* Set Settings \(allow Uploads, allow Links, ...\)
### Events
* Create Events
* Add Title and Description
* Choose Date and Location
* Add Tags
### More Info
Shows autmatically releated information for existing post.
* Show related Posts
* Show Pros and Cons
* Show Bestlist
* Show Votes
* Link to corresponding Chatroom
### Take Action
Shows automatically related actions for existing post.
* Show related Organisations
* Show related CanDos
* Show related Projects
* Show related Jobs
* Show related Events
* Show Map
### Badges System
* Importing Badge Information \(CSV\)
* Showing Badges
* Badge Administration by Admins
* Choosing Badges to display by User
### Chat
* Basic 1:1 Chat functionality
### Let's Talk
* Request Let's talk with Author of Post
* Requestor can request private or public Let's Talk
* Requestor can choose the Chat format \(Video, Audio, Text\)
* Interact with interested People 1:1
* Approve request from Requestor
### Organizations
* Propose Organizations by users
* Set Name and Details
* Set Homepage
* Set Region
* Set Topic
* Commit organizations by HC-Org-Team
* Panel for Organisation Handling by themselfes
* Choose/Mark Users as authorized to manage an Organization
### Moderation
[Cucumber Features](./integration/moderation)
* Report Button for users for doubtful Content
* Moderator Panel
* List of reported Content \(later replaced by User-Moderation\)
* Mark verified Users as Moderators
* Show Posts to be moderated highlighted to User-Moderators
* Statistics about kinds of reported Content by Time
* Statistics about Decisions in Moderation
### Administration
* Provide Admin-Interface to send Users Invite Code
* Static Pages for Data Privacy Statement ...
* Create, edit and delete Announcements
* Show Announcements on top of User Interface
### Invitation
* Allow Users to invite others by Email
* Allow Users to register with Invite Code
* Double-opt-in by Email
### Internationalization
[Cucumber Features](./integration/internationalization)
* Frontend UI
* Backend Error Messages
### Federation
* Provide Server-Server ActivityPub-API
* Provide User-Server Activitypub-API
* Receiving public addressed Article and Note Objects
* Receiving Like and Follow Activities
* Receiving Undo and Delete Activities for Articles and Notes
* Serving Webfinger records and Actor Objects
* Serving Followers, Following and Outbox collections

View File

@ -0,0 +1,20 @@
import { When, Then } from 'cypress-cucumber-preprocessor/steps'
Then('I click on the {string} button', text => {
cy.get('button').contains(text).click()
})
Then('my comment should be successfully created', () => {
cy.get('.iziToast-message')
.contains('Comment Submitted')
})
Then('I should see my comment', () => {
cy.get('div.comment p')
.should('contain', 'Human Connection rocks')
})
Then('the editor should be cleared', () => {
cy.get('.ProseMirror p')
.should('have.class', 'is-empty')
})

View File

@ -49,25 +49,20 @@ When('I click on "Report Post" from the content menu of the post', () => {
.click() .click()
}) })
When( When('I click on "Report User" from the content menu in the user info box', () => {
'I click on "Report User" from the content menu in the user info box', cy.contains('.ds-card', davidIrvingPostTitle)
() => { .get('.user-content-menu .content-menu-trigger')
cy.contains('.ds-card', davidIrvingName) .click({ force: true })
.find('.content-menu-trigger')
.first()
.click()
cy.get('.popover .ds-menu-item-link') cy.get('.popover .ds-menu-item-link')
.contains('Report User') .contains('Report User')
.click() .click()
} })
)
When('I click on the author', () => { When('I click on the author', () => {
cy.get('a.user') cy.get('.username')
.first()
.click() .click()
.wait(200) .url().should('include', '/profile/')
}) })
When('I report the author', () => { When('I report the author', () => {

View File

@ -61,3 +61,52 @@ Then(
'I can see my new name {string} when I click on my profile picture in the top right', 'I can see my new name {string} when I click on my profile picture in the top right',
name => matchNameInUserMenu(name) name => matchNameInUserMenu(name)
) )
When('I click on the {string} link', link => {
cy.get('a')
.contains(link)
.click()
})
Then('I should be on the {string} page', page => {
cy.location()
.should(loc => {
expect(loc.pathname).to.eq(page)
})
.get('h3')
.should('contain', 'Social media')
})
Then('I add a social media link', () => {
cy.get("input[name='social-media']")
.type('https://freeradical.zone/peter-pan')
.get('button')
.contains('Add link')
.click()
})
Then('it gets saved successfully', () => {
cy.get('.iziToast-message')
.should('contain', 'Updated user')
})
Then('the new social media link shows up on the page', () => {
cy.get('a[href="https://freeradical.zone/peter-pan"]')
.should('have.length', 1)
})
Given('I have added a social media link', () => {
cy.openPage('/settings/my-social-media')
.get("input[name='social-media']")
.type('https://freeradical.zone/peter-pan')
.get('button')
.contains('Add link')
.click()
})
Then('they should be able to see my social media links', () => {
cy.get('.ds-card-content')
.contains('Where else can I find Peter Pan?')
.get('a[href="https://freeradical.zone/peter-pan"]')
.should('have.length', 1)
})

View File

@ -228,7 +228,7 @@ Then('I get redirected to {string}', route => {
}) })
Then('the post was saved successfully', () => { Then('the post was saved successfully', () => {
cy.get('.ds-card-header > .ds-heading').should('contain', lastPost.title) cy.get('.ds-card-content > .ds-heading').should('contain', lastPost.title)
cy.get('.content').should('contain', lastPost.content) cy.get('.content').should('contain', lastPost.content)
}) })
@ -293,3 +293,43 @@ Then('I can login successfully with password {string}', password => {
}) })
cy.get('.iziToast-wrapper').should('contain', "You are logged in!") cy.get('.iziToast-wrapper').should('contain', "You are logged in!")
}) })
When('I log in with the following credentials:', table => {
const { email, password } = table.hashes()[0]
cy.login({ email, password })
})
When('open the notification menu and click on the first item', () => {
cy.get('.notifications-menu').click()
cy.get('.notification-mention-post').first().click()
})
Then('see {int} unread notifications in the top menu', count => {
cy.get('.notifications-menu').should('contain', count)
})
Then('I get to the post page of {string}', path => {
path = path.replace('...', '')
cy.url().should('contain', '/post/')
cy.url().should('contain', path)
})
When('I start to write a new post with the title {string} beginning with:', (title, intro) => {
cy.get('.post-add-button').click()
cy.get('input[name="title"]').type(title)
cy.get('.ProseMirror').type(intro)
})
When('mention {string} in the text', (mention) => {
cy.get('.ProseMirror').type(' @')
cy.get('.suggestion-list__item').contains(mention).click()
cy.debug()
})
Then('the notification gets marked as read', () => {
cy.get('.notification').first().should('have.class', 'read')
})
Then('there are no notifications in the top menu', () => {
cy.get('.notifications-menu').should('contain', '0')
})

View File

@ -1,6 +1,6 @@
Feature: Report and Moderate Feature: Report and Moderate
As a user As a user
I would like to report content that viloates the community guidlines I would like to report content that violates the community guidlines
So the moderators can take action on it So the moderators can take action on it
As a moderator As a moderator
@ -12,6 +12,7 @@ Feature: Report and Moderate
| Author | id | title | content | | Author | id | title | content |
| David Irving | p1 | The Truth about the Holocaust | It never existed! | | David Irving | p1 | The Truth about the Holocaust | It never existed! |
Scenario Outline: Report a post from various pages Scenario Outline: Report a post from various pages
Given I am logged in with a "user" role Given I am logged in with a "user" role
When I see David Irving's post on the <Page> When I see David Irving's post on the <Page>

View File

@ -0,0 +1,31 @@
Feature: Notifications for a mentions
As a user
I want to be notified if sb. mentions me in a post or comment
In order join conversations about or related to me
Background:
Given we have the following user accounts:
| name | slug | email | password |
| Wolle aus Hamburg | wolle-aus-hamburg | wolle@example.org | 1234 |
| Matt Rider | matt-rider | matt@example.org | 4321 |
Scenario: Mention another user, re-login as this user and see notifications
Given I log in with the following credentials:
| email | password |
| wolle@example.org | 1234 |
And I start to write a new post with the title "Hey Matt" beginning with:
"""
Big shout to our fellow contributor
"""
And mention "@matt-rider" in the text
And I click on "Save"
When I log out
And I log in with the following credentials:
| email | password |
| matt@example.org | 4321 |
And see 1 unread notifications in the top menu
And open the notification menu and click on the first item
Then I get to the post page of ".../hey-matt"
And the notification gets marked as read
But when I refresh the page
Then there are no notifications in the top menu

View File

@ -0,0 +1,22 @@
Feature: Post Comment
As a user
I want to comment on contributions of others
To be able to express my thoughts and emotions about these, discuss, and add give further information.
Background:
Given we have the following posts in our database:
| id | title | slug |
| bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays |
And I have a user account
And I am logged in
Scenario: Comment creation
Given I visit "post/bWBjpkTKZp/101-essays"
And I type in the following text:
"""
Human Connection rocks
"""
And I click on the "Comment" button
Then my comment should be successfully created
And I should see my comment
And the editor should be cleared

View File

@ -0,0 +1,21 @@
Feature: List Social Media Accounts
As a User
I'd like to enter my social media
So I can show them to other users to get in contact
Background:
Given I have a user account
And I am logged in
Scenario: Adding Social Media
Given I am on the "settings" page
And I click on the "Social media" link
Then I should be on the "/settings/my-social-media" page
When I add a social media link
Then it gets saved successfully
And the new social media link shows up on the page
Scenario: Other user's viewing my Social Media
Given I have added a social media link
When people visit my profile page
Then they should be able to see my social media links

View File

@ -46,7 +46,8 @@ Cypress.Commands.add('login', ({ email, password }) => {
cy.get('button[name=submit]') cy.get('button[name=submit]')
.as('submitButton') .as('submitButton')
.click() .click()
cy.location('pathname').should('eq', '/') // we're in! cy.get('.iziToast-message').should('contain', 'You are logged in!')
cy.get('.iziToast-close').click()
}) })
Cypress.Commands.add('logout', (email, password) => { Cypress.Commands.add('logout', (email, password) => {

View File

@ -1,3 +1,4 @@
secrets.yaml secrets.yaml
*/secrets.yaml configmap.yaml
kubeconfig.yaml **/secrets.yaml
**/configmap.yaml

View File

@ -1,225 +1,11 @@
# Human-Connection Nitro | Deployment Configuration # Human-Connection Nitro \| Deployment Configuration
[![Build Status](https://travis-ci.com/Human-Connection/Nitro-Deployment.svg?branch=master)](https://travis-ci.com/Human-Connection/Nitro-Deployment)
Todos: We deploy with [kubernetes](https://kubernetes.io/). In order to deploy your own
- [x] check labels and selectors if they all are correct network you have to [install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
- [x] configure NGINX from yml and get a kubernetes cluster.
- [x] configure Let's Encrypt cert-manager from yml
- [x] configure ingress from yml
- [x] configure persistent & shared storage between nodes
- [x] reproduce setup locally
## Minikube We have tested two different kubernetes providers: [Minikube](./minikube/README.md)
There are many Kubernetes distributions, but if you're just getting started, and [Digital Ocean](./digital-ocean/README.md).
Minikube is a tool that you can use to get your feet wet.
[Install Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/) Check out the specific documentation for your provider. After that, learn how
to apply the specific kubernetes configuration for [Human Connection](./human-connection/README.md).
Open minikube dashboard:
```
$ minikube dashboard
```
This will give you an overview.
Some of the steps below need some timing to make ressources available to other
dependent deployments. Keeping an eye on the dashboard is a great way to check
that.
Follow the [installation instruction](#installation-with-kubernetes) below.
If all the pods and services have settled and everything looks green in your
minikube dashboard, expose the `nitro-web` service on your host system with:
```shell
$ minikube service nitro-web --namespace=human-connection
```
## Digital Ocean
1. At first, create a cluster on Digital Ocean.
2. Download the config.yaml if the process has finished.
3. Put the config file where you can find it later (preferable in your home directory under `~/.kube/`)
4. In the open terminal you can set the current config for the active session: `export KUBECONFIG=~/.kube/THE-NAME-OF-YOUR-CLUSTER-kubeconfig.yaml`. You could make this change permanent by adding the line to your `.bashrc` or `~/.config/fish/config.fish` depending on your shell.
Otherwise you would have to always add `--kubeconfig ~/.kube/THE-NAME-OF-YOUR-CLUSTER-kubeconfig.yaml` on every `kubectl` command that you are running.
5. Now check if you can connect to the cluster and if its your newly created one by running: `kubectl get nodes`
If you got the steps right above and see your nodes you can continue.
First, install kubernetes dashboard:
```sh
$ kubectl apply -f dashboard/
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended/kubernetes-dashboard.yaml
```
Get your token on the command line:
```sh
$ kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}')
```
It should print something like:
```
Name: admin-user-token-6gl6l
Namespace: kube-system
Labels: <none>
Annotations: kubernetes.io/service-account.name=admin-user
kubernetes.io/service-account.uid=b16afba9-dfec-11e7-bbb9-901b0e532516
Type: kubernetes.io/service-account-token
Data
====
ca.crt: 1025 bytes
namespace: 11 bytes
token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi11c2VyLXRva2VuLTZnbDZsIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluLXVzZXIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJiMTZhZmJhOS1kZmVjLTExZTctYmJiOS05MDFiMGU1MzI1MTYiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06YWRtaW4tdXNlciJ9.M70CU3lbu3PP4OjhFms8PVL5pQKj-jj4RNSLA4YmQfTXpPUuxqXjiTf094_Rzr0fgN_IVX6gC4fiNUL5ynx9KU-lkPfk0HnX8scxfJNzypL039mpGt0bbe1IXKSIRaq_9VW59Xz-yBUhycYcKPO9RM2Qa1Ax29nqNVko4vLn1_1wPqJ6XSq3GYI8anTzV8Fku4jasUwjrws6Cn6_sPEGmL54sq5R4Z5afUtv-mItTmqZZdxnkRqcJLlg2Y8WbCPogErbsaCDJoABQ7ppaqHetwfM_0yMun6ABOQbIwwl8pspJhpplKwyo700OSpvTT9zlBsu-b35lzXGBRHzv5g_RA
```
Proxy localhost to the remote kubernetes dashboard:
```sh
$ kubectl proxy
```
Grab the token from above and paste it into the login screen at [http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/](http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/)
## Installation with kubernetes
You have to do some prerequisites e.g. change some secrets according to your
own setup.
### Edit secrets
```sh
$ cp secrets.template.yaml human-connection/secrets.yaml
```
Change all secrets as needed.
If you want to edit secrets, you have to `base64` encode them. See [kubernetes
documentation](https://kubernetes.io/docs/concepts/configuration/secret/#creating-a-secret-manually).
```shell
# example how to base64 a string:
$ echo -n 'admin' | base64
YWRtaW4=
```
Those secrets get `base64` decoded in a kubernetes pod.
### Create a namespace
```shell
$ kubectl apply -f namespace-human-connection.yaml
```
Switch to the namespace `human-connection` in your kubernetes dashboard.
### Run the configuration
```shell
$ kubectl apply -f human-connection/
```
This can take a while because kubernetes will download the docker images.
Sit back and relax and have a look into your kubernetes dashboard.
Wait until all pods turn green and they don't show a warning
`Waiting: ContainerCreating` anymore.
#### Setup Ingress and HTTPS
Follow [this quick start guide](https://docs.cert-manager.io/en/latest/tutorials/acme/quick-start/index.html)
and install certmanager via helm and tiller:
```
$ kubectl create serviceaccount tiller --namespace=kube-system
$ kubectl create clusterrolebinding tiller-admin --serviceaccount=kube-system:tiller --clusterrole=cluster-admin
$ helm init --service-account=tiller
$ helm repo update
$ helm install stable/nginx-ingress
$ kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.6/deploy/manifests/00-crds.yaml
$ helm install --name cert-manager --namespace cert-manager stable/cert-manager
```
Create letsencrypt issuers. *Change the email address* in these files before
running this command.
```sh
$ kubectl apply -f human-connection/https/
```
Create an ingress service in namespace `human-connection`. *Change the domain
name* according to your needs:
```sh
$ kubectl apply -f human-connection/ingress/
```
Check the ingress server is working correctly:
```sh
$ curl -kivL -H 'Host: <DOMAIN_NAME>' 'https://<IP_ADDRESS>'
```
If the response looks good, configure your domain registrar for the new IP
address and the domain.
Now let's get a valid HTTPS certificate. According to the tutorial above, check
your tls certificate for staging:
```sh
$ kubectl describe --namespace=human-connection certificate tls
$ kubectl describe --namespace=human-connection secret tls
```
If everything looks good, update the issuer of your ingress. Change the
annotation `certmanager.k8s.io/issuer` from `letsencrypt-staging` to
`letsencrypt-prod` in your ingress configuration in
`human-connection/ingress/ingress.yaml`.
```sh
$ kubectl apply -f human-connection/ingress/ingress.yaml
```
Delete the former secret to force a refresh:
```
$ kubectl --namespace=human-connection delete secret tls
```
Now, HTTPS should be configured on your domain. Congrats.
#### Legacy data migration
This setup is completely optional and only required if you have data on a server
which is running our legacy code and you want to import that data. It will
import the uploads folder and migrate a dump of mongodb into neo4j.
##### Prepare migration of Human Connection legacy server
Create a configmap with the specific connection data of your legacy server:
```sh
$ kubectl create configmap db-migration-worker \
--namespace=human-connection \
--from-literal=SSH_USERNAME=someuser \
--from-literal=SSH_HOST=yourhost \
--from-literal=MONGODB_USERNAME=hc-api \
--from-literal=MONGODB_PASSWORD=secretpassword \
--from-literal=MONGODB_AUTH_DB=hc_api \
--from-literal=MONGODB_DATABASE=hc_api \
--from-literal=UPLOADS_DIRECTORY=/var/www/api/uploads \
--from-literal=NEO4J_URI=bolt://localhost:7687
```
Create a secret with your public and private ssh keys. As the
[kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/secret/#use-case-pod-with-ssh-keys)
points out, you should be careful with your ssh keys. Anyone with access to your
cluster will have access to your ssh keys. Better create a new pair with
`ssh-keygen` and copy the public key to your legacy server with `ssh-copy-id`:
```sh
$ kubectl create secret generic ssh-keys \
--namespace=human-connection \
--from-file=id_rsa=/path/to/.ssh/id_rsa \
--from-file=id_rsa.pub=/path/to/.ssh/id_rsa.pub \
--from-file=known_hosts=/path/to/.ssh/known_hosts
```
##### Migrate legacy database
Patch the existing deployments to use a multi-container setup:
```bash
cd legacy-migration
kubectl apply -f volume-claim-mongo-export.yaml
kubectl patch --namespace=human-connection deployment nitro-backend --patch "$(cat deployment-backend.yaml)"
kubectl patch --namespace=human-connection deployment nitro-neo4j --patch "$(cat deployment-neo4j.yaml)"
cd ..
```
Run the migration:
```shell
$ kubectl --namespace=human-connection get pods
# change <POD_IDs> below
$ kubectl --namespace=human-connection exec -it nitro-neo4j-65bbdb597c-nc2lv migrate
$ kubectl --namespace=human-connection exec -it nitro-backend-c6cc5ff69-8h96z sync_uploads
```

83
deployment/backup.md Normal file
View File

@ -0,0 +1,83 @@
# Backup (offline)
This tutorial explains how to carry out an offline backup of your Neo4J
database in a kubernetes cluster.
An offline backup requires the Neo4J database to be stopped. Read
[the docs](https://neo4j.com/docs/operations-manual/current/tools/dump-load/).
Neo4J also offers online backups but this is available in enterprise edition
only.
The tricky part is to stop the Neo4J database *without* stopping the container.
Neo4J's docker container image starts `neo4j` by default, so we have to override
this command with sth. that keeps the container spinning but does not terminate
it.
## Stop and Restart Neo4J Database in Kubernetes
[This tutorial](http://bigdatums.net/2017/11/07/how-to-keep-docker-containers-running/)
explains how to keep a docker container running. For kubernetes, the way to
override the docker image `CMD` is explained [here](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#define-a-command-and-arguments-when-you-create-a-pod).
So, all we have to do is edit the kubernetes deployment of our Neo4J database
and set a custom `command` every time we have to carry out tasks like backup,
restore, seed etc.
{% hint style="info" %} TODO: implement maintenance mode {% endhint %}
First bring the application into maintenance mode to ensure there are no
database connections left and nobody can access the application.
Run the following:
```sh
kubectl --namespace=human-connection edit deployment nitro-neo4j
```
Add the following to `spec.template.spec.containers`:
```
["tail", "-f", "/dev/null"]
```
and write the file which will update the deployment.
The command `tail -f /dev/null` is the equivalent of *sleep forever*. It is a
hack to keep the container busy and to prevent its shutdown. It will also
override the default `neo4j` command and the kubernetes pod will not start the
database.
Now perform your tasks!
When you're done, edit the deployment again and remove the `command`. Write the
file and trigger an update of the deployment.
## Create a Backup in Kubernetes
First stop your Neo4J database, see above. Then:
```sh
kubectl --namespace=human-connection get pods
# Copy the ID of the pod running Neo4J.
kubectl --namespace=human-connection exec -it <POD-ID> bash
# Once you're in the pod, dump the db to a file e.g. `/root/neo4j-backup`.
neo4j-admin dump --to=/root/neo4j-backup
exit
# Download the file from the pod to your computer.
kubectl cp human-connection/<POD-ID>:/root/neo4j-backup ./neo4j-backup
```
Revert your changes to deployment `nitro-neo4j` which will restart the database.
## Restore a Backup in Kubernetes
First stop your Neo4J database. Then:
```sh
kubectl --namespace=human-connection get pods
# Copy the ID of the pod running Neo4J.
# Then upload your local backup to the pod. Note that once the pod gets deleted
# e.g. if you change the deployment, the backup file is gone with it.
kubectl cp ./neo4j-backup human-connection/<POD-ID>:/root/
kubectl --namespace=human-connection exec -it <POD-ID> bash
# Once you're in the pod restore the backup and overwrite the default database
# called `graph.db` with `--force`.
# This will delete all existing data in database `graph.db`!
neo4j-admin load --from=/root/neo4j-backup --force
exit
```
Revert your changes to deployment `nitro-neo4j` which will restart the database.

View File

@ -9,8 +9,6 @@
NEO4J_USER: "neo4j" NEO4J_USER: "neo4j"
NEO4J_AUTH: "none" NEO4J_AUTH: "none"
CLIENT_URI: "https://nitro-staging.human-connection.org" CLIENT_URI: "https://nitro-staging.human-connection.org"
MAPBOX_TOKEN: "pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"
PRIVATE_KEY_PASSPHRASE: "a7dsf78sadg87ad87sfagsadg78"
metadata: metadata:
name: configmap name: configmap
namespace: human-connection namespace: human-connection

View File

@ -1,39 +0,0 @@
---
kind: Pod
apiVersion: v1
metadata:
name: nitro-db-migration-worker
namespace: human-connection
spec:
volumes:
- name: secret-volume
secret:
secretName: ssh-keys
defaultMode: 0400
- name: mongo-export
persistentVolumeClaim:
claimName: mongo-export-claim
containers:
- name: nitro-db-migration-worker
image: humanconnection/db-migration-worker:latest
envFrom:
- configMapRef:
name: db-migration-worker
volumeMounts:
- name: secret-volume
readOnly: false
mountPath: /root/.ssh
- name: mongo-export
mountPath: /mongo-export/
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mongo-export-claim
namespace: human-connection
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

View File

@ -0,0 +1,26 @@
# Digital Ocean
As a start, read the [introduction into kubernetes](https://www.digitalocean.com/community/tutorials/an-introduction-to-kubernetes) by the folks at Digital Ocean. The following section should enable you to deploy Human Connection to your kubernetes cluster.
## Connect to your local cluster
1. Create a cluster at [Digital Ocean](https://www.digitalocean.com/).
2. Download the `***-kubeconfig.yaml` from the Web UI.
3. Move the file to the default location where kubectl expects it to be: `mv ***-kubeconfig.yaml ~/.kube/config`. Alternatively you can set the config on every command: `--kubeconfig ***-kubeconfig.yaml`
4. Now check if you can connect to the cluster and if its your newly created one by running: `kubectl get nodes`
The output should look about like this:
```
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
nifty-driscoll-uu1w Ready <none> 69d v1.13.2
nifty-driscoll-uuiw Ready <none> 69d v1.13.2
nifty-driscoll-uusn Ready <none> 69d v1.13.2
```
If you got the steps right above and see your nodes you can continue.
Digital Ocean kubernetes clusters don't have a graphical interface, so I suggest
to setup the [kubernetes dashboard](./dashboard/README.md) as a next step.
Configuring [HTTPS](./https/README.md) is bit tricky and therefore I suggest to
do this as a last step.

View File

@ -0,0 +1,55 @@
# Install Kubernetes Dashboard
The kubernetes dashboard is optional but very helpful for debugging. If you want to install it, you have to do so only **once** per cluster:
```bash
# in folder deployment/digital-ocean/
$ kubectl apply -f dashboard/
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended/kubernetes-dashboard.yaml
```
### Login to your dashboard
Proxy the remote kubernetes dashboard to localhost:
```bash
$ kubectl proxy
```
Visit:
[http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/](http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/)
You should see a login screen.
To get your token for the dashboard you can run this command:
```bash
$ kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}')
```
It should print something like:
```text
Name: admin-user-token-6gl6l
Namespace: kube-system
Labels: <none>
Annotations: kubernetes.io/service-account.name=admin-user
kubernetes.io/service-account.uid=b16afba9-dfec-11e7-bbb9-901b0e532516
Type: kubernetes.io/service-account-token
Data
====
ca.crt: 1025 bytes
namespace: 11 bytes
token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi11c2VyLXRva2VuLTZnbDZsIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluLXVzZXIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJiMTZhZmJhOS1kZmVjLTExZTctYmJiOS05MDFiMGU1MzI1MTYiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06YWRtaW4tdXNlciJ9.M70CU3lbu3PP4OjhFms8PVL5pQKj-jj4RNSLA4YmQfTXpPUuxqXjiTf094_Rzr0fgN_IVX6gC4fiNUL5ynx9KU-lkPfk0HnX8scxfJNzypL039mpGt0bbe1IXKSIRaq_9VW59Xz-yBUhycYcKPO9RM2Qa1Ax29nqNVko4vLn1_1wPqJ6XSq3GYI8anTzV8Fku4jasUwjrws6Cn6_sPEGmL54sq5R4Z5afUtv-mItTmqZZdxnkRqcJLlg2Y8WbCPogErbsaCDJoABQ7ppaqHetwfM_0yMun6ABOQbIwwl8pspJhpplKwyo700OSpvTT9zlBsu-b35lzXGBRHzv5g_RA
```
Grab the token from above and paste it into the [login screen](http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/)
When you are logged in, you should see sth. like:
![Dashboard](./dashboard-screenshot.png)
Feel free to save the login token from above in your password manager. Unlike the `kubeconfig` file, this token does not expire.

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View File

@ -0,0 +1,57 @@
# Setup Ingress and HTTPS
Follow [this quick start guide](https://docs.cert-manager.io/en/latest/tutorials/acme/quick-start/index.html) and install certmanager via helm and tiller:
```text
$ kubectl create serviceaccount tiller --namespace=kube-system
$ kubectl create clusterrolebinding tiller-admin --serviceaccount=kube-system:tiller --clusterrole=cluster-admin
$ helm init --service-account=tiller
$ helm repo update
$ helm install stable/nginx-ingress
$ kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.6/deploy/manifests/00-crds.yaml
$ helm install --name cert-manager --namespace cert-manager stable/cert-manager
```
Create letsencrypt issuers. _Change the email address_ in these files before running this command.
```bash
# in folder deployment/digital-ocean/https/
$ kubectl apply -f issuer.yaml
```
Create an ingress service in namespace `human-connection`. _Change the domain name_ according to your needs:
```bash
# in folder deployment/digital-ocean/https/
$ kubectl apply -f ingress.yaml
```
Check the ingress server is working correctly:
```bash
$ curl -kivL -H 'Host: <DOMAIN_NAME>' 'https://<IP_ADDRESS>'
```
If the response looks good, configure your domain registrar for the new IP address and the domain.
Now let's get a valid HTTPS certificate. According to the tutorial above, check your tls certificate for staging:
```bash
$ kubectl describe --namespace=human-connection certificate tls
$ kubectl describe --namespace=human-connection secret tls
```
If everything looks good, update the issuer of your ingress. Change the annotation `certmanager.k8s.io/issuer` from `letsencrypt-staging` to `letsencrypt-prod` in your ingress configuration in `ingress.yaml`.
```bash
# in folder deployment/digital-ocean/https/
$ kubectl apply -f ingress.yaml
```
Delete the former secret to force a refresh:
```text
$ kubectl --namespace=human-connection delete secret tls
```
Now, HTTPS should be configured on your domain. Congrats.

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