Merging backend to master
15
backend/.babelrc
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": "10"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-throw-expressions"
|
||||
]
|
||||
}
|
||||
2
backend/.codecov.yml
Normal file
@ -0,0 +1,2 @@
|
||||
coverage:
|
||||
range: "60...100"
|
||||
22
backend/.dockerignore
Normal file
@ -0,0 +1,22 @@
|
||||
.vscode/
|
||||
.nyc_output/
|
||||
.github/
|
||||
.travis.yml
|
||||
.graphqlconfig
|
||||
.env
|
||||
|
||||
Dockerfile
|
||||
docker-compose*.yml
|
||||
|
||||
./*.png
|
||||
./*.log
|
||||
|
||||
node_modules/
|
||||
scripts/
|
||||
dist/
|
||||
|
||||
db-migration-worker/
|
||||
neo4j/
|
||||
|
||||
public/uploads/*
|
||||
!.gitkeep
|
||||
12
backend/.env.template
Normal file
@ -0,0 +1,12 @@
|
||||
NEO4J_URI=bolt://localhost:7687
|
||||
NEO4J_USER=neo4j
|
||||
NEO4J_PASSWORD=letmein
|
||||
GRAPHQL_PORT=4000
|
||||
GRAPHQL_URI=http://localhost:4000
|
||||
CLIENT_URI=http://localhost:3000
|
||||
MOCK=false
|
||||
|
||||
JWT_SECRET="b/&&7b78BF&fv/Vd"
|
||||
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"
|
||||
|
||||
PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78"
|
||||
20
backend/.eslintrc.js
Normal file
@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
"extends": "standard",
|
||||
"parser": "babel-eslint",
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"jest/globals": true
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
]
|
||||
},
|
||||
"plugins": ["jest"]
|
||||
};
|
||||
20
backend/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Authenticate '...'
|
||||
2. Post following data to endpoint '...'
|
||||
3. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
17
backend/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
13
backend/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
node_modules/
|
||||
.env
|
||||
.vscode
|
||||
.idea
|
||||
yarn-error.log
|
||||
dist/*
|
||||
coverage.lcov
|
||||
.nyc_output/
|
||||
public/uploads/*
|
||||
!.gitkeep
|
||||
|
||||
# Apple macOS folder attribute file
|
||||
.DS_Store
|
||||
3
backend/.graphqlconfig
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"schemaPath": "./src/schema.graphql"
|
||||
}
|
||||
64
backend/.travis.yml
Normal file
@ -0,0 +1,64 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "10"
|
||||
cache:
|
||||
yarn: true
|
||||
directories:
|
||||
- node_modules
|
||||
services:
|
||||
- docker
|
||||
|
||||
env:
|
||||
- DOCKER_COMPOSE_VERSION=1.23.2
|
||||
|
||||
before_install:
|
||||
- sudo rm /usr/local/bin/docker-compose
|
||||
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
|
||||
- chmod +x docker-compose
|
||||
- sudo mv docker-compose /usr/local/bin
|
||||
- yarn global add wait-on
|
||||
|
||||
install:
|
||||
- docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-backend:latest .
|
||||
- docker-compose -f docker-compose.yml -f docker-compose.travis.yml up -d
|
||||
- wait-on http://localhost:7474 && docker-compose exec neo4j migrate
|
||||
|
||||
script:
|
||||
- docker-compose exec backend yarn run lint
|
||||
- docker-compose exec backend yarn run test --ci
|
||||
- docker-compose exec backend yarn run db:reset
|
||||
- docker-compose exec backend yarn run db:seed
|
||||
- docker-compose exec backend yarn run test:cucumber
|
||||
- docker-compose exec backend yarn run test:coverage
|
||||
- docker-compose exec backend yarn run db:reset
|
||||
- docker-compose exec backend yarn run db:seed
|
||||
|
||||
after_success:
|
||||
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
|
||||
- chmod +x send.sh
|
||||
- ./send.sh success $WEBHOOK_URL
|
||||
|
||||
after_failure:
|
||||
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
|
||||
- chmod +x send.sh
|
||||
- ./send.sh failure $WEBHOOK_URL
|
||||
|
||||
deploy:
|
||||
- provider: script
|
||||
script: scripts/docker_push.sh
|
||||
on:
|
||||
branch: master
|
||||
- provider: script
|
||||
script: scripts/deploy.sh nitro.human-connection.org
|
||||
on:
|
||||
branch: master
|
||||
tags: true
|
||||
- provider: script
|
||||
script: scripts/deploy.sh nitro-staging.human-connection.org
|
||||
on:
|
||||
branch: master
|
||||
- provider: script
|
||||
script: scripts/deploy.sh "nitro-$TRAVIS_COMMIT.human-connection.org"
|
||||
on:
|
||||
tags: true
|
||||
all_branches: true
|
||||
46
backend/CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at developer@human-connection.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## 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]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
24
backend/Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
FROM node:10-alpine as base
|
||||
LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
|
||||
|
||||
EXPOSE 4000
|
||||
ARG BUILD_COMMIT
|
||||
ENV BUILD_COMMIT=$BUILD_COMMIT
|
||||
ARG WORKDIR=/nitro-backend
|
||||
RUN mkdir -p $WORKDIR
|
||||
WORKDIR $WORKDIR
|
||||
COPY package.json yarn.lock ./
|
||||
COPY .env.template .env
|
||||
CMD ["yarn", "run", "start"]
|
||||
|
||||
FROM base as builder
|
||||
RUN yarn install --frozen-lockfile --non-interactive
|
||||
COPY . .
|
||||
RUN cp .env.template .env
|
||||
RUN yarn run build
|
||||
|
||||
# reduce image size with a multistage build
|
||||
FROM base as production
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /nitro-backend/dist ./dist
|
||||
RUN yarn install --frozen-lockfile --non-interactive
|
||||
21
backend/LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Human-Connection gGmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
178
backend/README.md
Normal file
@ -0,0 +1,178 @@
|
||||
<p align="center">
|
||||
<a href="https://human-connection.org"><img align="center" src="humanconnection.png" height="200" alt="Human Connection" /></a>
|
||||
</p>
|
||||
|
||||
# NITRO Backend
|
||||
[](https://travis-ci.com/Human-Connection/Nitro-Backend)
|
||||
[](https://github.com/Human-Connection/Nitro-Backend/blob/backend/LICENSE.md)
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_shield)
|
||||
[](https://discord.gg/6ub73U3)
|
||||
|
||||
> This Prototype tries to resolve the biggest hurdle of connecting
|
||||
> 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.
|
||||
|
||||
>
|
||||
> #### 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
|
||||
Connect with other developers over [Discord](https://discord.gg/6ub73U3)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Forking the repository
|
||||
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
|
||||
|
||||
Run:
|
||||
```sh
|
||||
docker-compose up
|
||||
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
# -or-
|
||||
npm install
|
||||
```
|
||||
|
||||
Copy:
|
||||
```
|
||||
cp .env.template .env
|
||||
```
|
||||
Configure the file `.env` according to your needs and your local setup.
|
||||
|
||||
Start the GraphQL service:
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
# -or-
|
||||
npm dev
|
||||
```
|
||||
|
||||
And on the production machine run following:
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
# -or-
|
||||
npm start
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
Set your Neo4j connection string and credentials in `.env`.
|
||||
For example:
|
||||
|
||||
_.env_
|
||||
|
||||
```yaml
|
||||
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!
|
||||
|
||||
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`.
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
yarn run db:seed
|
||||
# -or-
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
For a reset you can use the reset script:
|
||||
|
||||
```bash
|
||||
yarn db:reset
|
||||
# -or-
|
||||
npm run db:reset
|
||||
```
|
||||
|
||||
## Run Tests
|
||||
|
||||
**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:
|
||||
```bash
|
||||
yarn run test
|
||||
# -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
|
||||
## Todo`s
|
||||
|
||||
- [x] add jwt authentication
|
||||
- [ ] get directives working correctly (@toLower, @auth, @role, etc.)
|
||||
- [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
|
||||
|
||||
|
||||
## License
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_large)
|
||||
1
backend/db-migration-worker/.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
.ssh/
|
||||
1
backend/db-migration-worker/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.ssh/
|
||||
3
backend/db-migration-worker/.ssh/known_hosts
Normal file
@ -0,0 +1,3 @@
|
||||
|1|GuOYlVEhTowidPs18zj9p5F2j3o=|sDHJYLz9Ftv11oXeGEjs7SpVyg0= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBM5N29bI5CeKu1/RBPyM2fwyf7fuajOO+tyhKe1+CC2sZ1XNB5Ff6t6MtCLNRv2mUuvzTbW/HkisDiA5tuXUHOk=
|
||||
|1|2KP9NV+Q5g2MrtjAeFSVcs8YeOI=|nf3h4wWVwC4xbBS1kzgzE2tBldk= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNhRK6BeIEUxXlS0z/pOfkUkSPfn33g4J1U3L+MyUQYHm+7agT08799ANJhnvELKE1tt4Vx80I9UR81oxzZcy3E=
|
||||
|1|HonYIRNhKyroUHPKU1HSZw0+Qzs=|5T1btfwFBz2vNSldhqAIfTbfIgQ= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNhRK6BeIEUxXlS0z/pOfkUkSPfn33g4J1U3L+MyUQYHm+7agT08799ANJhnvELKE1tt4Vx80I9UR81oxzZcy3E=
|
||||
13
backend/db-migration-worker/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
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
|
||||
12
backend/db-migration-worker/migrate.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
for var in "SSH_USERNAME" "SSH_HOST" "MONGODB_USERNAME" "MONGODB_PASSWORD" "MONGODB_DATABASE" "MONGODB_AUTH_DB" "NEO4J_URI"
|
||||
do
|
||||
if [[ -z "${!var}" ]]; then
|
||||
echo "${var} is undefined"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
/migration/mongo/import.sh
|
||||
/migration/neo4j/import.sh
|
||||
24
backend/db-migration-worker/migration/mongo/import.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "SSH_USERNAME ${SSH_USERNAME}"
|
||||
echo "SSH_HOST ${SSH_HOST}"
|
||||
echo "MONGODB_USERNAME ${MONGODB_USERNAME}"
|
||||
echo "MONGODB_PASSWORD ${MONGODB_PASSWORD}"
|
||||
echo "MONGODB_DATABASE ${MONGODB_DATABASE}"
|
||||
echo "MONGODB_AUTH_DB ${MONGODB_AUTH_DB}"
|
||||
echo "-------------------------------------------------"
|
||||
|
||||
mongo ${MONGODB_DATABASE} --eval "db.dropDatabase();"
|
||||
rm -rf /mongo-export/*
|
||||
|
||||
ssh -4 -M -S my-ctrl-socket -fnNT -L 27018:localhost:27017 -l ${SSH_USERNAME} ${SSH_HOST}
|
||||
mongodump --host localhost -d ${MONGODB_DATABASE} --port 27018 --username ${MONGODB_USERNAME} --password ${MONGODB_PASSWORD} --authenticationDatabase ${MONGODB_AUTH_DB} --gzip --archive=/tmp/mongodump.archive
|
||||
mongorestore --gzip --archive=/tmp/mongodump.archive
|
||||
ssh -S my-ctrl-socket -O check -l ${SSH_USERNAME} ${SSH_HOST}
|
||||
ssh -S my-ctrl-socket -O exit -l ${SSH_USERNAME} ${SSH_HOST}
|
||||
|
||||
for collection in "categories" "badges" "users" "contributions" "comments" "follows" "shouts"
|
||||
do
|
||||
mongoexport --db ${MONGODB_DATABASE} --collection $collection --out "/mongo-export/$collection.json"
|
||||
done
|
||||
10
backend/db-migration-worker/migration/neo4j/badges.cql
Normal file
@ -0,0 +1,10 @@
|
||||
CALL apoc.load.json('file:/mongo-export/badges.json') YIELD value as badge
|
||||
MERGE(b:Badge {id: badge._id["$oid"]})
|
||||
ON CREATE SET
|
||||
b.key = badge.key,
|
||||
b.type = badge.type,
|
||||
b.icon = badge.image.path,
|
||||
b.status = badge.status,
|
||||
b.createdAt = badge.createdAt.`$date`,
|
||||
b.updatedAt = badge.updatedAt.`$date`
|
||||
;
|
||||
89
backend/db-migration-worker/migration/neo4j/categories.cql
Normal file
@ -0,0 +1,89 @@
|
||||
CALL apoc.load.json('file:/mongo-export/categories.json') YIELD value as category
|
||||
MERGE(c:Category {id: category._id["$oid"]})
|
||||
ON CREATE SET
|
||||
c.name = category.title,
|
||||
c.slug = category.slug,
|
||||
c.icon = category.icon,
|
||||
c.createdAt = category.createdAt.`$date`,
|
||||
c.updatedAt = category.updatedAt.`$date`
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-justforfun")
|
||||
SET c.icon = 'smile'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-luck")
|
||||
SET c.icon = 'heart-o'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-health")
|
||||
SET c.icon = 'medkit'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-environment")
|
||||
SET c.icon = 'tree'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-animal-justice")
|
||||
SET c.icon = 'paw'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-human-rights")
|
||||
SET c.icon = 'balance-scale'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-education")
|
||||
SET c.icon = 'graduation-cap'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-cooperation")
|
||||
SET c.icon = 'users'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-politics")
|
||||
SET c.icon = 'university'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-economy")
|
||||
SET c.icon = 'money'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-technology")
|
||||
SET c.icon = 'flash'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-internet")
|
||||
SET c.icon = 'mouse-pointer'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-art")
|
||||
SET c.icon = 'paint-brush'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-freedom-of-speech")
|
||||
SET c.icon = 'bullhorn'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-sustainability")
|
||||
SET c.icon = 'shopping-cart'
|
||||
;
|
||||
|
||||
MATCH (c:Category)
|
||||
WHERE (c.icon = "categories-peace")
|
||||
SET c.icon = 'angellist'
|
||||
;
|
||||
14
backend/db-migration-worker/migration/neo4j/comments.cql
Normal file
@ -0,0 +1,14 @@
|
||||
CALL apoc.load.json('file:/mongo-export/comments.json') YIELD value as json
|
||||
MERGE (comment:Comment {id: json._id["$oid"]})
|
||||
ON CREATE SET
|
||||
comment.content = json.content,
|
||||
comment.contentExcerpt = json.contentExcerpt,
|
||||
comment.deleted = json.deleted,
|
||||
comment.disabled = false
|
||||
WITH comment, json, json.contributionId as postId
|
||||
MATCH (post:Post {id: postId})
|
||||
WITH comment, post, json.userId as userId
|
||||
MATCH (author:User {id: userId})
|
||||
MERGE (comment)-[:COMMENTS]->(post)
|
||||
MERGE (author)-[:WROTE]->(comment)
|
||||
;
|
||||
@ -0,0 +1,25 @@
|
||||
CALL apoc.load.json('file:/mongo-export/contributions.json') YIELD value as post
|
||||
MERGE (p:Post {id: post._id["$oid"]})
|
||||
ON CREATE SET
|
||||
p.title = post.title,
|
||||
p.slug = post.slug,
|
||||
p.image = post.teaserImg,
|
||||
p.content = post.content,
|
||||
p.contentExcerpt = post.contentExcerpt,
|
||||
p.visibility = toLower(post.visibility),
|
||||
p.createdAt = post.createdAt.`$date`,
|
||||
p.updatedAt = post.updatedAt.`$date`,
|
||||
p.deleted = post.deleted,
|
||||
p.disabled = NOT post.isEnabled
|
||||
WITH p, post
|
||||
MATCH (u:User {id: post.userId})
|
||||
MERGE (u)-[:WROTE]->(p)
|
||||
WITH p, post, post.categoryIds as categoryIds
|
||||
UNWIND categoryIds AS categoryId
|
||||
MATCH (c:Category {id: categoryId})
|
||||
MERGE (p)-[:CATEGORIZED]->(c)
|
||||
WITH p, post.tags AS tags
|
||||
UNWIND tags AS tag
|
||||
MERGE (t:Tag {id: apoc.create.uuid(), name: tag})
|
||||
MERGE (p)-[:TAGGED]->(t)
|
||||
;
|
||||
4
backend/db-migration-worker/migration/neo4j/follows.cql
Normal file
@ -0,0 +1,4 @@
|
||||
CALL apoc.load.json('file:/mongo-export/follows.json') YIELD value as follow
|
||||
MATCH (u1:User {id: follow.userId}), (u2:User {id: follow.foreignId})
|
||||
MERGE (u1)-[:FOLLOWS]->(u2)
|
||||
;
|
||||
9
backend/db-migration-worker/migration/neo4j/import.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
echo "MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n,r;" | cypher-shell -a $NEO4J_URI
|
||||
for collection in "badges" "categories" "users" "follows" "contributions" "shouts" "comments"
|
||||
do
|
||||
echo "Import ${collection}..." && cypher-shell -a $NEO4J_URI < $SCRIPT_DIRECTORY/$collection.cql
|
||||
done
|
||||
4
backend/db-migration-worker/migration/neo4j/shouts.cql
Normal file
@ -0,0 +1,4 @@
|
||||
CALL apoc.load.json('file:/mongo-export/shouts.json') YIELD value as shout
|
||||
MATCH (u:User {id: shout.userId}), (p:Post {id: shout.foreignId})
|
||||
MERGE (u)-[:SHOUTED]->(p)
|
||||
;
|
||||
20
backend/db-migration-worker/migration/neo4j/users.cql
Normal file
@ -0,0 +1,20 @@
|
||||
CALL apoc.load.json('file:/mongo-export/users.json') YIELD value as user
|
||||
MERGE(u:User {id: user._id["$oid"]})
|
||||
ON CREATE SET
|
||||
u.name = user.name,
|
||||
u.slug = user.slug,
|
||||
u.email = user.email,
|
||||
u.password = user.password,
|
||||
u.avatar = user.avatar,
|
||||
u.coverImg = user.coverImg,
|
||||
u.wasInvited = user.wasInvited,
|
||||
u.role = toLower(user.role),
|
||||
u.createdAt = user.createdAt.`$date`,
|
||||
u.updatedAt = user.updatedAt.`$date`,
|
||||
u.deleted = user.deletedAt IS NOT NULL,
|
||||
u.disabled = false
|
||||
WITH u, user, user.badgeIds AS badgeIds
|
||||
UNWIND badgeIds AS badgeId
|
||||
MATCH (b:Badge {id: badgeId})
|
||||
MERGE (b)-[:REWARDED]->(u)
|
||||
;
|
||||
12
backend/db-migration-worker/sync_uploads.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
for var in "SSH_USERNAME" "SSH_HOST" "UPLOADS_DIRECTORY"
|
||||
do
|
||||
if [[ -z "${!var}" ]]; then
|
||||
echo "${var} is undefined"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
rsync --archive --update --verbose ${SSH_USERNAME}@${SSH_HOST}:${UPLOADS_DIRECTORY}/* /uploads/
|
||||
18
backend/docker-compose.cypress.yml
Normal file
@ -0,0 +1,18 @@
|
||||
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
|
||||
36
backend/docker-compose.db-migration.yml
Normal file
@ -0,0 +1,36 @@
|
||||
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:
|
||||
23
backend/docker-compose.override.yml
Normal file
@ -0,0 +1,23 @@
|
||||
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:
|
||||
9
backend/docker-compose.prod.yml
Normal file
@ -0,0 +1,9 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
neo4j:
|
||||
environment:
|
||||
- NEO4J_PASSWORD=letmein
|
||||
backend:
|
||||
environment:
|
||||
- NEO4J_PASSWORD=letmein
|
||||
14
backend/docker-compose.travis.yml
Normal file
@ -0,0 +1,14 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
neo4j:
|
||||
environment:
|
||||
- NEO4J_AUTH=none
|
||||
ports:
|
||||
- 7687:7687
|
||||
- 7474:7474
|
||||
backend:
|
||||
image: humanconnection/nitro-backend:builder
|
||||
build:
|
||||
context: .
|
||||
target: builder
|
||||
34
backend/docker-compose.yml
Normal file
@ -0,0 +1,34 @@
|
||||
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
|
||||
BIN
backend/graphql-playground.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
backend/humanconnection.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
3
backend/neo4j/Dockerfile
Normal file
@ -0,0 +1,3 @@
|
||||
FROM neo4j:3.5.0
|
||||
RUN wget https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.5.0.1/apoc-3.5.0.1-all.jar -P plugins/
|
||||
COPY migrate.sh /usr/local/bin/migrate
|
||||
18
backend/neo4j/migrate.sh
Executable file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# If the user has the password `neo4j` this is a strong indicator, that we are
|
||||
# the initial default user. Before we can create constraints, we have to change
|
||||
# the default password. This is a security feature of neo4j.
|
||||
if echo ":exit" | cypher-shell --password neo4j 2> /dev/null ; then
|
||||
echo "CALL dbms.security.changePassword('${NEO4J_PASSWORD}');" | cypher-shell --password neo4j
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
echo '
|
||||
CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]);
|
||||
CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE;
|
||||
CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE;
|
||||
CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE;
|
||||
CREATE CONSTRAINT ON (o:Organization) ASSERT o.slug IS UNIQUE;
|
||||
' | cypher-shell
|
||||
103
backend/package.json
Normal file
@ -0,0 +1,103 @@
|
||||
{
|
||||
"name": "human-connection-backend",
|
||||
"version": "0.0.1",
|
||||
"description": "GraphQL Backend for Human Connection",
|
||||
"main": "src/index.js",
|
||||
"config": {
|
||||
"no_auth": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 PERMISSIONS=disabled"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src/ -d dist/ --copy-files",
|
||||
"start": "node dist/",
|
||||
"dev": "nodemon --exec babel-node src/ -e js,graphql",
|
||||
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,graphql",
|
||||
"lint": "eslint src --config .eslintrc.js",
|
||||
"test": "nyc --reporter=text-lcov yarn test:jest",
|
||||
"test:cypress": "run-p --race test:before:*",
|
||||
"test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 babel-node src/ 2> /dev/null",
|
||||
"test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 PERMISSIONS=disabled babel-node src/ 2> /dev/null",
|
||||
"test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand",
|
||||
"test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/",
|
||||
"test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand",
|
||||
"test:jest": "run-p --race test:before:* 'test:jest:cmd {@}' --",
|
||||
"test:cucumber": "run-p --race test:before:* 'test:cucumber:cmd {@}' --",
|
||||
"test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --",
|
||||
"test:coverage": "nyc report --reporter=text-lcov > coverage.lcov",
|
||||
"db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js",
|
||||
"db:reset": "babel-node src/seed/reset-db.js",
|
||||
"db:seed": "$npm_package_config_no_auth run-p --race dev db:script:seed"
|
||||
},
|
||||
"author": "Human Connection gGmbH",
|
||||
"license": "MIT",
|
||||
"jest": {
|
||||
"verbose": true,
|
||||
"testMatch": [
|
||||
"**/src/**/?(*.)+(spec|test).js?(x)"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"activitystrea.ms": "~2.1.3",
|
||||
"apollo-cache-inmemory": "~1.5.1",
|
||||
"apollo-client": "~2.5.1",
|
||||
"apollo-link-context": "~1.0.14",
|
||||
"apollo-link-http": "~1.5.13",
|
||||
"apollo-server": "~2.4.8",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"cheerio": "~1.0.0-rc.2",
|
||||
"cors": "~2.8.5",
|
||||
"cross-env": "~5.2.0",
|
||||
"date-fns": "2.0.0-alpha.27",
|
||||
"dotenv": "~7.0.0",
|
||||
"express": "~4.16.4",
|
||||
"faker": "~4.1.0",
|
||||
"graphql": "~14.1.1",
|
||||
"graphql-custom-directives": "~0.2.14",
|
||||
"graphql-iso-date": "~3.6.1",
|
||||
"graphql-middleware": "~3.0.2",
|
||||
"graphql-shield": "~5.3.0",
|
||||
"graphql-tag": "~2.10.1",
|
||||
"graphql-yoga": "~1.17.4",
|
||||
"helmet": "~3.15.1",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"linkifyjs": "~2.1.8",
|
||||
"lodash": "~4.17.11",
|
||||
"ms": "~2.1.1",
|
||||
"neo4j-driver": "~1.7.3",
|
||||
"neo4j-graphql-js": "~2.4.2",
|
||||
"node-fetch": "~2.3.0",
|
||||
"npm-run-all": "~4.1.5",
|
||||
"request": "~2.88.0",
|
||||
"sanitize-html": "~1.20.0",
|
||||
"slug": "~1.0.0",
|
||||
"trunc-html": "~1.1.2",
|
||||
"uuid": "~3.3.2",
|
||||
"wait-on": "~3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "~7.2.3",
|
||||
"@babel/core": "~7.3.4",
|
||||
"@babel/node": "~7.2.2",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
||||
"@babel/preset-env": "~7.3.4",
|
||||
"@babel/register": "~7.0.0",
|
||||
"apollo-server-testing": "~2.4.8",
|
||||
"babel-core": "~7.0.0-0",
|
||||
"babel-eslint": "~10.0.1",
|
||||
"babel-jest": "~24.5.0",
|
||||
"chai": "~4.2.0",
|
||||
"cucumber": "~5.1.0",
|
||||
"debug": "~4.1.1",
|
||||
"eslint": "~5.15.1",
|
||||
"eslint-config-standard": "~12.0.0",
|
||||
"eslint-plugin-import": "~2.16.0",
|
||||
"eslint-plugin-jest": "~22.3.2",
|
||||
"eslint-plugin-node": "~8.0.1",
|
||||
"eslint-plugin-promise": "~4.0.1",
|
||||
"eslint-plugin-standard": "~4.0.0",
|
||||
"graphql-request": "~1.8.2",
|
||||
"jest": "~24.5.0",
|
||||
"nodemon": "~1.18.10",
|
||||
"nyc": "~13.3.0",
|
||||
"supertest": "~4.0.0"
|
||||
}
|
||||
}
|
||||
0
backend/public/.gitkeep
Normal file
1
backend/public/img/badges/fundraisingbox_de_airship.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M126.284 211.948c102.42-35.097 185.118-48.813 243.947 3.76-87.197-24.763-166.785-19.12-243.947-3.76zm56.557 37.817c83.2 8.086 125.85 7.018 185.85-18.187-78.351 41.912-114.88 34.814-185.85 18.187zm102.729 20.872c0-1.67 1.563-3.027 3.492-3.027 1.927 0 3.493 1.357 3.493 3.027 0 1.67-1.566 3.026-3.493 3.026-1.93 0-3.492-1.355-3.492-3.026zm-12.265.16c0-1.67 1.562-3.027 3.49-3.027 1.93 0 3.493 1.356 3.493 3.026 0 1.671-1.564 3.026-3.493 3.026-1.928 0-3.49-1.355-3.49-3.026zm-12.426.158c0-1.671 1.567-3.026 3.492-3.026 1.93 0 3.493 1.355 3.493 3.026 0 1.672-1.563 3.026-3.493 3.026-1.925 0-3.492-1.354-3.492-3.026zm59.115-4.762l-4.274 14.591-55.034-.141 44.12-5.849 15.188-8.601zm-186.512-44.348v-5.807c13.51-1.104 33.205-1.49 52.186 2.728l-52.186 3.08zm248.469-4.061c0-26.937-46.004-48.784-112.812-48.784-29.018 0-62.094 4.123-90.233 10.995l-46.092-5.762 3.41 20.454C122.504 201.562 114 209.429 114 217.784c0 8.419 8.628 16.34 22.526 23.251l-3.47 20.823 47.352-5.918a373.092 373.092 0 0 0 24.043 4.893v6.96h20.215l1.573-3.841a400.647 400.647 0 0 0 22.102 1.965l4.608 11.551.326 6.43h66.102l7.206-20.662h-14.52c42.795-7.105 69.888-24.776 69.888-45.452z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M0 .056h291.913v103.538H0z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><path d="M247.532 256.288c26.553 0 49.62 4.608 61.301 11.379-10-8.244-33.676-14.039-61.3-14.039-27.625 0-51.301 5.795-61.302 14.039 11.684-6.77 34.75-11.379 61.301-11.379" fill="#E5ECD6"/><path d="M248.467 272.148c28.174 0 52.65 6.146 65.046 15.173-10.611-10.993-35.735-18.718-65.046-18.718-29.31 0-54.433 7.725-65.044 18.718 12.395-9.027 36.872-15.173 65.044-15.173" fill="#B5CB88"/><path d="M248.467 291.044c30.201 0 56.439 6.45 69.725 15.93-11.372-11.54-38.304-19.653-69.725-19.653-31.419 0-58.35 8.113-69.723 19.653 13.286-9.48 39.524-15.93 69.723-15.93" fill="#E5ECD6"/><path d="M248.424 312.164c33.088 0 61.833 6.994 76.39 17.272-12.46-12.512-41.964-21.31-76.39-21.31-34.423 0-63.929 8.798-76.39 21.31 14.558-10.278 43.303-17.272 76.39-17.272" fill="#B5CB88"/><g transform="translate(102 146)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M145.956 28.306c-56.672 0-105.907 7.595-130.842 18.756C36.458 33.475 86.994 23.92 145.956 23.92c58.963 0 109.5 9.556 130.843 23.143-24.937-11.16-74.17-18.756-130.843-18.756m69.48 26.622c-4.413 2.168-9.05 1.749-10.362-.937-1.308-2.684 1.205-6.62 5.617-8.787 4.412-2.166 9.05-1.745 10.36.94 1.31 2.683-1.204 6.618-5.616 8.784m-69.479 36.038c-33.854 0-61.301-7.914-61.301-17.676 0-9.762 27.447-17.678 61.301-17.678 33.855 0 61.302 7.916 61.302 17.678s-27.447 17.676-61.302 17.676m54.659 2.678c-.498 1.453-2.839 1.968-5.228 1.145-2.39-.822-3.925-2.668-3.43-4.122.498-1.455 2.839-1.966 5.23-1.145 2.389.821 3.924 2.665 3.428 4.122m-100.66 0c-.497 1.453-2.838 1.968-5.227 1.145-2.39-.822-3.926-2.668-3.43-4.122.5-1.455 2.837-1.966 5.228-1.145 2.39.821 3.927 2.665 3.43 4.122M77.101 55.188c-4.647-1.597-7.631-5.186-6.667-8.015.967-2.828 5.517-3.826 10.163-2.228 4.648 1.597 7.632 5.187 6.666 8.014-.966 2.828-5.515 3.828-10.162 2.23m148.591-28.937C214.793 11.05 183.223.056 145.956.056 108.689.056 77.12 11.05 66.22 26.252 26.35 33.765 0 46.746 0 61.505c0 23.244 65.347 42.089 145.956 42.089 80.61 0 145.957-18.845 145.957-42.09 0-14.758-26.351-27.739-66.22-35.252" fill="#FEFEFE" mask="url(#d)"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
backend/public/img/badges/fundraisingbox_de_balloon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="512" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255-33L476 95v256L255 479 34 351V95z"/><path id="c" d="M0 0h132.6v218.216H0z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255 223)" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(181.645 120)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M11.221 66.902C6.636 33.096 34.24 9.57 62.304 9.57c.985 0 1.966.033 2.945.091-26.822 5.83-50.335 27.206-54.028 57.242M67.378.001C30.569 0-5.633 32.784.733 79.817c3.475 25.675 37.881 81.413 65.736 83.034a.02.02 0 0 1-.004.01c-.257.111-.492.482-.524.857-.001-.006-.005-.01-.005-.017a1.132 1.132 0 0 0 .003.205.84.84 0 0 0 .04.173c-.582 1.261-1.33 2.487-1.804 4.159-.67 2.366 3.474 5.2 7.465 2.42 2.523-1.76-1.758-3.08-1.977-4.84a9.117 9.117 0 0 0-.116-.674c3.67 2.972 10.917 10.466 7.945 21.534-1.91 7.111-5.973 11.194-9.56 14.794-4.17 4.19-8.109 8.146-6.837 15.37a1.662 1.662 0 0 0 1.924 1.349 1.664 1.664 0 0 0 1.346-1.927c-.972-5.52 1.918-8.423 5.919-12.442 3.687-3.702 8.275-8.311 10.415-16.28 3.49-12.99-5.024-21.714-9.236-25.105 27.033-5.322 57.021-57.103 60.365-81.804C138.19 33.6 104.183.001 67.378.001" fill="#FEFEFE" mask="url(#d)"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M230.035 250.385c.86.402 1.793.598 2.712.593a6.087 6.087 0 0 0 2.66-.63c1.828-.903 3.288-2.82 3.642-5.787-.02-.147-.024-.306-.039-.458h-12.88c-.013.143-.01.303-.026.445.408 2.984 1.992 4.939 3.931 5.837M237.234 99h-12.187c-23.28 13.57-56.759 56.364.884 141.217h11.366c-30.487-66-23.202-120.912-.063-141.217zM259.063 99h12.187c23.28 13.57 56.891 56.364-.751 141.217h-12.167c30.487-66 23.87-120.912.73-141.217zM273.348 244.102c-.01.12-.007.25-.022.363.407 2.987 1.993 4.944 3.93 5.842.863.402 1.794.601 2.713.594a6.115 6.115 0 0 0 2.662-.628 5.906 5.906 0 0 0 1.59-1.179c.157-.176.314-.36.472-.538.85-1.094 1.448-2.571 1.607-4.454h-12.952zM223.388 244.34l-.02-.237h-12.902c.04.311.1.603.165.89a728.456 728.456 0 0 0 4.557 5.28c.63.199 1.277.333 1.924.329a6.091 6.091 0 0 0 2.662-.628c1.795-.887 3.228-2.76 3.614-5.633M245.652 250.096a6.352 6.352 0 0 0 2.711.593 6.138 6.138 0 0 0 2.666-.63c1.859-.92 3.337-2.896 3.654-5.957h-12.897c-.009.129-.013.261-.022.387.455 2.85 2.003 4.73 3.888 5.607M284.012 96.298c-10.502-4.528-22.225-7.073-34.6-7.073-12.373 0-24.095 2.545-34.597 7.073h69.197z"/><path d="M220.209 240.285c-70.207-81.191-29.736-127.722-2.779-141.217h-9.395C184.08 112.061 168 135.971 168 163.328c0 25.886 19.326 53.366 39.087 76.957h13.122zM277 240.285c70.207-81.191 29.736-127.722 2.778-141.217h9.395c23.957 12.993 40.036 36.903 40.036 64.26 0 25.886-19.326 53.366-39.087 76.957H277zM237.045 278.261l2.188-15.376h2.898v4.38h14.439v-4.38h3.45l2.183 15.376h-25.158zm-.603 27.055l2.329-20.592h3.614l2.059 16.025 14.905 5.014-22.907-.447zm43.804-51.73c-.09.002-.177.016-.267.016a9.026 9.026 0 0 1-3.852-.839c-1.689-.784-3.168-2.126-4.188-3.978-.942 1.62-2.257 2.798-3.746 3.535a8.84 8.84 0 0 1-3.846.903 9.036 9.036 0 0 1-3.853-.835c-1.71-.795-3.205-2.163-4.225-4.051-.95 1.929-2.381 3.323-4.049 4.146a8.76 8.76 0 0 1-3.843.906 9.04 9.04 0 0 1-3.857-.838c-1.587-.739-2.997-1.963-4.008-3.646-.95 1.785-2.326 3.083-3.91 3.867a8.782 8.782 0 0 1-3.845.905 9.006 9.006 0 0 1-3.855-.836c-1.687-.785-3.166-2.13-4.186-3.98-.94 1.62-2.256 2.8-3.747 3.536a8.74 8.74 0 0 1-3.173.862c2.917 3.326 5.77 6.544 8.484 9.626h10.363l-2.185 15.376h-4.61v6.463h3.426l-3.516 21.696h.03l3.911 4.67h31.626l3.913-4.67h.008v-.009l.01-.012h-.015l-3.508-21.675h2.891v-6.463h-3.833l-2.186-15.376h9.33c2.664-2.983 5.457-6.092 8.311-9.3zM248.011 99h5.029c16.71 35.173 10.995 57.883 1.544 141.217h-12.886c-10.733-70.609-14.48-113.26 1.544-141.217h4.769z"/><path d="M261.625 249.933a6.384 6.384 0 0 0 2.71.592 6.17 6.17 0 0 0 2.664-.628c1.793-.888 3.225-2.76 3.61-5.636l-.013-.159h-4.205c-.011.025-.02.05-.032.075l-.172-.075h-8.494c.412 2.978 1.995 4.93 3.932 5.831"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
1
backend/public/img/badges/fundraisingbox_de_crane.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M126 146l94.405 71.42-38.172-25.45-27.501-3.282 95.226 49.254-28.733 91.941 37.763-51.306-17.24 4.515 87.017-41.867 8.209-41.045 2.463 10.67 30.373-3.282-29.553-20.111-40.225 15.186 18.881 26.269-64.852-66.494z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 637 B |
1
backend/public/img/badges/fundraisingbox_de_glider.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M0 0h218.936v174.69H0z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(124 138)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M104.134 27.903L5.11 0l-.592.604 97.681 29.448 87.491 33.51L4.473.651 0 5.219l100.81 83.795-39.555 15.02 46.152-9.598L218.08 70.762l.02.055-72.984 20.65-6.912 36.332 17.764 10.131c.265-6.47-6.641-9.97-5.288-17.874a31.598 31.598 0 0 0 1.546-2.977l.12.018a8.186 8.186 0 0 0 2.182-16.225 8.147 8.147 0 0 0-6.094 1.646 8.12 8.12 0 0 1 6.565-2.006c.27.035.532.087.794.139l-1.385-11.538 2.93-.353 1.561 12.991c6.621 3.448 8.733 12.552 1.896 15.793l3.072 25.581.35 2.918-2.55-1.456-7.14-4.072-.178.06c-.01-.006-.019-.015-.029-.021l.173-.059-18.673-10.648-.916-.524.197-1.033 6.836-35.923-31.345 8.87 10.75 16.354c-6.477 5.957-3.536 3.9-10.95 9.05l-15.685 4.519c-1.989 5.116-9.56 10.71 3.271 13.775 14.892-5.856 18.646-6.024 32.718-13.112l27.452 41.764 12.621 1.104 21.208-35.327 26.96-68.784-114.803-42.677z" fill="#FEFEFE" mask="url(#d)"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M235.643 286.994c-7.337-3.551-12.277-8.112-15.098-13.57l-116.648-18.373-4.258-23.658 13.038 4.518 10.197 14.959-4.824 2.328 99.665 9.536c-.513-9.03 2.799-19.675 9.271-31.656-1.464 20.596 1.8 37.705 15.463 47.156 23.486 6.045 48.729 7.994 74.61 8.465-28.956 4.908-55.646 3.822-81.416.295zm69.532-27.415l-9.773-8.359-33.12-.45-10.792-3.35 2.282-14.323 57.564 4.523c-14.163-9.303-33.081-17.106-50.387-19.464v-5.573l.071-.34 147.07-6.467-19.661-7.346-126.49 9.388 2.19-10.52h-15.855l1.777 10.453-125.741-9.33-19.66 7.344 146.156 6.427.067.391v4.842c-.049 0-.098-.004-.145-.004-29.507 0-40.107 20.221-42.641 37.652l-79.63-4.934-9.978-19.48-29.413-6.494 4.3 32.913-4.3.502v9.3l42.598 6.01 86.112 18.034h10.441a78.766 78.766 0 0 0 5.452 2.24l1.33 8.006c-2.342 1.298-3.945 3.766-3.945 6.631a7.612 7.612 0 0 0 15.222 0c0-3.778-2.76-6.89-6.366-7.486l-.9-5.403c17.834 5.241 39.833 5.613 67.932 2.156l-.573 3.444c-3.433.736-6.01 3.781-6.01 7.435a7.611 7.611 0 1 0 15.22 0c0-2.995-1.747-5.563-4.265-6.803l.841-5.07 5.734-5.15h5.136c6.532-7.727 10.336-8.41 10.336-26.918 0-1.424-.303-2.907-.843-4.427h-27.343zM223.91 109.559c4.673-.533 9.103.431 13.206 2.575 1.986 1.039 2.466.165 2.88-1.414 2.241-3.537 4.34-7.236 8.198-9.306 11.677-6.263 26.247.19 30.135 13.27 1.77 5.957 1.12 11.827-.688 17.605-1.005 3.22-2.371 6.327-3.573 9.483-4.357 9.065-9.837 17.545-13.952 26.727-.854 1.906-1.6 1.957-3.322 1.064-2.773-1.438-5.53-2.964-8.534-3.923-4.288-1.848-8.602-3.644-12.86-5.56-6.696-3.015-13.344-6.187-18.823-11.203-7.401-6.774-10.928-14.974-7.87-25 2.305-7.555 7.88-11.94 15.204-14.318"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
1
backend/public/img/badges/fundraisingbox_de_starter.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="512" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255-33L476 95v256L255 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M128.748 228.578l61.936 31.273 188.772-121.85zM215.634 274.035l-14.938 27.141 24.659-22.233zM217.273 271.672l70.296 35.496L382.634 138z"/><path d="M217.495 267.595L358.438 153.66 190.882 264.248l5.072 37.632 16.98-30.852 4.563-3.434z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 654 B |
1
backend/public/img/badges/indiegogo_en_bear.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M0 0h280.118v187.763H0z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(108 134)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M163.39 149.606c-15.27-3.601-29.887-9.802-42.72-18.873 27.116 9.276 59.092 11.735 86.94 4.313 5.732 9.119 12.64 13.725 19.001 17.29 3.803 2.266 5.487 3.529 6.159 6.883l.958 4.714c.827 4.517-.43 6.509-3.67 8.897l-4.463 3.087c-5.162 3.798-5.112 11.852 2.063 11.846h21.003c7.333-.043 15.421-8.131 17.356-16.9l13.312-76.946c4.428-27.484-10.356-51.243-29.436-68.604C229.875 7.108 203.244-5.11 177.288 2.105 133.059 14.76 125.194 3.303 106.21 3.8c-12.501.33-18.655 3.835-29.956 8.434-9.583 3.902-19.977 6.67-29.195 11.133 4.37-5.153 9.852-9.154 15.82-12.316-3.005-4.873-11.102-6.727-14.773-3.955-4.665 3.508-8.828 13.264-7.669 19.958-3.305 2.128-6.357 4.614-9.042 7.629-6.095 6.848-8.07 13.003-8.703 16.488-.556 2.559-.892 3.309-2.648 4.437L2.633 66.225c-2.825 1.8-3.648 5.47-1.12 9.4 7.115 11.115 18.142 9.597 23.066 8.214 2.105-.551 2.823-.201 3.986 1.183 4.264 4.786 14.15 8.82 28.89 6.746 4.917-.653 8.087 2.192 7.681 7.185L60.73 154.9c-1.517 10.845-3.575 14.505-11.227 16.699l-15.68 4.317c-6.09 2.032-8.685 11.852-.744 11.846h34.72c6.472-.155 11.16-1.741 15.407-6.341 16.097-17.618 20.644-41.42 26.332-53.78 14.2 12.144 29.457 23.05 33.438 32.4 1.971 4.63.537 9.656-4.626 12.454-4.123 2.115-4.885 10.306 2.794 9.585l18.273-.01c5.423-.116 7.822-4.267 7.524-11.521-.305-6.631-1.65-13.762-3.55-20.944" fill="#FEFEFE" mask="url(#d)"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
backend/public/img/badges/indiegogo_en_panda.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M194.65 255.66c4.047 4.982 9.571 3.188 15.086-1.194 8.055-6.402 16.476-17.269 19.764-26.627 2.495-7.093.732-16.373-5.516-20.253-6.478-4.02-13.564.416-16.274 5.952-4.439 9.062-6.906 12.694-13.617 18.74-7.817 6.823-6.24 14.631.557 23.381zm107.461 0c6.797-8.75 8.374-16.559.558-23.382-6.712-6.046-9.178-9.678-13.614-18.74-2.713-5.536-9.796-9.972-16.277-5.952-6.248 3.88-8.011 13.16-5.517 20.253 3.288 9.358 11.709 20.225 19.765 26.627 5.515 4.382 11.041 6.176 15.085 1.193zm-27.558-26.665c4.408 4.596 13.593 2.09 12.798-5.04-.078-.677-.316-1.14-.681-1.583-5.136-6.53-10.59-4.72-12.941 2.797-.51 1.73.058 3.004.824 3.826zm-52.345 0c.766-.822 1.338-2.097.828-3.822-2.354-7.522-7.808-9.332-12.944-2.8-.366.442-.604.905-.682 1.581-.795 7.13 8.393 9.637 12.798 5.041zm-46.864-42.102a134.897 134.897 0 0 1 11.14-16.112c5.049-6.26 10.538-11.57 16.521-15.894l-3.069-3.96c-12.943-16.678-35.918 14.46-24.592 35.966zm118.413-32.006c5.982 4.323 11.472 9.633 16.523 15.894a135.717 135.717 0 0 1 11.14 16.112c11.326-21.507-11.652-52.644-24.595-35.966l-3.068 3.96zm-12.793 148.086c-9.533 6.024-22.681 7.08-32.584-.319-9.899 7.4-23.05 6.343-32.584.32 11.038 26.404 54.131 26.404 65.168 0zm-77.322-162.669c-17.905-23.078-50.233 22.52-30.874 51.153-6.196 11.49-10.812 24.013-13.066 36.701-1.673 4.842-5.068 10.247-8.702 12.538 0 0 3.541 1.435 7.232 1.614.023 3.57.214 6.986.558 10.262-.592 3.486-1.754 6.857-3.452 9.046 0 0 2.194.077 4.793-.356 1.887 9.088 5.145 16.903 9.583 23.556 1.129 3.366 1.695 6.9 1.225 9.64 0 0 2.069-1.012 4.214-2.693.309.336.619.67.936 1.001 2.25 4.433 4.814 11.088 2.906 15.704 0 0 6.777-1.493 9.759-5.2 1.319 2.512 2.001 5.115 1.247 7.41 0 0 2.882-1.73 5.193-4.288 5.679 2.733 11.894 4.871 18.563 6.465-1.526-3.76-2.325-8.128-2.168-13.093-4.717-4.3-7.891-10.057-8.221-16.612-.381-7.577 1.707-13.788 5.644-19.447-.755 2.826-1.166 5.664-1.121 8.508.147 11.827 6.973 20.626 15.905 24.124 8.105 3.18 17.946 1.991 23.146-5.26-.971-.228-1.944-.68-2.916-1.363-5.113-3.562-11.209-5.599-14.235-9.004-2.945-3.307-2.691-8.974 2.608-9.427 3.835-.332 9.649 1.588 15.981 1.588 6.334 0 12.144-1.92 15.984-1.588 5.298.453 5.548 6.12 2.606 9.427-3.029 3.405-9.124 5.442-14.235 9.004-.972.682-1.945 1.135-2.916 1.362 5.201 7.252 15.041 8.44 23.147 5.261 8.93-3.498 15.757-12.297 15.908-24.124.044-2.909-.388-5.813-1.179-8.704 3.995 5.714 6.083 11.978 5.698 19.643-.328 6.555-3.5 12.311-8.219 16.612.159 4.965-.643 9.33-2.17 13.09 6.669-1.594 12.884-3.729 18.564-6.462 2.31 2.558 5.196 4.287 5.196 4.287-.757-2.294-.072-4.897 1.243-7.407 2.986 3.705 9.759 5.198 9.759 5.198-1.907-4.616.657-11.27 2.907-15.704.317-.327.626-.662.936-1 2.141 1.68 4.213 2.692 4.213 2.692-.47-2.74.097-6.274 1.224-9.64 4.443-6.648 7.7-14.468 9.584-23.556 2.599.436 4.793.356 4.793.356-1.698-2.189-2.86-5.555-3.452-9.044.345-3.276.536-6.692.561-10.264 3.688-.179 7.229-1.612 7.229-1.612-3.634-2.293-7.029-7.698-8.702-12.54-2.254-12.688-6.87-25.21-13.065-36.7 19.358-28.634-12.97-74.232-30.875-51.154l-7.396 9.534c-10.869-5.877-23.226-8.89-37.343-8.89-14.113 0-26.473 3.013-37.341 8.89l-7.397-9.534z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
1
backend/public/img/badges/indiegogo_en_rabbit.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M0 0h170.423v190.223H0z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(163 133)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M166.489 131.825c-1.473-1.058-3.205-1.59-5.055-1.64-.457 6.738-2.37 13.122-5.414 19.051.17-3.725.102-7.574.139-11.43.365-38.024-25.043-68.757-55.531-70.73-15.427-.998-24.246-2.62-32.522-17.258-4.209-7.476-8.694-13.01-14.506-15.995 5.49.17 9.962 1.896 13.843 4.935 7.902-9.387 12.119-22.148 11.271-34.633-.266-3.669-2.423-4.599-5.458-3.92C55.139 4.406 48.259 17.56 45.958 31.511c-2.137-.257-4.432-.262-6.922.011-18.57 2.036-29.837 12.815-38.007 30.464-6.125 13.231 16.172 29.308 38.519 26.05-4.534 3.588-11.775 5.888-19.235 6.687 3.513 34.395 5.22 55.208 23.367 73.087 2.47 2.45 1.558 5.335-1.846 5.937-7.459 1.338-16.863 4.724-16.792 13.26.079 2.416 1.348 3.198 3.909 3.216h30.665c6.359.03 9.516-1.617 12.461-6.109 5.481-9.447 7.894-13.456 21.287-15.267 4.308-.58 5.463-3.02 4.227-7.052-10.239-33.426 12.827-44.218 29.58-42.314-14.528 2.398-33.529 13.087-21.94 46.98 1.143 3.323.858 6.253-3.664 6.865-7.692.985-19.784 4.091-19.534 13.88.126 2.27 1.351 2.985 3.49 3.017h30.7c7-.122 9.536-2.118 12.4-6.023 5.81-8.472 13.706-8.113 19.675-13.87 3.003-2.897 4.851-6.236 5.996-9.864 4.231-.685 8.758-3.59 12.054-8.185 5.377-7.5 5.442-16.658.141-20.456" fill="#FEFEFE" mask="url(#d)"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
backend/public/img/badges/indiegogo_en_racoon.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1
backend/public/img/badges/indiegogo_en_rhino.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M96 134h304.563v187.156H96z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M233.572 271.01c-6.76-4.6-11.25-9.185-15.986-13.274 3.792 1.377 8.023 3.209 13.55 5.875 4.745 2.288 10.686 2.315 14.2-3.954 6.953-12.513 6.215-24.384 2.593-35.755 7.358 12.029 11.76 24.825 3.788 39.516 24.56 8.805 51.835 8.101 80.266-2.195-10.935-20.436-10.743-44.23-1.969-63.405-5.608 24.92.452 53.337 22.912 72.526 3.892 3.067 10.577 7.094 19.964 2.115 3.8-2.017 7.911-5.055 11.643-8.464-6.773 10.094-16.673 16.925-26.168 17.712.62 5.349 1.587 9.12 4.62 10.944 1.496.913 1.996 2.159 1.705 4.097l-1.14 7.26c-.613 3.047-1.516 4.663-4.944 5.458-13.168 3.534-10.77 11.695-3.248 11.69h15.154c5.555-.036 7.675-2.274 9.697-5.745l10.8-20.05c2.724-5.373 3.196-9.243 1.618-16.325l-3.724-12.733c-.969-3.3-.899-5.469 1.3-8.149 4.538-5.501 6.385-10.606 7.854-19.371 1.993-12.962 4.435-36.15.066-53.626-5.476-29.484-25.04-49.834-55.373-44.32-58.384 12.407-79.885-1.403-97.514-3.75-21.65-2.876-31.163 8.51-54.29 18.17 6.589-7.674 4.7-17.8 4.358-21.257-17.537.597-19.418 11.694-22.262 24.364-7.197-.895-12.441 2.044-14.888 10.181-6.707 19.633-21.913 21.532-29.577 20.886-1.769-.16-3.044-.723-4.335-2.073-3.484-3.843-5-9.245-4.752-17.793.144-2.482-2.28-2.809-4.11-1.32-4.522 3.993-9.938 13.102-11.465 27.708-.33 2.956-.517 3.703-2.258 6.166-3.195 4.77-5.135 10.542-5.635 16.787-.1 1.517.113 2.431 1.218 3.599 3.305 3.358 7.055 4.495 11.96 4.276 2.502-.089 3.088.005 4.891 1.84 12.791 13.16 20.004 6.143 42.201 8.222 22.875 2.137 39.078-5.802 46.26-22.102-1.034 12.313-6.938 22.012-17.133 28.431 2.766 4.267 6.674 7.578 13.054 9.51 1.803.57 2.582 1.412 2.535 3.635-.1 11.664 1.936 19.135 3.291 26.036 1.909 10.214 4.25 24.27-7.288 27.113-13.167 3.534-10.773 11.695-3.246 11.69h15.153c5.552-.094 9.04-1.625 9.691-5.745 1.826-11.108 3.626-14.96 7.916-24.43 2.928-6.469 5.201-12.769 7.047-19.97" fill="#FEFEFE" mask="url(#d)"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
1
backend/public/img/badges/indiegogo_en_tiger.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M149 133h197.237v189.77H149z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M271.983 301.966c-8.364.88-17.497-1.828-24.363-9.108-6.87 7.276-16.007 9.988-24.373 9.113-9.277 7.49-8.469 20.8 4.571 20.8h39.6c13.044 0 13.847-13.317 4.565-20.805m-5.28-75.342c-.039 1.26 1.02 1.754 2.108 1.212 8.188-4.17 15.895-2.294 20.334-18.03.253-.933-.016-1.463-1.016-1.665-4.606-.938-11.352-2.32-15.98 1.017-4.78 3.45-5.047 11.206-5.446 17.466m-38.173 0c-.399-6.26-.663-14.016-5.447-17.466-4.624-3.337-11.37-1.955-15.975-1.017-1 .202-1.27.732-1.018 1.665 4.44 15.736 12.148 13.86 20.334 18.03 1.09.542 2.15.048 2.106-1.212m19.087-52.888c10.23 0 20.464 1.781 30.69 5.34 2.265.695 3.222-1.32 1.772-2.685-9.789-8.417-21.04-12.329-32.462-12.26-11.42-.069-22.672 3.843-32.46 12.26-1.449 1.365-.494 3.38 1.771 2.685 10.227-3.559 20.458-5.34 30.69-5.34m-25.682 22.118c14.401-3.159 36.964-3.159 51.363 0 2.24.462 2.76-1.326 1.487-2.249-14.989-10.87-39.347-10.87-54.335 0-1.274.923-.754 2.711 1.485 2.25m84.907 91.212c18.226-11.169 27.342-44.08 6.566-76.568-1.112-1.646-3.009-.658-2.366 1.69 5.31 18.81 9.856 44.55-6.773 72.111-1.37 2.442.575 3.904 2.573 2.767m-118.45 0c1.997 1.137 3.945-.325 2.572-2.767-16.632-27.562-12.082-53.302-6.77-72.112.642-2.347-1.256-3.335-2.368-1.689-20.778 32.487-11.66 65.4 6.565 76.568m109.691-72.166c-1.47-1.436-3.266-.266-2.617 1.638 4.863 13.282 5.382 21.047 2.34 34.861-.702 3.111 2.083 4.15 3.734 2.05 8.518-11.839 6.568-27.957-3.457-38.549m-100.929 0c-10.029 10.592-11.98 26.71-3.459 38.548 1.652 2.101 4.436 1.062 3.731-2.049-3.038-13.814-2.519-21.579 2.344-34.86.65-1.905-1.148-3.075-2.616-1.639m10.684 99.41c-.587-4.866 2.393-10.63 8.83-13.823-7.377-2.622-13.304-8.265-15.13-16.082-3.725-15.93 2.586-27.452 9.781-36.574-3.448 8.332-6.012 17.802-4.478 28.098 2.651 17.814 23.762 22.192 38.972 4.347a5.937 5.937 0 0 1-1.229-.718l-10.733-8.355c-4.53-3.792.198-11.802 7.68-8.432 1.526.658 2.075 1.026 3.837 1.01h4.5c1.761.016 2.31-.352 3.838-1.01 7.478-3.37 12.21 4.64 7.68 8.432l-10.738 8.355a5.855 5.855 0 0 1-1.224.718c15.205 17.85 36.32 13.472 38.971-4.347 1.525-10.234-1.002-19.658-4.42-27.952 7.173 9.168 13.435 20.724 9.721 36.428-1.847 7.811-7.772 13.453-15.142 16.076 6.448 3.19 9.431 8.96 8.84 13.83 13.604-4.805 25.07-12.445 33.402-23.119 3.515 2.727 7.033 8.127 8.982 16.097 7.267-11.59 5.723-25.319 2.934-33.122 2.974 1.391 6.955 5.561 9.13 12.083 4.117-12.372 1.098-24.299-4.144-31.306 3.438.722 6.39 3.253 8.212 6.202 1.823-13.045-4.316-26.077-10.38-32.4 2.431.021 5.142 1.17 6.873 2.732-3.695-15.869-13.086-25.616-16.297-28.566a100.124 100.124 0 0 0-5.95-10.615c-3.995-5.673-3.172-11.237 1.441-15.836 18.95-19.27-6.892-57.465-26.66-38.16l-10.595 10.768c-5.095 5.18-9.29 6.324-16.369 4.498a82.25 82.25 0 0 0-20.356-2.532c-7.098 0-13.9.887-20.354 2.532-7.079 1.826-11.274.682-16.37-4.498l-10.598-10.769c-19.765-19.304-45.61 18.89-26.657 38.161 4.615 4.6 5.436 10.163 1.443 15.836a98.73 98.73 0 0 0-5.95 10.615c-3.21 2.95-12.603 12.697-16.298 28.566 1.728-1.563 4.443-2.71 6.873-2.733-6.064 6.324-12.203 19.356-10.38 32.401 1.824-2.949 4.774-5.48 8.213-6.202-5.245 7.007-8.26 18.934-4.148 31.306 2.178-6.522 6.156-10.692 9.13-12.083-2.789 7.803-4.329 21.532 2.934 33.122 1.953-7.97 5.47-13.37 8.985-16.097 8.334 10.67 19.795 18.314 33.398 23.118" fill="#FEFEFE" mask="url(#d)"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
1
backend/public/img/badges/indiegogo_en_turtle.svg
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
1
backend/public/img/badges/indiegogo_en_whale.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M188.74 245.033c-3.953 0-7.161 1.979-7.161 4.426 0 2.444 3.208 4.427 7.162 4.427 3.954 0 7.161-1.983 7.161-4.427 0-2.447-3.207-4.426-7.161-4.426m21.144 8.09c-7.708-.69-10.217 6.19-21.144 6.19-10.924 0-13.433-6.88-21.144-6.19 5.133 3.257 8.929 12.703 21.144 12.703 12.215 0 16.018-9.446 21.144-12.703m147.05-30.274c2.975-9.662 5.02-12.315 11.313-17.998 31.238-27.646 30.43-51.658 24.545-70.851-5.557 18.187-33.065 4.357-40.66 28.398-13.773-21.315-34.758.35-49.452-13.277 1.532 27.69 25.526 33.642 38.183 48.287 4.444 5.152 3.989 11.73-1.956 15.517-49.149 31.406-100.128 1.906-149.571 1.017-29.169-.523-58.305 6.71-79.76 14.678-9.511 3.825-11.391 10.684-8.035 19.454 13.923 32.119 49.884 52.038 90.795 58.184-16.337.539-32.45-1.076-47.332-4.852-5.706 5.575-12.046 11.423-19.477 17.533 32.45 7.447 57.723.387 78.558-11.304a189.342 189.342 0 0 0 40.502-.944c32.71 10.862 73.437 22.47 117.684-7.714-51.504-16.794-68.083-38.293-121.901-33.977 30.804-9.268 54.943-2.28 83.925 7.978 14.89-12.964 26.424-29.613 32.64-50.129"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
backend/public/img/badges/indiegogo_en_wolf.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M0 0h197.01v186.749H0z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(149 135)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M49.467 17.814c-5.418-4.322-11.367-7.31-17.501-7.373-4.208-.017-5.188-.143-7.916-2.47C19.304 3.894 16.122 1.23 11.564.16 9.108-.35 8.214.324 7.7 2.604c-1.15 5.252-2.128 7.936-.598 15.377.496 2.086-1.204 2.514-3.507 1.135-3.663-2.276-5.075 3.447-1.612 7.282 3.112 3.439 7.559 6.192 8.223 15.72.26 3.761.578 6.618.995 9.014.483 2.634.568 3.038.113 5.636-3.691 19.968 4.455 21.12 9.908 33.044 4.086 8.933 8.75 17.055 14.4 22.549 2.416 2.293 3.74 3.88 5.048 7.015 4.922 11.494 9.567 28.374 8.574 50.046-.3 6.38-3.776 10.275-10.251 8.8-3.782-.86-7.311.24-7.3 4.123.006 1.734.618 4.185 3.927 4.405h14.67c3.519-.014 6.028-1.376 7.004-4.792 5.766-21.198 7.343-36.803 8.017-52.548.4-6.025 2.717-9.076 9.089-9.544 15.108-.951 32.244-3.475 46.518-6.29-2.863-9.972-4.225-20.618-2.973-33.955 4.204 27.358 11.447 31.555 28.017 55.235 8.376 11.97 7.227 30.77-4.102 41.189-.913.84-1.938 1.534-3.082 1.988-2.16.862-2.996.672-5.11.189-3.78-.858-7.31.24-7.3 4.123.008 1.734.618 4.185 3.927 4.405h14.67c3.53-.053 5.39-1.316 6.736-3.811l13.022-24.778c3.375-6.477 2.747-11.292-.021-17.974-1.057-2.462-2.313-6.117-3.357-9.33-1.78-5.84-.413-9.097 2.269-13.242 4.059-6.362 10.075-10.92 14.302-20.597-.55 6.769-3.346 12.22-6.521 17.24-3.15 5.21-3.77 8.492-2.05 14.373 3.522 12.064 7.018 22.587 8.536 26.661 3.067 7.749 7.642 9.631 10.662 12.84 1.363 1.376 2.358 1.154 3.126-.572 1.67-4.08 5.263-7.486 5.34-15.825-.115-7.849-1.454-36.989-6.296-61.132-5.165-25.71-17.168-45.135-48.56-44.38-23.8.995-50.62 5.209-69.908-6.633-7.2 7.161-16.35 8.217-25.05 2.631 9.129 2.359 17.167-1.261 22.149-9.938 1.434-2.602 1.067-5.016-1.773-6.18l-13.266-5.427c-1.73-.726-3.018-1.351-4.84-2.862" fill="#FEFEFE" mask="url(#d)"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
backend/public/img/badges/user_role_admin.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#AD245D" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><path d="M35.01 367.726c-.08-21.169-.205-53.162 21.257-71.332 3.817-3.253 9.93-7.497 17.321-9.224 2.575-.523 4.956-.756 7.262-.979 4.438-.431 8.27-.804 12.054-2.9l4.954-2.846c9.87-5.655 19.194-10.996 28.226-17.377 5.085-3.632 6.726-15.73 6.095-25.428-.214-2.792-1.893-5.7-3.67-8.777-1.097-1.901-2.232-3.867-3.065-5.916l-.073-.199a56.976 56.976 0 0 1-.422-1.443c-1.195-4.205-1.933-6.378-2.386-7.476-7.029-.944-11.8-8.647-12.888-21.006l-.031-.557c-.645-12.785.808-16.13 2.316-17.716.24-.254.505-.475.783-.666-1.754-16.051 3.115-32.521 13.358-44.704 9.314-11.079 21.955-17.18 35.592-17.18 3.73 0 7.55.458 11.355 1.362 25.63 6.228 41.679 30.27 40.062 59.227.53.251 1.018.61 1.44 1.066 2.752 2.964 2.47 10.97 2.22 14.276l-.024.41c-.335 5.236-.684 10.65-3.052 15.73-1.739 3.918-4.405 6.242-6.76 8.29-2.396 2.089-4.288 3.735-5.294 6.885-.7 2.416-1.645 4.866-2.559 7.235-1.752 4.538-3.407 8.827-3.54 13.244-.427 10.222 1.17 18.391 4.172 21.359 5.097 5.163 13.003 9.391 19.978 13.121 1.6.855 3.166 1.692 4.654 2.517 9.28 5.052 16.07 7.915 25.309 8.557 9.118.849 18.056 5.193 24.754 11.97.736.641 1.82 1.744 3.694 3.648 4.416 4.492 4.416 4.492 4.426 5.852l.007.758c10.783 17.702 11.14 40.656 11.415 58.169l.05 3.28-3.278.028c-42.05.363-84.058.677-126.058.993-42.12.314-84.232.632-126.367.994l-3.273.029-.014-3.274zM329.011 135.763a5.232 5.232 0 0 0-5.223 5.23 5.232 5.232 0 0 0 5.223 5.23 5.236 5.236 0 0 0 5.231-5.23 5.236 5.236 0 0 0-5.23-5.23m0 40.237C309.705 176 294 160.297 294 140.993 294 121.698 309.706 106 329.011 106 348.303 106 364 121.698 364 140.993 364 160.297 348.303 176 329.011 176" fill="#FFF"/><path d="M330.511 101C308.173 101 290 119.164 290 141.492 290 163.828 308.173 182 330.511 182 352.836 182 371 163.828 371 141.492 371 119.164 352.836 101 330.511 101m0 51.022c5.823 0 10.531-4.716 10.531-10.53a10.517 10.517 0 0 0-10.53-10.529 10.51 10.51 0 0 0-10.523 10.529c0 5.814 4.7 10.53 10.522 10.53m0-40.496c16.563 0 29.963 13.406 29.963 29.966 0 16.555-13.4 29.982-29.963 29.982-16.555 0-29.985-13.427-29.985-29.982 0-16.56 13.43-29.966 29.985-29.966" fill="#AD245D"/><path d="M331 106.209c-20.305 0-36.825 16.06-36.825 35.799 0 19.747 16.52 35.813 36.825 35.813 20.306 0 36.827-16.066 36.827-35.813 0-19.74-16.521-35.8-36.827-35.8zM314.287 215l-4.11-21.345c-.324-.129-.648-.265-.972-.404l-18.012 12.169-23.607-23.609 12.186-18.009a63.31 63.31 0 0 1-.403-.968L258 158.712v-33.383l21.361-4.13c.131-.327.267-.652.407-.979l-12.18-18.025 23.608-23.612 18.015 12.198c.322-.137.643-.27.964-.4L314.287 69h33.416l4.13 21.387c.319.13.638.26.956.396l18.024-12.2 23.608 23.612-12.186 18.031c.139.324.273.648.402.971L404 125.33v33.381l-21.37 4.124c-.13.32-.262.64-.398.96l12.19 18.017-23.606 23.609-18.021-12.171c-.32.137-.642.27-.964.402L347.701 215h-33.414z" fill="#FFF"/><path d="M330 171.448c-17.342 0-31.45-13.656-31.45-30.44 0-16.778 14.108-30.427 31.45-30.427 17.341 0 31.449 13.649 31.449 30.426 0 16.785-14.108 30.441-31.45 30.441zM350.979 63h-41.97l-1.64 8.517-1.953 10.156-8.55-5.788-7.18-4.862-6.132 6.132-17.399 17.4-6.126 6.126 4.85 7.178 5.8 8.583-10.173 1.966-8.506 1.646v41.932l8.51 1.643 10.16 1.962-5.787 8.553-4.858 7.179 6.13 6.13 17.399 17.399 6.126 6.126 7.177-4.85 8.56-5.782 1.954 10.141 1.64 8.513H350.975l1.645-8.507 1.964-10.15 8.566 5.787 7.178 4.846 6.124-6.124 17.4-17.399 6.13-6.13-4.858-7.18-5.788-8.554 10.153-1.96 8.51-1.643v-41.932l-8.507-1.646-10.165-1.965 5.8-8.584 4.85-7.178-6.125-6.126-17.4-17.4-6.13-6.13-7.18 4.858-8.558 5.792-1.964-10.166L350.98 63zm-20.98 118.948c23.176 0 41.95-18.318 41.95-40.94 0-22.607-18.774-40.927-41.95-40.927-23.174 0-41.948 18.32-41.948 40.926 0 22.623 18.774 40.941 41.949 40.941zM342.313 73.5l3.855 19.963a47.184 47.184 0 0 1 6.037 2.502l16.824-11.386 17.4 17.4-11.362 16.818a53.171 53.171 0 0 1 2.502 6.066l19.932 3.855v24.601l-19.932 3.848a56.644 56.644 0 0 1-2.502 6.066l11.362 16.795-17.4 17.4-16.824-11.364a44.931 44.931 0 0 1-6.037 2.504l-3.855 19.932H317.68l-3.84-19.932a43.821 43.821 0 0 1-6.043-2.504l-16.818 11.364-17.4-17.4 11.364-16.795a53.759 53.759 0 0 1-2.51-6.066l-19.933-3.848v-24.601l19.933-3.855a50.617 50.617 0 0 1 2.51-6.066l-11.364-16.818 17.4-17.4 16.818 11.386a45.957 45.957 0 0 1 6.043-2.502l3.84-19.963h24.632z" fill="#AD245D"/></g></svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
1
backend/public/img/badges/user_role_developer.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#AD245D" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><g fill="#FFF"><path d="M35.01 367.726c-.08-21.169-.205-53.162 21.257-71.332 3.817-3.253 9.93-7.497 17.321-9.224 2.575-.523 4.956-.756 7.262-.979 4.438-.431 8.27-.804 12.054-2.9l4.954-2.846c9.87-5.655 19.194-10.996 28.226-17.377 5.085-3.632 6.726-15.73 6.095-25.428-.214-2.792-1.893-5.7-3.67-8.777-1.097-1.901-2.232-3.867-3.065-5.916l-.073-.199a56.976 56.976 0 0 1-.422-1.443c-1.195-4.205-1.933-6.378-2.386-7.476-7.029-.944-11.8-8.647-12.888-21.006l-.031-.557c-.645-12.785.808-16.13 2.316-17.716.24-.254.505-.475.783-.666-1.754-16.051 3.115-32.521 13.358-44.704 9.314-11.079 21.955-17.18 35.592-17.18 3.73 0 7.55.458 11.355 1.362 25.63 6.228 41.679 30.27 40.062 59.227.53.251 1.018.61 1.44 1.066 2.752 2.964 2.47 10.97 2.22 14.276l-.024.41c-.335 5.236-.684 10.65-3.052 15.73-1.739 3.918-4.405 6.242-6.76 8.29-2.396 2.089-4.288 3.735-5.294 6.885-.7 2.416-1.645 4.866-2.559 7.235-1.752 4.538-3.407 8.827-3.54 13.244-.427 10.222 1.17 18.391 4.172 21.359 5.097 5.163 13.003 9.391 19.978 13.121 1.6.855 3.166 1.692 4.654 2.517 9.28 5.052 16.07 7.915 25.309 8.557 9.118.849 18.056 5.193 24.754 11.97.736.641 1.82 1.744 3.694 3.648 4.416 4.492 4.416 4.492 4.426 5.852l.007.758c10.783 17.702 11.14 40.656 11.415 58.169l.05 3.28-3.278.028c-42.05.363-84.058.677-126.058.993-42.12.314-84.232.632-126.367.994l-3.273.029-.014-3.274z"/><text font-family="Impact" font-size="118" font-style="condensed" font-weight="700" transform="translate(1 -1)"><tspan x="256" y="208"></></tspan></text></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
backend/public/img/badges/user_role_moderator.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="512" height="444" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#AD245D" d="M384 .297L511.392 221.65l-128 221.702-255.392.352L.608 222.35 128.608.65z"/><g fill="#FFF"><path d="M34.944 367.726c-.081-21.169-.205-53.162 21.215-71.332 3.81-3.253 9.91-7.497 17.288-9.224 2.57-.523 4.946-.756 7.247-.979 4.43-.431 8.254-.804 12.03-2.9l4.945-2.846c9.851-5.655 19.157-10.996 28.171-17.377 5.075-3.632 6.713-15.73 6.082-25.428-.213-2.792-1.888-5.7-3.662-8.777-1.095-1.901-2.228-3.867-3.059-5.916l-.073-.199a57.061 57.061 0 0 1-.42-1.443c-1.194-4.205-1.93-6.378-2.382-7.476-7.015-.944-11.778-8.647-12.864-21.006l-.03-.557c-.644-12.785.806-16.13 2.31-17.716.241-.254.505-.475.783-.666-1.75-16.051 3.11-32.521 13.331-44.704 9.296-11.079 21.912-17.18 35.524-17.18 3.722 0 7.535.458 11.332 1.362 25.58 6.228 41.597 30.27 39.983 59.227.53.251 1.016.61 1.439 1.066 2.745 2.964 2.464 10.97 2.215 14.276l-.024.41c-.335 5.236-.683 10.65-3.046 15.73-1.736 3.918-4.397 6.242-6.747 8.29-2.391 2.089-4.28 3.735-5.284 6.885-.698 2.416-1.642 4.866-2.554 7.235-1.749 4.538-3.4 8.827-3.534 13.244-.425 10.222 1.17 18.391 4.165 21.359 5.087 5.163 12.977 9.391 19.939 13.121 1.597.855 3.16 1.692 4.645 2.517 9.262 5.052 16.038 7.915 25.259 8.557 9.1.849 18.02 5.193 24.706 11.97.735.641 1.817 1.744 3.687 3.648 4.408 4.492 4.408 4.492 4.417 5.852l.007.758c10.762 17.702 11.12 40.656 11.392 58.169l.05 3.28-3.271.028c-41.968.363-83.894.677-125.812.993-42.038.314-84.067.632-126.12.994l-3.267.029-.013-3.274zM332.387 115.763h15.318v86.734h-15.318v.513l-22.127-17.64v.12h-10.21V211h-22.128v-25.51h-20.424v-52.722h52.762v-.508l22.127-17.065v.568zm34.313 72.093l-7.988-4.591c15.803-27.453 1.717-46.642 1.106-47.443l7.304-5.607c.774.993 18.547 24.675-.422 57.641zm27.361 21.216l-13.866-7.975c27.437-47.66 2.98-80.973 1.918-82.36L394.795 109c1.343 1.723 32.2 42.839-.734 100.072z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
backend/public/img/badges/wooold_de_bee.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M0 .89h265.71v170.52H0z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#84A939" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><path d="M210.84 346.697l2.17.412c3.654-5.4 7.304-10.797 11.167-16.514l-13.948-4.158c.216 7.17.415 13.715.61 20.26" fill="#FEFEFE"/><g transform="translate(124 96.872)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M265.476 135.466c2.28-16.36-12.373-28.485-33.146-37.925-3.264-1.498-32.042-13.807-71.066-11.423-.493-.025-.99-.055-1.485-.077-4.622-.21-8.12-1.25-9.72-6.232 8.024-2.458 14.575-6.522 17.408-15.188 2.705-8.281.812-14.61-5.396-18.966 3.091-4.423 7.693-7.442 11.87-10.785 5.529-4.425 11.32-7.711 18.427-8.009.24.227.53.414.906.525 2.803.837 4.097-3.51 1.295-4.345-1.034-.309-1.896.11-2.368.801-6.4.293-11.767 2.709-17.154 6.329-5.428 3.635-11.634 8.205-15.427 13.712-.48-1.289-2.718-6.195-9.466-8.056-4.63-1.276-7.919-.39-9.835.541 1.488-12.829-8.348-26.169-18.146-33.456-.048-.826-.524-1.61-1.556-1.917-2.804-.835-4.097 3.502-1.295 4.337.41.121.772.127 1.11.054 9.044 6.741 19.157 19.93 16.758 31.839-.028.13-.005.243-.002.363-16.899-.786-23.471 21.104-10.53 35.477-4.684 2.295-5.616 2.748-8.613 1.177-32.087-24.754-64.257-30.463-67.89-31.01C27.605 39.755 8.707 41.88 1.662 56.82-2.182 65.082.903 73.342 8.21 80.396A46.714 46.714 0 0 0 10.342 95.9c13.312 41.39 67.248 17.494 97.338 5.981 1.043 2.698 2.299 5.946 3.627 9.388l1.733.513c-12.762 1.374-25.1 8.753-34.245 20.253 11.938-.127 26.762 2.062 42.317 6.698 15.322 4.568 28.756 10.738 38.637 17.268-1.399-14.62-7.72-27.531-17.685-35.634l.046.013 9.111-6.463c18.754 25.75 51.289 77.278 85.606 49.502a46.669 46.669 0 0 0 10.267-11.807c9.978-1.902 17.079-7.125 18.382-16.146" fill="#FEFEFE" mask="url(#b)"/></g><path d="M239.142 255.648c-17.848-5.32-34.719-7.408-47.378-6.481a74.024 74.024 0 0 0-.923 2.876 70.569 70.569 0 0 0-2.728 14.609c12.465-.643 28.687 1.501 45.785 6.598 16.902 5.04 31.502 12.025 41.573 19.298a70.533 70.533 0 0 0 6.48-16.67c-10.089-7.605-25.188-14.976-42.809-20.23" fill="#E5ECD6"/><path d="M189.62 286.635c3.972 16.568 14.648 29.797 29.836 34.325 15.22 4.537 31.434-.721 43.835-12.47-9.486-5.756-21.669-11.116-35.364-15.198-13.858-4.13-27.135-6.319-38.307-6.657" fill="#FEFEFE"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
1
backend/public/img/badges/wooold_de_butterfly.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M.073.459h124.425v172.749H.073z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#84A939" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><path d="M232.409 254.848c.268-7.727 7.016-33.547 7.258-39.582.838-19.569-19.295-37.316-35.404-44.459-20.896-9.313-46.108-15.054-68.428-7.619-17.536 5.837-23.16 21.744-17.995 38.456 5.905 18.931 25.96 23.883 43.23 27.984-2.735 6.685-13.588 12.887-16.81 23.005-2.78 8.729-2.97 17.367-1.227 26.318.333 1.703 1.236 3.314 1.819 4.836 5.689 14.863 40.24 34.999 49.693 39.224 3.09 1.366 18.603 9.7 24.605 9.519.476-.016.962-.047 1.453-.096 12.115-1.246 17.577-9.799 19.811-20.828.36-1.791-8.229-49.896-8.005-56.758" fill="#E5ECD6"/><path d="M268.885 204.496c-.268-7.83-8.948-10.453-13.935-7.422-2.966.291-5.702 2.075-6.746 5.894-4.237 15.355-1.235 30.542-.916 46.19.358 17.416-.37 34.49 2.024 51.804 1.158 8.493 14.876 8.837 16.773.612 3.27-14.248 1.438-28.557 1.233-42.969-.291-18.067 2.216-36.006 1.567-54.11" fill="#FEFEFE"/><path d="M268.54 185.138c3.437-13.134 12.433-23.528 20.018-34.472 9.29-13.399 19.905-24.457 35.392-29.791.675.343 1.439.565 2.342.565 6.73 0 6.73-10.44 0-10.44-2.483 0-4.111 1.491-4.696 3.324-13.922 4.858-24.18 13.72-33.687 25.246-10.257 12.408-21.876 27.757-26.012 43.492-1.119 4.336 5.523 6.417 6.643 2.076M251.284 184.303c-2.824-31.263-34.996-55.942-62.636-66.026-.649-1.789-2.215-3.207-4.696-3.207-6.732 0-6.732 10.42 0 10.42.979 0 1.782-.23 2.481-.612 24.382 8.926 55.359 31.37 57.89 59.222.381 4.386 7.37 4.679 6.961.203" fill="#BBD094"/><g transform="translate(273.18 159.325)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M8.09 95.524C7.822 87.797 1.074 61.977.832 55.94c-.838-19.568 19.294-37.316 35.404-44.458 20.896-9.313 46.106-15.055 68.427-7.62C122.2 9.702 127.824 25.608 122.66 42.32c-5.905 18.932-25.961 23.884-43.23 27.984 2.734 6.685 13.588 12.887 16.81 23.005 2.78 8.73 2.97 17.367 1.226 26.318-.334 1.704-1.236 3.315-1.818 4.836-5.69 14.863-40.24 35-49.695 39.225-3.088 1.366-18.601 9.7-24.604 9.518a21.536 21.536 0 0 1-1.453-.095C7.78 171.864 2.32 163.31.085 152.282c-.36-1.792 8.228-49.896 8.005-56.758" fill="#E5ECD6" mask="url(#b)"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
1
backend/public/img/badges/wooold_de_double_rainbow.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M.474.589h222.082V155.85H.474z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#84A939" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><path d="M106 295.77c0-8.772 4.114-14.323 7.973-17.622 3.405-2.91 6.612-4.072 6.612-4.072s.991-13.569 10.898-22.434c.097-.086.18-.179.278-.265 3.804-3.31 8.891-5.92 15.692-6.854.112-.015.214-.039.33-.053.545-.07 1.087-.144 1.654-.193 3.884-.328 7.329-.025 10.403.706 18.218 4.333 22.977 24.233 22.977 24.233s9.721-6.152 19.125.324c9.397 6.48 7.454 15.87 7.454 15.87 17.507-2.266 26.092 4.209 26.092 18.623 0 14.412-17.015 17.001-17.015 17.001h-93.672s-.333-.157-.89-.452C120.203 318.614 106 310.138 106 295.771" fill="#FEFEFE"/><path d="M333.747 275.73c0-55.854-45.477-101.293-101.378-101.293-43.712 0-80.995 27.807-95.194 66.632a36.758 36.758 0 0 0-9.437 5.96l-.479.442c-5.63 5.087-8.934 11.15-10.906 16.39 5.97-58.709 55.707-104.681 116.016-104.681 64.32 0 116.648 52.284 116.648 116.55 0 16.978-3.683 33.105-10.245 47.668h-16.984a100.603 100.603 0 0 0 11.96-47.669" fill="#E5ECD6"/><path d="M320.29 275.73c0-48.44-39.44-87.85-87.923-87.85-34.819 0-64.929 20.35-79.154 49.752-.883.022-1.766.048-2.66.124-.625.051-1.223.127-1.818.205l-.773.112c-3.737.513-7.249 1.487-10.519 2.889 14.22-38.649 51.374-66.313 94.924-66.313 55.784 0 101.17 45.345 101.17 101.08a100.39 100.39 0 0 1-11.987 47.67h-15.386c8.92-13.742 14.127-30.106 14.127-47.67" fill="#CCDBAE"/><path d="M308.526 275.73c0-41.957-34.164-76.092-76.156-76.092-28.736 0-53.744 16.016-66.704 39.555-.62-.182-1.233-.366-1.877-.518-3.268-.778-6.708-1.125-10.244-1.06 14.209-29.225 44.179-49.44 78.825-49.44 48.315 0 87.626 39.278 87.626 87.554 0 17.574-5.227 33.94-14.183 47.67h-14.145c10.533-13.055 16.858-29.635 16.858-47.67" fill="#B5CB88"/><g transform="translate(182.963 98.98)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M104.288 16.014c56.52-.178 102.65 45.62 102.829 102.093.04 13.318-2.485 26.05-7.103 37.743h16.444c3.993-11.874 6.14-24.585 6.097-37.791C222.35 53.079 169.273.382 104.241.588 59.286.73 20.245 26.132.474 63.257c7.3-3.05 14.945-5.433 22.867-7.06 18.668-24.318 47.967-40.079 80.947-40.183" fill="#E5ECD6" mask="url(#b)"/></g><path d="M376.475 217.131c.042 13.462-2.936 26.238-8.287 37.7h14.566c4.626-11.69 7.153-24.425 7.11-37.742-.178-56.354-46.21-102.058-102.612-101.88-32.822.104-61.988 15.75-80.621 39.905 6.557-1.328 13.306-2.117 20.192-2.376 15.83-14.881 37.068-24.076 60.472-24.15 49.019-.155 89.023 39.565 89.18 88.543" fill="#CCDBAE"/><path d="M364.578 217.169c.042 13.669-3.534 26.506-9.78 37.663h13.049a87.88 87.88 0 0 0 8.33-37.7c-.155-48.812-40.028-88.4-88.881-88.246-23.205.076-44.275 9.145-60.024 23.84 1.434-.048 2.867-.108 4.313-.108 4.568 0 9.074.26 13.516.738 12.112-8.07 26.607-12.831 42.23-12.881 42.461-.134 77.112 34.271 77.247 76.694" fill="#B5CB88"/><path d="M354.798 254.83h-.377c.03.174.072.345.102.519.098-.17.18-.349.275-.518" fill="#CABAA1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
1
backend/public/img/badges/wooold_de_end_of_rainbow.svg
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
1
backend/public/img/badges/wooold_de_flower.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#84A939" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><path d="M238.234 309.762c-1.375.258-2.792 1.55-3.708 2.736-1.608 2.081-3.007 1.861-5.165.778-8.43-4.235-16.65-1.328-20.991 6.88-.792 1.496-2.614 2.848-4.257 3.43-6.343 2.24-10.08 6.638-10.719 13.498h98.273c-.897-5.657-3.359-9.528-8.075-12.241-2.12-1.22-4.18-3.107-5.416-5.194-4.232-7.157-10.743-9.897-18.71-7.382-3.109.983-9.193-2.98-10.331-6.279h-6.066c-.46 1.829-1.749 3.193-4.835 3.774" fill="#FEFEFE"/><path d="M252.144 200.674c-2.639 3.425-9.63 13.408-11.283 24.964l-.87 5.505c-2.211-5.534-6.18-10.496-11.796-14.408-11.918-8.303-25.534-11.544-39.636-13.142-6.885-.78-13.842-.944-21.559-1.436 4.949 20.969 11.08 40.049 25.452 55.231 6.342 6.698 14.362 10.992 23.85 10.166 5.9-.514 11.66-2.65 17.862-4.149 4.946 11.405 7.394 24.204 8.876 37.28.143 1.266.255 2.46.227 3.56h5.569c-.882-7.237-1.761-14.484-2.983-21.665-.397-2.335-.79-4.38-.575-5.91.299-1.79.748-1.79 1.582-3.072.884-1.193 2.198-2.26 4.092-3.402 2.143 1.826 3.998 3.807 6.216 5.223 4.423 2.827 7.262 5.105 14.637 5.803 2.418.234 5.006.276 7.187-.337 4.318-1.207 8.743-2.716 12.51-5.075 14.244-8.905 24.206-21.803 32.765-35.977-.592-.457-.912-.851-1.319-.994-14.417-5.058-29.212-8.309-44.538-6.92-8.774.796-17.394 3.209-23.143 10.763-3.56 4.676-6.985 9.634-5.063 16.138-.412 3.335-1.967 5.828-4.244 7.79-1.402-12.036-2.767-39.862 11.142-58.371" fill="#E5ECD6"/><path d="M258.188 206.85s7.083 18.602 21.241 17.487c0 0 20.87.371 21.987-27.161 0 0 21.198 8.399 32.42-4.093 0 0 15.653-18.974-8.567-34.6 0 0 18.63-8.93 11.921-30.137 0 0-6.707-19.347-35.028-7.812 0 0-.746-26.045-23.107-26.788 0 0-23.538.821-21.239 27.53 0 0-23.85-13.022-35.03 5.582 0 0-10.062 18.973 10.435 31.996 0 0-22.36 17.115-7.826 35.719 0 0 10.435 12.649 27.762 4.836m25.716-27.533c-6.827 0-12.36-5.525-12.36-12.342 0-6.816 5.533-12.342 12.36-12.342 6.826 0 12.36 5.526 12.36 12.342 0 6.817-5.534 12.342-12.36 12.342" fill="#FFF"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
1
backend/public/img/badges/wooold_de_lifetree.svg
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
1
backend/public/img/badges/wooold_de_magic_rainbow.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M.318.89h249.647v250.352H.318z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#84A939" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><g transform="translate(135.784 96.866)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M124.802 234.855c59.98 0 108.778-48.803 108.778-108.788 0-59.988-48.797-108.79-108.778-108.79-46.9 0-86.909 29.866-102.143 71.563a39.335 39.335 0 0 0-10.123 6.401l-.513.474C5.98 101.181 2.435 107.691.318 113.318 6.725 50.265 60.092.89 124.802.89c69.014 0 125.163 56.153 125.163 125.177 0 69.021-56.15 125.175-125.163 125.175-46.925 0-87.849-25.989-109.267-64.306l19.208-.021c19.576 28.89 52.615 47.94 90.059 47.94" fill="#E5ECD6" mask="url(#b)"/></g><path d="M260.586 114.37c59.852 0 108.55 48.701 108.55 108.562 0 59.86-48.698 108.561-108.55 108.561-37.305 0-70.211-18.96-89.753-47.711l17.75-.02c17.315 20.476 43.151 33.52 72.003 33.52 52.019 0 94.34-42.325 94.34-94.35 0-52.025-42.321-94.35-94.34-94.35-37.36 0-69.668 21.853-84.936 53.43-.947.027-1.892.055-2.851.136-.673.056-1.31.135-1.949.22l-.834.12c-4.005.55-7.774 1.598-11.285 3.104 15.257-41.51 55.121-71.222 101.855-71.222" fill="#CCDBAE"/><path d="M260.586 128.899c51.845 0 94.023 42.182 94.023 94.033s-42.178 94.033-94.023 94.033c-28.657 0-54.333-12.913-71.588-33.202l17.15-.02c14.463 12.977 33.527 20.913 54.438 20.913 45.057 0 81.715-36.662 81.715-81.724 0-45.062-36.658-81.724-81.715-81.724-30.832 0-57.664 17.202-71.57 42.483-.663-.194-1.324-.392-2.016-.556-3.508-.834-7.195-1.207-10.99-1.138 15.245-31.39 47.403-53.098 84.576-53.098" fill="#B5CB88"/><path d="M127 247.046c0-9.422 4.415-15.384 8.555-18.926 3.653-3.125 7.094-4.374 7.094-4.374s1.065-14.572 11.693-24.095c.104-.091.195-.192.299-.285 4.083-3.554 9.543-6.356 16.837-7.361.12-.016.231-.04.354-.057.584-.073 1.167-.154 1.773-.207 4.169-.351 7.867-.026 11.166.76 19.546 4.653 24.653 26.027 24.653 26.027s10.43-6.608 20.522.347c10.082 6.958 7.997 17.045 7.997 17.045 18.783-2.436 27.997 4.52 27.997 20 0 15.478-18.256 18.259-18.256 18.259H147.17s-.357-.167-.954-.485C142.24 271.58 127 262.478 127 247.046" fill="#FEFEFE"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
backend/public/img/badges/wooold_de_super_founder.svg
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
4
backend/scripts/deploy.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "See me deployed at $1 :)"
|
||||
|
||||
3
backend/scripts/docker_push.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
docker push humanconnection/nitro-backend:latest
|
||||
1
backend/scripts/test.sh
Executable file
@ -0,0 +1 @@
|
||||
#!/usr/bin/env bash
|
||||
231
backend/src/activitypub/ActivityPub.js
Normal file
@ -0,0 +1,231 @@
|
||||
import {
|
||||
extractNameFromId,
|
||||
extractDomainFromUrl,
|
||||
signAndSend
|
||||
} from './utils'
|
||||
import {
|
||||
isPublicAddressed,
|
||||
sendAcceptActivity,
|
||||
sendRejectActivity
|
||||
} from './utils/activity'
|
||||
import request from 'request'
|
||||
import as from 'activitystrea.ms'
|
||||
import NitroDataSource from './NitroDataSource'
|
||||
import router from './routes'
|
||||
import dotenv from 'dotenv'
|
||||
import Collections from './Collections'
|
||||
import uuid from 'uuid/v4'
|
||||
const debug = require('debug')('ea')
|
||||
|
||||
let activityPub = null
|
||||
|
||||
export { activityPub }
|
||||
|
||||
export default class ActivityPub {
|
||||
constructor (domain, port, uri) {
|
||||
if (domain === 'localhost') { this.domain = `${domain}:${port}` } else { this.domain = domain }
|
||||
this.port = port
|
||||
this.dataSource = new NitroDataSource(uri)
|
||||
this.collections = new Collections(this.dataSource)
|
||||
}
|
||||
|
||||
static init (server) {
|
||||
if (!activityPub) {
|
||||
dotenv.config()
|
||||
const url = new URL(process.env.GRAPHQL_URI)
|
||||
activityPub = new ActivityPub(url.hostname || 'localhost', url.port || 4000, url.origin)
|
||||
|
||||
// integrate into running graphql express server
|
||||
server.express.set('ap', activityPub)
|
||||
server.express.set('port', url.port)
|
||||
server.express.use(router)
|
||||
console.log('-> ActivityPub middleware added to the graphql express server')
|
||||
} else {
|
||||
console.log('-> ActivityPub middleware already added to the graphql express server')
|
||||
}
|
||||
}
|
||||
|
||||
handleFollowActivity (activity) {
|
||||
debug(`inside FOLLOW ${activity.actor}`)
|
||||
let toActorName = extractNameFromId(activity.object)
|
||||
let fromDomain = extractDomainFromUrl(activity.actor)
|
||||
const dataSource = this.dataSource
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
url: activity.actor,
|
||||
headers: {
|
||||
'Accept': 'application/activity+json'
|
||||
}
|
||||
}, async (err, response, toActorObject) => {
|
||||
if (err) return reject(err)
|
||||
debug(`name = ${toActorName}@${this.domain}`)
|
||||
// save shared inbox
|
||||
toActorObject = JSON.parse(toActorObject)
|
||||
await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
|
||||
|
||||
let followersCollectionPage = await this.dataSource.getFollowersCollectionPage(activity.object)
|
||||
|
||||
const followActivity = as.follow()
|
||||
.id(activity.id)
|
||||
.actor(activity.actor)
|
||||
.object(activity.object)
|
||||
|
||||
// add follower if not already in collection
|
||||
if (followersCollectionPage.orderedItems.includes(activity.actor)) {
|
||||
debug('follower already in collection!')
|
||||
debug(`inbox = ${toActorObject.inbox}`)
|
||||
resolve(sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox))
|
||||
} else {
|
||||
followersCollectionPage.orderedItems.push(activity.actor)
|
||||
}
|
||||
debug(`toActorObject = ${toActorObject}`)
|
||||
toActorObject = typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
|
||||
debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
|
||||
debug(`inbox = ${toActorObject.inbox}`)
|
||||
debug(`outbox = ${toActorObject.outbox}`)
|
||||
debug(`followers = ${toActorObject.followers}`)
|
||||
debug(`following = ${toActorObject.following}`)
|
||||
|
||||
try {
|
||||
await dataSource.saveFollowersCollectionPage(followersCollectionPage)
|
||||
debug('follow activity saved')
|
||||
resolve(sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox))
|
||||
} catch (e) {
|
||||
debug('followers update error!', e)
|
||||
resolve(sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleUndoActivity (activity) {
|
||||
debug('inside UNDO')
|
||||
switch (activity.object.type) {
|
||||
case 'Follow':
|
||||
const followActivity = activity.object
|
||||
return this.dataSource.undoFollowActivity(followActivity.actor, followActivity.object)
|
||||
case 'Like':
|
||||
return this.dataSource.deleteShouted(activity)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
handleCreateActivity (activity) {
|
||||
debug('inside create')
|
||||
switch (activity.object.type) {
|
||||
case 'Article':
|
||||
case 'Note':
|
||||
const articleObject = activity.object
|
||||
if (articleObject.inReplyTo) {
|
||||
return this.dataSource.createComment(activity)
|
||||
} else {
|
||||
return this.dataSource.createPost(activity)
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteActivity (activity) {
|
||||
debug('inside delete')
|
||||
switch (activity.object.type) {
|
||||
case 'Article':
|
||||
case 'Note':
|
||||
return this.dataSource.deletePost(activity)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdateActivity (activity) {
|
||||
debug('inside update')
|
||||
switch (activity.object.type) {
|
||||
case 'Note':
|
||||
case 'Article':
|
||||
return this.dataSource.updatePost(activity)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
handleLikeActivity (activity) {
|
||||
// TODO differ if activity is an Article/Note/etc.
|
||||
return this.dataSource.createShouted(activity)
|
||||
}
|
||||
|
||||
handleDislikeActivity (activity) {
|
||||
// TODO differ if activity is an Article/Note/etc.
|
||||
return this.dataSource.deleteShouted(activity)
|
||||
}
|
||||
|
||||
async handleAcceptActivity (activity) {
|
||||
debug('inside accept')
|
||||
switch (activity.object.type) {
|
||||
case 'Follow':
|
||||
const followObject = activity.object
|
||||
const followingCollectionPage = await this.collections.getFollowingCollectionPage(followObject.actor)
|
||||
followingCollectionPage.orderedItems.push(followObject.object)
|
||||
await this.dataSource.saveFollowingCollectionPage(followingCollectionPage)
|
||||
}
|
||||
}
|
||||
|
||||
getActorObject (url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
url: url,
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
resolve(JSON.parse(body))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
generateStatusId (slug) {
|
||||
return `http://${this.domain}/activitypub/users/${slug}/status/${uuid()}`
|
||||
}
|
||||
|
||||
async sendActivity (activity) {
|
||||
delete activity.send
|
||||
const fromName = extractNameFromId(activity.actor)
|
||||
if (Array.isArray(activity.to) && isPublicAddressed(activity)) {
|
||||
debug('is public addressed')
|
||||
const sharedInboxEndpoints = await this.dataSource.getSharedInboxEndpoints()
|
||||
// serve shared inbox endpoints
|
||||
sharedInboxEndpoints.map((sharedInbox) => {
|
||||
return this.trySend(activity, fromName, new URL(sharedInbox).host, sharedInbox)
|
||||
})
|
||||
activity.to = activity.to.filter((recipient) => {
|
||||
return !(isPublicAddressed({ to: recipient }))
|
||||
})
|
||||
// serve the rest
|
||||
activity.to.map(async (recipient) => {
|
||||
debug('serve rest')
|
||||
const actorObject = await this.getActorObject(recipient)
|
||||
return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox)
|
||||
})
|
||||
} else if (typeof activity.to === 'string') {
|
||||
debug('is string')
|
||||
const actorObject = await this.getActorObject(activity.to)
|
||||
return this.trySend(activity, fromName, new URL(activity.to).host, actorObject.inbox)
|
||||
} else if (Array.isArray(activity.to)) {
|
||||
activity.to.map(async (recipient) => {
|
||||
const actorObject = await this.getActorObject(recipient)
|
||||
return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox)
|
||||
})
|
||||
}
|
||||
}
|
||||
async trySend (activity, fromName, host, url, tries = 5) {
|
||||
try {
|
||||
return await signAndSend(activity, fromName, host, url)
|
||||
} catch (e) {
|
||||
if (tries > 0) {
|
||||
setTimeout(function () {
|
||||
return this.trySend(activity, fromName, host, url, --tries)
|
||||
}, 20000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
backend/src/activitypub/Collections.js
Normal file
@ -0,0 +1,28 @@
|
||||
export default class Collections {
|
||||
constructor (dataSource) {
|
||||
this.dataSource = dataSource
|
||||
}
|
||||
getFollowersCollection (actorId) {
|
||||
return this.dataSource.getFollowersCollection(actorId)
|
||||
}
|
||||
|
||||
getFollowersCollectionPage (actorId) {
|
||||
return this.dataSource.getFollowersCollectionPage(actorId)
|
||||
}
|
||||
|
||||
getFollowingCollection (actorId) {
|
||||
return this.dataSource.getFollowingCollection(actorId)
|
||||
}
|
||||
|
||||
getFollowingCollectionPage (actorId) {
|
||||
return this.dataSource.getFollowingCollectionPage(actorId)
|
||||
}
|
||||
|
||||
getOutboxCollection (actorId) {
|
||||
return this.dataSource.getOutboxCollection(actorId)
|
||||
}
|
||||
|
||||
getOutboxCollectionPage (actorId) {
|
||||
return this.dataSource.getOutboxCollectionPage(actorId)
|
||||
}
|
||||
}
|
||||
552
backend/src/activitypub/NitroDataSource.js
Normal file
@ -0,0 +1,552 @@
|
||||
import {
|
||||
throwErrorIfApolloErrorOccurred,
|
||||
extractIdFromActivityId,
|
||||
extractNameFromId,
|
||||
constructIdFromName
|
||||
} from './utils'
|
||||
import {
|
||||
createOrderedCollection,
|
||||
createOrderedCollectionPage
|
||||
} from './utils/collection'
|
||||
import {
|
||||
createArticleObject,
|
||||
isPublicAddressed
|
||||
} from './utils/activity'
|
||||
import crypto from 'crypto'
|
||||
import gql from 'graphql-tag'
|
||||
import { createHttpLink } from 'apollo-link-http'
|
||||
import { setContext } from 'apollo-link-context'
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory'
|
||||
import fetch from 'node-fetch'
|
||||
import { ApolloClient } from 'apollo-client'
|
||||
import trunc from 'trunc-html'
|
||||
const debug = require('debug')('ea:nitro-datasource')
|
||||
|
||||
export default class NitroDataSource {
|
||||
constructor (uri) {
|
||||
this.uri = uri
|
||||
const defaultOptions = {
|
||||
query: {
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'all'
|
||||
}
|
||||
}
|
||||
const link = createHttpLink({ uri: this.uri, fetch: fetch }) // eslint-disable-line
|
||||
const cache = new InMemoryCache()
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
// generate the authentication token (maybe from env? Which user?)
|
||||
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJuYW1lIjoiUGV0ZXIgTHVzdGlnIiwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9qb2huY2FmYXp6YS8xMjguanBnIiwiaWQiOiJ1MSIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5vcmciLCJzbHVnIjoicGV0ZXItbHVzdGlnIiwiaWF0IjoxNTUyNDIwMTExLCJleHAiOjE2Mzg4MjAxMTEsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMCIsInN1YiI6InUxIn0.G7An1yeQUViJs-0Qj-Tc-zm0WrLCMB3M02pfPnm6xzw'
|
||||
// return the headers to the context so httpLink can read them
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
Authorization: token ? `Bearer ${token}` : ''
|
||||
}
|
||||
}
|
||||
})
|
||||
this.client = new ApolloClient({
|
||||
link: authLink.concat(link),
|
||||
cache: cache,
|
||||
defaultOptions
|
||||
})
|
||||
}
|
||||
|
||||
async getFollowersCollection (actorId) {
|
||||
const slug = extractNameFromId(actorId)
|
||||
debug(`slug= ${slug}`)
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${slug}") {
|
||||
followedByCount
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
debug('successfully fetched followers')
|
||||
debug(result.data)
|
||||
if (result.data) {
|
||||
const actor = result.data.User[0]
|
||||
const followersCount = actor.followedByCount
|
||||
|
||||
const followersCollection = createOrderedCollection(slug, 'followers')
|
||||
followersCollection.totalItems = followersCount
|
||||
|
||||
return followersCollection
|
||||
} else {
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
}
|
||||
|
||||
async getFollowersCollectionPage (actorId) {
|
||||
const slug = extractNameFromId(actorId)
|
||||
debug(`getFollowersPage slug = ${slug}`)
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug:"${slug}") {
|
||||
followedBy {
|
||||
slug
|
||||
}
|
||||
followedByCount
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
debug(result.data)
|
||||
if (result.data) {
|
||||
const actor = result.data.User[0]
|
||||
const followers = actor.followedBy
|
||||
const followersCount = actor.followedByCount
|
||||
|
||||
const followersCollection = createOrderedCollectionPage(slug, 'followers')
|
||||
followersCollection.totalItems = followersCount
|
||||
debug(`followers = ${JSON.stringify(followers, null, 2)}`)
|
||||
await Promise.all(
|
||||
followers.map(async (follower) => {
|
||||
followersCollection.orderedItems.push(constructIdFromName(follower.slug))
|
||||
})
|
||||
)
|
||||
|
||||
return followersCollection
|
||||
} else {
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
}
|
||||
|
||||
async getFollowingCollection (actorId) {
|
||||
const slug = extractNameFromId(actorId)
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug:"${slug}") {
|
||||
followingCount
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
debug(result.data)
|
||||
if (result.data) {
|
||||
const actor = result.data.User[0]
|
||||
const followingCount = actor.followingCount
|
||||
|
||||
const followingCollection = createOrderedCollection(slug, 'following')
|
||||
followingCollection.totalItems = followingCount
|
||||
|
||||
return followingCollection
|
||||
} else {
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
}
|
||||
|
||||
async getFollowingCollectionPage (actorId) {
|
||||
const slug = extractNameFromId(actorId)
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug:"${slug}") {
|
||||
following {
|
||||
slug
|
||||
}
|
||||
followingCount
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
debug(result.data)
|
||||
if (result.data) {
|
||||
const actor = result.data.User[0]
|
||||
const following = actor.following
|
||||
const followingCount = actor.followingCount
|
||||
|
||||
const followingCollection = createOrderedCollectionPage(slug, 'following')
|
||||
followingCollection.totalItems = followingCount
|
||||
|
||||
await Promise.all(
|
||||
following.map(async (user) => {
|
||||
followingCollection.orderedItems.push(await constructIdFromName(user.slug))
|
||||
})
|
||||
)
|
||||
|
||||
return followingCollection
|
||||
} else {
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
}
|
||||
|
||||
async getOutboxCollection (actorId) {
|
||||
const slug = extractNameFromId(actorId)
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug:"${slug}") {
|
||||
contributions {
|
||||
title
|
||||
slug
|
||||
content
|
||||
contentExcerpt
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
debug(result.data)
|
||||
if (result.data) {
|
||||
const actor = result.data.User[0]
|
||||
const posts = actor.contributions
|
||||
|
||||
const outboxCollection = createOrderedCollection(slug, 'outbox')
|
||||
outboxCollection.totalItems = posts.length
|
||||
|
||||
return outboxCollection
|
||||
} else {
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
}
|
||||
|
||||
async getOutboxCollectionPage (actorId) {
|
||||
const slug = extractNameFromId(actorId)
|
||||
debug(`inside getting outbox collection page => ${slug}`)
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug:"${slug}") {
|
||||
actorId
|
||||
contributions {
|
||||
id
|
||||
activityId
|
||||
objectId
|
||||
title
|
||||
slug
|
||||
content
|
||||
contentExcerpt
|
||||
createdAt
|
||||
author {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
debug(result.data)
|
||||
if (result.data) {
|
||||
const actor = result.data.User[0]
|
||||
const posts = actor.contributions
|
||||
|
||||
const outboxCollection = createOrderedCollectionPage(slug, 'outbox')
|
||||
outboxCollection.totalItems = posts.length
|
||||
await Promise.all(
|
||||
posts.map(async (post) => {
|
||||
outboxCollection.orderedItems.push(await createArticleObject(post.activityId, post.objectId, post.content, post.author.name, post.id, post.createdAt))
|
||||
})
|
||||
)
|
||||
|
||||
debug('after createNote')
|
||||
return outboxCollection
|
||||
} else {
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
}
|
||||
|
||||
async undoFollowActivity (fromActorId, toActorId) {
|
||||
const fromUserId = await this.ensureUser(fromActorId)
|
||||
const toUserId = await this.ensureUser(toActorId)
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
RemoveUserFollowedBy(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) {
|
||||
from { name }
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
debug(`undoFollowActivity result = ${JSON.stringify(result, null, 2)}`)
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
|
||||
async saveFollowersCollectionPage (followersCollection, onlyNewestItem = true) {
|
||||
debug('inside saveFollowers')
|
||||
let orderedItems = followersCollection.orderedItems
|
||||
const toUserName = extractNameFromId(followersCollection.id)
|
||||
const toUserId = await this.ensureUser(constructIdFromName(toUserName))
|
||||
orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems
|
||||
|
||||
return Promise.all(
|
||||
orderedItems.map(async (follower) => {
|
||||
debug(`follower = ${follower}`)
|
||||
const fromUserId = await this.ensureUser(follower)
|
||||
debug(`fromUserId = ${fromUserId}`)
|
||||
debug(`toUserId = ${toUserId}`)
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
AddUserFollowedBy(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) {
|
||||
from { name }
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
debug(`addUserFollowedBy edge = ${JSON.stringify(result, null, 2)}`)
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
debug('saveFollowers: added follow edge successfully')
|
||||
})
|
||||
)
|
||||
}
|
||||
async saveFollowingCollectionPage (followingCollection, onlyNewestItem = true) {
|
||||
debug('inside saveFollowers')
|
||||
let orderedItems = followingCollection.orderedItems
|
||||
const fromUserName = extractNameFromId(followingCollection.id)
|
||||
const fromUserId = await this.ensureUser(constructIdFromName(fromUserName))
|
||||
orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems
|
||||
return Promise.all(
|
||||
orderedItems.map(async (following) => {
|
||||
debug(`follower = ${following}`)
|
||||
const toUserId = await this.ensureUser(following)
|
||||
debug(`fromUserId = ${fromUserId}`)
|
||||
debug(`toUserId = ${toUserId}`)
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
AddUserFollowing(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) {
|
||||
from { name }
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
debug(`addUserFollowing edge = ${JSON.stringify(result, null, 2)}`)
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
debug('saveFollowing: added follow edge successfully')
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async createPost (activity) {
|
||||
// TODO how to handle the to field? Now the post is just created, doesn't matter who is the recipient
|
||||
// createPost
|
||||
const postObject = activity.object
|
||||
if (!isPublicAddressed(postObject)) {
|
||||
return debug('createPost: not send to public (sending to specific persons is not implemented yet)')
|
||||
}
|
||||
const title = postObject.summary ? postObject.summary : postObject.content.split(' ').slice(0, 5).join(' ')
|
||||
const postId = extractIdFromActivityId(postObject.id)
|
||||
debug('inside create post')
|
||||
let result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
CreatePost(content: "${postObject.content}", contentExcerpt: "${trunc(postObject.content, 120)}", title: "${title}", id: "${postId}", objectId: "${postObject.id}", activityId: "${activity.id}") {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
|
||||
// ensure user and add author to post
|
||||
const userId = await this.ensureUser(postObject.attributedTo)
|
||||
debug(`userId = ${userId}`)
|
||||
debug(`postId = ${postId}`)
|
||||
result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
AddPostAuthor(from: {id: "${userId}"}, to: {id: "${postId}"}) {
|
||||
from {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
|
||||
async deletePost (activity) {
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
DeletePost(id: "${extractIdFromActivityId(activity.object.id)}") {
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
|
||||
async updatePost (activity) {
|
||||
const postObject = activity.object
|
||||
const postId = extractIdFromActivityId(postObject.id)
|
||||
const date = postObject.updated ? postObject.updated : new Date().toISOString()
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
UpdatePost(content: "${postObject.content}", contentExcerpt: "${trunc(postObject.content, 120).html}", id: "${postId}", updatedAt: "${date}") {
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
|
||||
async createShouted (activity) {
|
||||
const userId = await this.ensureUser(activity.actor)
|
||||
const postId = extractIdFromActivityId(activity.object)
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
AddUserShouted(from: {id: "${userId}"}, to: {id: "${postId}"}) {
|
||||
from {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
if (!result.data.AddUserShouted) {
|
||||
debug('something went wrong shouting post')
|
||||
throw Error('User or Post not exists')
|
||||
}
|
||||
}
|
||||
|
||||
async deleteShouted (activity) {
|
||||
const userId = await this.ensureUser(activity.actor)
|
||||
const postId = extractIdFromActivityId(activity.object)
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
RemoveUserShouted(from: {id: "${userId}"}, to: {id: "${postId}"}) {
|
||||
from {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
if (!result.data.AddUserShouted) {
|
||||
debug('something went wrong disliking a post')
|
||||
throw Error('User or Post not exists')
|
||||
}
|
||||
}
|
||||
|
||||
async getSharedInboxEndpoints () {
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
SharedInboxEndpoint {
|
||||
uri
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
return result.data.SharedInboxEnpoint
|
||||
}
|
||||
async addSharedInboxEndpoint (uri) {
|
||||
try {
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
CreateSharedInboxEndpoint(uri: "${uri}")
|
||||
}
|
||||
`
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async createComment (activity) {
|
||||
const postObject = activity.object
|
||||
let result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
CreateComment(content: "${postObject.content}", activityId: "${extractIdFromActivityId(activity.id)}") {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
|
||||
const toUserId = await this.ensureUser(activity.actor)
|
||||
const result2 = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
AddCommentAuthor(from: {id: "${result.data.CreateComment.id}"}, to: {id: "${toUserId}"}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result2)
|
||||
|
||||
const postId = extractIdFromActivityId(postObject.inReplyTo)
|
||||
result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
AddCommentPost(from: { id: "${result.data.CreateComment.id}", to: { id: "${postId}" }}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will search for user existence and will create a disabled user with a random 16 bytes password when no user is found.
|
||||
*
|
||||
* @param actorId
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async ensureUser (actorId) {
|
||||
debug(`inside ensureUser = ${actorId}`)
|
||||
const name = extractNameFromId(actorId)
|
||||
const queryResult = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${name}") {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
if (queryResult.data && Array.isArray(queryResult.data.User) && queryResult.data.User.length > 0) {
|
||||
debug('ensureUser: user exists.. return id')
|
||||
// user already exists.. return the id
|
||||
return queryResult.data.User[0].id
|
||||
} else {
|
||||
debug('ensureUser: user not exists.. createUser')
|
||||
// user does not exist.. create it
|
||||
const pw = crypto.randomBytes(16).toString('hex')
|
||||
const slug = name.toLowerCase().split(' ').join('-')
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
CreateUser(password: "${pw}", slug:"${slug}", actorId: "${actorId}", name: "${name}", email: "${slug}@test.org") {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
|
||||
return result.data.CreateUser.id
|
||||
}
|
||||
}
|
||||
}
|
||||
54
backend/src/activitypub/routes/inbox.js
Normal file
@ -0,0 +1,54 @@
|
||||
import express from 'express'
|
||||
import { activityPub } from '../ActivityPub'
|
||||
|
||||
const debug = require('debug')('ea:inbox')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// Shared Inbox endpoint (federated Server)
|
||||
// For now its only able to handle Note Activities!!
|
||||
router.post('/', async function (req, res, next) {
|
||||
debug(`Content-Type = ${req.get('Content-Type')}`)
|
||||
debug(`body = ${JSON.stringify(req.body, null, 2)}`)
|
||||
debug(`Request headers = ${JSON.stringify(req.headers, null, 2)}`)
|
||||
switch (req.body.type) {
|
||||
case 'Create':
|
||||
await activityPub.handleCreateActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Undo':
|
||||
await activityPub.handleUndoActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Follow':
|
||||
await activityPub.handleFollowActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Delete':
|
||||
await activityPub.handleDeleteActivity(req.body).catch(next)
|
||||
break
|
||||
/* eslint-disable */
|
||||
case 'Update':
|
||||
await activityPub.handleUpdateActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Accept':
|
||||
await activityPub.handleAcceptActivity(req.body).catch(next)
|
||||
case 'Reject':
|
||||
// Do nothing
|
||||
break
|
||||
case 'Add':
|
||||
break
|
||||
case 'Remove':
|
||||
break
|
||||
case 'Like':
|
||||
await activityPub.handleLikeActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Dislike':
|
||||
await activityPub.handleDislikeActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Announce':
|
||||
debug('else!!')
|
||||
debug(JSON.stringify(req.body, null, 2))
|
||||
}
|
||||
/* eslint-enable */
|
||||
res.status(200).end()
|
||||
})
|
||||
|
||||
export default router
|
||||
29
backend/src/activitypub/routes/index.js
Normal file
@ -0,0 +1,29 @@
|
||||
import user from './user'
|
||||
import inbox from './inbox'
|
||||
import webFinger from './webFinger'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import verify from './verify'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/.well-known/webFinger',
|
||||
cors(),
|
||||
express.urlencoded({ extended: true }),
|
||||
webFinger
|
||||
)
|
||||
router.use('/activitypub/users',
|
||||
cors(),
|
||||
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
|
||||
express.urlencoded({ extended: true }),
|
||||
user
|
||||
)
|
||||
router.use('/activitypub/inbox',
|
||||
cors(),
|
||||
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
|
||||
express.urlencoded({ extended: true }),
|
||||
verify,
|
||||
inbox
|
||||
)
|
||||
|
||||
export default router
|
||||
43
backend/src/activitypub/routes/serveUser.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { createActor } from '../utils/actor'
|
||||
const gql = require('graphql-tag')
|
||||
const debug = require('debug')('ea:serveUser')
|
||||
|
||||
export async function serveUser (req, res, next) {
|
||||
let name = req.params.name
|
||||
|
||||
if (name.startsWith('@')) {
|
||||
name = name.slice(1)
|
||||
}
|
||||
|
||||
debug(`name = ${name}`)
|
||||
const result = await req.app.get('ap').dataSource.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${name}") {
|
||||
publicKey
|
||||
}
|
||||
}
|
||||
`
|
||||
}).catch(reason => { debug(`serveUser User fetch error: ${reason}`) })
|
||||
|
||||
if (result.data && Array.isArray(result.data.User) && result.data.User.length > 0) {
|
||||
const publicKey = result.data.User[0].publicKey
|
||||
const actor = createActor(name, publicKey)
|
||||
debug(`actor = ${JSON.stringify(actor, null, 2)}`)
|
||||
debug(`accepts json = ${req.accepts(['application/activity+json', 'application/ld+json', 'application/json'])}`)
|
||||
if (req.accepts(['application/activity+json', 'application/ld+json', 'application/json'])) {
|
||||
return res.json(actor)
|
||||
} else if (req.accepts('text/html')) {
|
||||
// TODO show user's profile page instead of the actor object
|
||||
/* const outbox = JSON.parse(result.outbox)
|
||||
const posts = outbox.orderedItems.filter((el) => { return el.object.type === 'Note'})
|
||||
const actor = result.actor
|
||||
debug(posts) */
|
||||
// res.render('user', { user: actor, posts: JSON.stringify(posts)})
|
||||
return res.json(actor)
|
||||
}
|
||||
} else {
|
||||
debug(`error getting publicKey for actor ${name}`)
|
||||
next()
|
||||
}
|
||||
}
|
||||
92
backend/src/activitypub/routes/user.js
Normal file
@ -0,0 +1,92 @@
|
||||
import { sendCollection } from '../utils/collection'
|
||||
import express from 'express'
|
||||
import { serveUser } from './serveUser'
|
||||
import { activityPub } from '../ActivityPub'
|
||||
import verify from './verify'
|
||||
|
||||
const router = express.Router()
|
||||
const debug = require('debug')('ea:user')
|
||||
|
||||
router.get('/:name', async function (req, res, next) {
|
||||
debug('inside user.js -> serveUser')
|
||||
await serveUser(req, res, next)
|
||||
})
|
||||
|
||||
router.get('/:name/following', (req, res) => {
|
||||
debug('inside user.js -> serveFollowingCollection')
|
||||
const name = req.params.name
|
||||
if (!name) {
|
||||
res.status(400).send('Bad request! Please specify a name.')
|
||||
} else {
|
||||
const collectionName = req.query.page ? 'followingPage' : 'following'
|
||||
sendCollection(collectionName, req, res)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:name/followers', (req, res) => {
|
||||
debug('inside user.js -> serveFollowersCollection')
|
||||
const name = req.params.name
|
||||
if (!name) {
|
||||
return res.status(400).send('Bad request! Please specify a name.')
|
||||
} else {
|
||||
const collectionName = req.query.page ? 'followersPage' : 'followers'
|
||||
sendCollection(collectionName, req, res)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:name/outbox', (req, res) => {
|
||||
debug('inside user.js -> serveOutboxCollection')
|
||||
const name = req.params.name
|
||||
if (!name) {
|
||||
return res.status(400).send('Bad request! Please specify a name.')
|
||||
} else {
|
||||
const collectionName = req.query.page ? 'outboxPage' : 'outbox'
|
||||
sendCollection(collectionName, req, res)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:name/inbox', verify, async function (req, res, next) {
|
||||
debug(`body = ${JSON.stringify(req.body, null, 2)}`)
|
||||
debug(`actorId = ${req.body.actor}`)
|
||||
// const result = await saveActorId(req.body.actor)
|
||||
switch (req.body.type) {
|
||||
case 'Create':
|
||||
await activityPub.handleCreateActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Undo':
|
||||
await activityPub.handleUndoActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Follow':
|
||||
await activityPub.handleFollowActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Delete':
|
||||
await activityPub.handleDeleteActivity(req.body).catch(next)
|
||||
break
|
||||
/* eslint-disable */
|
||||
case 'Update':
|
||||
await activityPub.handleUpdateActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Accept':
|
||||
await activityPub.handleAcceptActivity(req.body).catch(next)
|
||||
case 'Reject':
|
||||
// Do nothing
|
||||
break
|
||||
case 'Add':
|
||||
break
|
||||
case 'Remove':
|
||||
break
|
||||
case 'Like':
|
||||
await activityPub.handleLikeActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Dislike':
|
||||
await activityPub.handleDislikeActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Announce':
|
||||
debug('else!!')
|
||||
debug(JSON.stringify(req.body, null, 2))
|
||||
}
|
||||
/* eslint-enable */
|
||||
res.status(200).end()
|
||||
})
|
||||
|
||||
export default router
|
||||
15
backend/src/activitypub/routes/verify.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { verifySignature } from '../security'
|
||||
const debug = require('debug')('ea:verify')
|
||||
|
||||
export default async (req, res, next) => {
|
||||
debug(`actorId = ${req.body.actor}`)
|
||||
// TODO stop if signature validation fails
|
||||
if (await verifySignature(`${req.protocol}://${req.hostname}:${req.app.get('port')}${req.originalUrl}`, req.headers)) {
|
||||
debug('verify = true')
|
||||
next()
|
||||
} else {
|
||||
// throw Error('Signature validation failed!')
|
||||
debug('verify = false')
|
||||
next()
|
||||
}
|
||||
}
|
||||
34
backend/src/activitypub/routes/webFinger.js
Normal file
@ -0,0 +1,34 @@
|
||||
import express from 'express'
|
||||
import { createWebFinger } from '../utils/actor'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', async function (req, res) {
|
||||
const resource = req.query.resource
|
||||
if (!resource || !resource.includes('acct:')) {
|
||||
return res.status(400).send('Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.')
|
||||
} else {
|
||||
const nameAndDomain = resource.replace('acct:', '')
|
||||
const name = nameAndDomain.split('@')[0]
|
||||
|
||||
const result = await req.app.get('ap').dataSource.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${name}") {
|
||||
slug
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
if (result.data && result.data.User.length > 0) {
|
||||
const webFinger = createWebFinger(name)
|
||||
return res.contentType('application/jrd+json').json(webFinger)
|
||||
} else {
|
||||
return res.status(404).json({ error: `No record found for ${nameAndDomain}.` })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
84
backend/src/activitypub/security/httpSignature.spec.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { generateRsaKeyPair, createSignature, verifySignature } from '.'
|
||||
import crypto from 'crypto'
|
||||
import request from 'request'
|
||||
jest.mock('request')
|
||||
|
||||
let privateKey
|
||||
let publicKey
|
||||
let headers
|
||||
const passphrase = 'a7dsf78sadg87ad87sfagsadg78'
|
||||
|
||||
describe('activityPub/security', () => {
|
||||
beforeEach(() => {
|
||||
const pair = generateRsaKeyPair({ passphrase })
|
||||
privateKey = pair.privateKey
|
||||
publicKey = pair.publicKey
|
||||
headers = {
|
||||
'Date': '2019-03-08T14:35:45.759Z',
|
||||
'Host': 'democracy-app.de',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
describe('createSignature', () => {
|
||||
describe('returned http signature', () => {
|
||||
let signatureB64
|
||||
let httpSignature
|
||||
|
||||
beforeEach(() => {
|
||||
const signer = crypto.createSign('rsa-sha256')
|
||||
signer.update('(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json')
|
||||
signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64')
|
||||
httpSignature = createSignature({ privateKey, keyId: 'https://human-connection.org/activitypub/users/lea#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase })
|
||||
})
|
||||
|
||||
it('contains keyId', () => {
|
||||
expect(httpSignature).toContain('keyId="https://human-connection.org/activitypub/users/lea#main-key"')
|
||||
})
|
||||
|
||||
it('contains default algorithm "rsa-sha256"', () => {
|
||||
expect(httpSignature).toContain('algorithm="rsa-sha256"')
|
||||
})
|
||||
|
||||
it('contains headers', () => {
|
||||
expect(httpSignature).toContain('headers="(request-target) date host content-type"')
|
||||
})
|
||||
|
||||
it('contains signature', () => {
|
||||
expect(httpSignature).toContain('signature="' + signatureB64 + '"')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('verifySignature', () => {
|
||||
let httpSignature
|
||||
|
||||
beforeEach(() => {
|
||||
httpSignature = createSignature({ privateKey, keyId: 'http://localhost:4001/activitypub/users/test-user#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase })
|
||||
const body = {
|
||||
'publicKey': {
|
||||
'id': 'https://localhost:4001/activitypub/users/test-user#main-key',
|
||||
'owner': 'https://localhost:4001/activitypub/users/test-user',
|
||||
'publicKeyPem': publicKey
|
||||
}
|
||||
}
|
||||
|
||||
const mockedRequest = jest.fn((_, callback) => callback(null, null, JSON.stringify(body)))
|
||||
request.mockImplementation(mockedRequest)
|
||||
})
|
||||
|
||||
it('resolves false', async () => {
|
||||
await expect(verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers)).resolves.toEqual(false)
|
||||
})
|
||||
|
||||
describe('valid signature', () => {
|
||||
beforeEach(() => {
|
||||
headers.Signature = httpSignature
|
||||
})
|
||||
|
||||
it('resolves true', async () => {
|
||||
await expect(verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers)).resolves.toEqual(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
154
backend/src/activitypub/security/index.js
Normal file
@ -0,0 +1,154 @@
|
||||
import dotenv from 'dotenv'
|
||||
import { resolve } from 'path'
|
||||
import crypto from 'crypto'
|
||||
import request from 'request'
|
||||
const debug = require('debug')('ea:security')
|
||||
|
||||
dotenv.config({ path: resolve('src', 'activitypub', '.env') })
|
||||
|
||||
export function generateRsaKeyPair (options = {}) {
|
||||
const { passphrase = process.env.PRIVATE_KEY_PASSPHRASE } = options
|
||||
return crypto.generateKeyPairSync('rsa', {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem'
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem',
|
||||
cipher: 'aes-256-cbc',
|
||||
passphrase
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// signing
|
||||
export function createSignature (options) {
|
||||
const {
|
||||
privateKey, keyId, url,
|
||||
headers = {},
|
||||
algorithm = 'rsa-sha256',
|
||||
passphrase = process.env.PRIVATE_KEY_PASSPHRASE
|
||||
} = options
|
||||
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) }
|
||||
const signer = crypto.createSign(algorithm)
|
||||
const signingString = constructSigningString(url, headers)
|
||||
signer.update(signingString)
|
||||
const signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64')
|
||||
const headersString = Object.keys(headers).reduce((result, key) => { return result + ' ' + key.toLowerCase() }, '')
|
||||
return `keyId="${keyId}",algorithm="${algorithm}",headers="(request-target)${headersString}",signature="${signatureB64}"`
|
||||
}
|
||||
|
||||
// verifying
|
||||
export function verifySignature (url, headers) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const signatureHeader = headers['signature'] ? headers['signature'] : headers['Signature']
|
||||
if (!signatureHeader) {
|
||||
debug('No Signature header present!')
|
||||
resolve(false)
|
||||
}
|
||||
debug(`Signature Header = ${signatureHeader}`)
|
||||
const signature = extractKeyValueFromSignatureHeader(signatureHeader, 'signature')
|
||||
const algorithm = extractKeyValueFromSignatureHeader(signatureHeader, 'algorithm')
|
||||
const headersString = extractKeyValueFromSignatureHeader(signatureHeader, 'headers')
|
||||
const keyId = extractKeyValueFromSignatureHeader(signatureHeader, 'keyId')
|
||||
|
||||
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
|
||||
debug('Unsupported hash algorithm specified!')
|
||||
resolve(false)
|
||||
}
|
||||
|
||||
const usedHeaders = headersString.split(' ')
|
||||
const verifyHeaders = {}
|
||||
Object.keys(headers).forEach((key) => {
|
||||
if (usedHeaders.includes(key.toLowerCase())) {
|
||||
verifyHeaders[key.toLowerCase()] = headers[key]
|
||||
}
|
||||
})
|
||||
const signingString = constructSigningString(url, verifyHeaders)
|
||||
debug(`keyId= ${keyId}`)
|
||||
request({
|
||||
url: keyId,
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}, (err, response, body) => {
|
||||
if (err) reject(err)
|
||||
debug(`body = ${body}`)
|
||||
const actor = JSON.parse(body)
|
||||
const publicKeyPem = actor.publicKey.publicKeyPem
|
||||
resolve(httpVerify(publicKeyPem, signature, signingString, algorithm))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// private: signing
|
||||
function constructSigningString (url, headers) {
|
||||
const urlObj = new URL(url)
|
||||
let signingString = `(request-target): post ${urlObj.pathname}${urlObj.search !== '' ? urlObj.search : ''}`
|
||||
return Object.keys(headers).reduce((result, key) => {
|
||||
return result + `\n${key.toLowerCase()}: ${headers[key]}`
|
||||
}, signingString)
|
||||
}
|
||||
|
||||
// private: verifying
|
||||
function httpVerify (pubKey, signature, signingString, algorithm) {
|
||||
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) }
|
||||
const verifier = crypto.createVerify(algorithm)
|
||||
verifier.update(signingString)
|
||||
return verifier.verify(pubKey, signature, 'base64')
|
||||
}
|
||||
|
||||
// private: verifying
|
||||
// This function can be used to extract the signature,headers,algorithm etc. out of the Signature Header.
|
||||
// Just pass what you want as key
|
||||
function extractKeyValueFromSignatureHeader (signatureHeader, key) {
|
||||
const keyString = signatureHeader.split(',').filter((el) => {
|
||||
return !!el.startsWith(key)
|
||||
})[0]
|
||||
|
||||
let firstEqualIndex = keyString.search('=')
|
||||
// When headers are requested add 17 to the index to remove "(request-target) " from the string
|
||||
if (key === 'headers') { firstEqualIndex += 17 }
|
||||
return keyString.substring(firstEqualIndex + 2, keyString.length - 1)
|
||||
}
|
||||
|
||||
// Obtained from invoking crypto.getHashes()
|
||||
export const SUPPORTED_HASH_ALGORITHMS = [
|
||||
'rsa-md4',
|
||||
'rsa-md5',
|
||||
'rsa-mdC2',
|
||||
'rsa-ripemd160',
|
||||
'rsa-sha1',
|
||||
'rsa-sha1-2',
|
||||
'rsa-sha224',
|
||||
'rsa-sha256',
|
||||
'rsa-sha384',
|
||||
'rsa-sha512',
|
||||
'blake2b512',
|
||||
'blake2s256',
|
||||
'md4',
|
||||
'md4WithRSAEncryption',
|
||||
'md5',
|
||||
'md5-sha1',
|
||||
'md5WithRSAEncryption',
|
||||
'mdc2',
|
||||
'mdc2WithRSA',
|
||||
'ripemd',
|
||||
'ripemd160',
|
||||
'ripemd160WithRSA',
|
||||
'rmd160',
|
||||
'sha1',
|
||||
'sha1WithRSAEncryption',
|
||||
'sha224',
|
||||
'sha224WithRSAEncryption',
|
||||
'sha256',
|
||||
'sha256WithRSAEncryption',
|
||||
'sha384',
|
||||
'sha384WithRSAEncryption',
|
||||
'sha512',
|
||||
'sha512WithRSAEncryption',
|
||||
'ssl3-md5',
|
||||
'ssl3-sha1',
|
||||
'whirlpool']
|
||||
108
backend/src/activitypub/utils/activity.js
Normal file
@ -0,0 +1,108 @@
|
||||
import { activityPub } from '../ActivityPub'
|
||||
import { signAndSend, throwErrorIfApolloErrorOccurred } from './index'
|
||||
|
||||
import crypto from 'crypto'
|
||||
import as from 'activitystrea.ms'
|
||||
import gql from 'graphql-tag'
|
||||
const debug = require('debug')('ea:utils:activity')
|
||||
|
||||
export function createNoteObject (text, name, id, published) {
|
||||
const createUuid = crypto.randomBytes(16).toString('hex')
|
||||
|
||||
return {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${createUuid}`,
|
||||
'type': 'Create',
|
||||
'actor': `https://${activityPub.domain}/activitypub/users/${name}`,
|
||||
'object': {
|
||||
'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${id}`,
|
||||
'type': 'Note',
|
||||
'published': published,
|
||||
'attributedTo': `https://${activityPub.domain}/activitypub/users/${name}`,
|
||||
'content': text,
|
||||
'to': 'https://www.w3.org/ns/activitystreams#Public'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createArticleObject (activityId, objectId, text, name, id, published) {
|
||||
const actorId = await getActorId(name)
|
||||
|
||||
return {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': `${activityId}`,
|
||||
'type': 'Create',
|
||||
'actor': `${actorId}`,
|
||||
'object': {
|
||||
'id': `${objectId}`,
|
||||
'type': 'Article',
|
||||
'published': published,
|
||||
'attributedTo': `${actorId}`,
|
||||
'content': text,
|
||||
'to': 'https://www.w3.org/ns/activitystreams#Public'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getActorId (name) {
|
||||
const result = await activityPub.dataSource.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${name}") {
|
||||
actorId
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
if (Array.isArray(result.data.User) && result.data.User[0]) {
|
||||
return result.data.User[0].actorId
|
||||
} else {
|
||||
throw Error(`No user with name: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function sendAcceptActivity (theBody, name, targetDomain, url) {
|
||||
as.accept()
|
||||
.id(`https://${activityPub.domain}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
|
||||
.actor(`https://${activityPub.domain}/activitypub/users/${name}`)
|
||||
.object(theBody)
|
||||
.prettyWrite((err, doc) => {
|
||||
if (!err) {
|
||||
return signAndSend(doc, name, targetDomain, url)
|
||||
} else {
|
||||
debug(`error serializing Accept object: ${err}`)
|
||||
throw new Error('error serializing Accept object')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function sendRejectActivity (theBody, name, targetDomain, url) {
|
||||
as.reject()
|
||||
.id(`https://${activityPub.domain}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
|
||||
.actor(`https://${activityPub.domain}/activitypub/users/${name}`)
|
||||
.object(theBody)
|
||||
.prettyWrite((err, doc) => {
|
||||
if (!err) {
|
||||
return signAndSend(doc, name, targetDomain, url)
|
||||
} else {
|
||||
debug(`error serializing Accept object: ${err}`)
|
||||
throw new Error('error serializing Accept object')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function isPublicAddressed (postObject) {
|
||||
if (typeof postObject.to === 'string') {
|
||||
postObject.to = [postObject.to]
|
||||
}
|
||||
if (typeof postObject === 'string') {
|
||||
postObject.to = [postObject]
|
||||
}
|
||||
if (Array.isArray(postObject)) {
|
||||
postObject.to = postObject
|
||||
}
|
||||
return postObject.to.includes('Public') ||
|
||||
postObject.to.includes('as:Public') ||
|
||||
postObject.to.includes('https://www.w3.org/ns/activitystreams#Public')
|
||||
}
|
||||
40
backend/src/activitypub/utils/actor.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { activityPub } from '../ActivityPub'
|
||||
|
||||
export function createActor (name, pubkey) {
|
||||
return {
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1'
|
||||
],
|
||||
'id': `https://${activityPub.domain}/activitypub/users/${name}`,
|
||||
'type': 'Person',
|
||||
'preferredUsername': `${name}`,
|
||||
'name': `${name}`,
|
||||
'following': `https://${activityPub.domain}/activitypub/users/${name}/following`,
|
||||
'followers': `https://${activityPub.domain}/activitypub/users/${name}/followers`,
|
||||
'inbox': `https://${activityPub.domain}/activitypub/users/${name}/inbox`,
|
||||
'outbox': `https://${activityPub.domain}/activitypub/users/${name}/outbox`,
|
||||
'url': `https://${activityPub.domain}/activitypub/@${name}`,
|
||||
'endpoints': {
|
||||
'sharedInbox': `https://${activityPub.domain}/activitypub/inbox`
|
||||
},
|
||||
'publicKey': {
|
||||
'id': `https://${activityPub.domain}/activitypub/users/${name}#main-key`,
|
||||
'owner': `https://${activityPub.domain}/activitypub/users/${name}`,
|
||||
'publicKeyPem': pubkey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createWebFinger (name) {
|
||||
return {
|
||||
'subject': `acct:${name}@${activityPub.domain}`,
|
||||
'links': [
|
||||
{
|
||||
'rel': 'self',
|
||||
'type': 'application/activity+json',
|
||||
'href': `https://${activityPub.domain}/users/${name}`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
70
backend/src/activitypub/utils/collection.js
Normal file
@ -0,0 +1,70 @@
|
||||
import { activityPub } from '../ActivityPub'
|
||||
import { constructIdFromName } from './index'
|
||||
const debug = require('debug')('ea:utils:collections')
|
||||
|
||||
export function createOrderedCollection (name, collectionName) {
|
||||
return {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}`,
|
||||
'summary': `${name}s ${collectionName} collection`,
|
||||
'type': 'OrderedCollection',
|
||||
'first': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}?page=true`,
|
||||
'totalItems': 0
|
||||
}
|
||||
}
|
||||
|
||||
export function createOrderedCollectionPage (name, collectionName) {
|
||||
return {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}?page=true`,
|
||||
'summary': `${name}s ${collectionName} collection`,
|
||||
'type': 'OrderedCollectionPage',
|
||||
'totalItems': 0,
|
||||
'partOf': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}`,
|
||||
'orderedItems': []
|
||||
}
|
||||
}
|
||||
export function sendCollection (collectionName, req, res) {
|
||||
const name = req.params.name
|
||||
const id = constructIdFromName(name)
|
||||
|
||||
switch (collectionName) {
|
||||
case 'followers':
|
||||
attachThenCatch(activityPub.collections.getFollowersCollection(id), res)
|
||||
break
|
||||
|
||||
case 'followersPage':
|
||||
attachThenCatch(activityPub.collections.getFollowersCollectionPage(id), res)
|
||||
break
|
||||
|
||||
case 'following':
|
||||
attachThenCatch(activityPub.collections.getFollowingCollection(id), res)
|
||||
break
|
||||
|
||||
case 'followingPage':
|
||||
attachThenCatch(activityPub.collections.getFollowingCollectionPage(id), res)
|
||||
break
|
||||
|
||||
case 'outbox':
|
||||
attachThenCatch(activityPub.collections.getOutboxCollection(id), res)
|
||||
break
|
||||
|
||||
case 'outboxPage':
|
||||
attachThenCatch(activityPub.collections.getOutboxCollectionPage(id), res)
|
||||
break
|
||||
|
||||
default:
|
||||
res.status(500).end()
|
||||
}
|
||||
}
|
||||
|
||||
function attachThenCatch (promise, res) {
|
||||
return promise
|
||||
.then((collection) => {
|
||||
res.status(200).contentType('application/activity+json').send(collection)
|
||||
})
|
||||
.catch((err) => {
|
||||
debug(`error getting a Collection: = ${err}`)
|
||||
res.status(500).end()
|
||||
})
|
||||
}
|
||||
103
backend/src/activitypub/utils/index.js
Normal file
@ -0,0 +1,103 @@
|
||||
import { activityPub } from '../ActivityPub'
|
||||
import gql from 'graphql-tag'
|
||||
import { createSignature } from '../security'
|
||||
import request from 'request'
|
||||
const debug = require('debug')('ea:utils')
|
||||
|
||||
export function extractNameFromId (uri) {
|
||||
const urlObject = new URL(uri)
|
||||
const pathname = urlObject.pathname
|
||||
const splitted = pathname.split('/')
|
||||
|
||||
return splitted[splitted.indexOf('users') + 1]
|
||||
}
|
||||
|
||||
export function extractIdFromActivityId (uri) {
|
||||
const urlObject = new URL(uri)
|
||||
const pathname = urlObject.pathname
|
||||
const splitted = pathname.split('/')
|
||||
|
||||
return splitted[splitted.indexOf('status') + 1]
|
||||
}
|
||||
|
||||
export function constructIdFromName (name, fromDomain = activityPub.domain) {
|
||||
return `http://${fromDomain}/activitypub/users/${name}`
|
||||
}
|
||||
|
||||
export function extractDomainFromUrl (url) {
|
||||
return new URL(url).hostname
|
||||
}
|
||||
|
||||
export function throwErrorIfApolloErrorOccurred (result) {
|
||||
if (result.error && (result.error.message || result.error.errors)) {
|
||||
throw new Error(`${result.error.message ? result.error.message : result.error.errors[0].message}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function signAndSend (activity, fromName, targetDomain, url) {
|
||||
// fix for development: replace with http
|
||||
url = url.indexOf('localhost') > -1 ? url.replace('https', 'http') : url
|
||||
debug(`passhprase = ${process.env.PRIVATE_KEY_PASSPHRASE}`)
|
||||
return new Promise(async (resolve, reject) => {
|
||||
debug('inside signAndSend')
|
||||
// get the private key
|
||||
const result = await activityPub.dataSource.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${fromName}") {
|
||||
privateKey
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
reject(result.error)
|
||||
} else {
|
||||
// add security context
|
||||
const parsedActivity = JSON.parse(activity)
|
||||
if (Array.isArray(parsedActivity['@context'])) {
|
||||
parsedActivity['@context'].push('https://w3id.org/security/v1')
|
||||
} else {
|
||||
const context = [parsedActivity['@context']]
|
||||
context.push('https://w3id.org/security/v1')
|
||||
parsedActivity['@context'] = context
|
||||
}
|
||||
|
||||
// deduplicate context strings
|
||||
parsedActivity['@context'] = [...new Set(parsedActivity['@context'])]
|
||||
const privateKey = result.data.User[0].privateKey
|
||||
const date = new Date().toUTCString()
|
||||
|
||||
debug(`url = ${url}`)
|
||||
request({
|
||||
url: url,
|
||||
headers: {
|
||||
'Host': targetDomain,
|
||||
'Date': date,
|
||||
'Signature': createSignature({ privateKey,
|
||||
keyId: `http://${activityPub.domain}/activitypub/users/${fromName}#main-key`,
|
||||
url,
|
||||
headers: {
|
||||
'Host': targetDomain,
|
||||
'Date': date,
|
||||
'Content-Type': 'application/activity+json'
|
||||
}
|
||||
}),
|
||||
'Content-Type': 'application/activity+json'
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(parsedActivity)
|
||||
}, (error, response) => {
|
||||
if (error) {
|
||||
debug(`Error = ${JSON.stringify(error, null, 2)}`)
|
||||
reject(error)
|
||||
} else {
|
||||
debug('Response Headers:', JSON.stringify(response.headers, null, 2))
|
||||
debug('Response Body:', JSON.stringify(response.body, null, 2))
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
16
backend/src/bootstrap/directives.js
Normal file
@ -0,0 +1,16 @@
|
||||
import {
|
||||
GraphQLLowerCaseDirective,
|
||||
GraphQLTrimDirective,
|
||||
GraphQLDefaultToDirective
|
||||
} from 'graphql-custom-directives'
|
||||
|
||||
export default function applyDirectives (augmentedSchema) {
|
||||
const directives = [
|
||||
GraphQLLowerCaseDirective,
|
||||
GraphQLTrimDirective,
|
||||
GraphQLDefaultToDirective
|
||||
]
|
||||
augmentedSchema._directives.push.apply(augmentedSchema._directives, directives)
|
||||
|
||||
return augmentedSchema
|
||||
}
|
||||
18
backend/src/bootstrap/neo4j.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { v1 as neo4j } from 'neo4j-driver'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
let driver
|
||||
|
||||
export function getDriver (options = {}) {
|
||||
const {
|
||||
uri = process.env.NEO4J_URI || 'bolt://localhost:7687',
|
||||
username = process.env.NEO4J_USERNAME || 'neo4j',
|
||||
password = process.env.NEO4J_PASSWORD || 'neo4j'
|
||||
} = options
|
||||
if (!driver) {
|
||||
driver = neo4j.driver(uri, neo4j.auth.basic(username, password))
|
||||
}
|
||||
return driver
|
||||
}
|
||||
13
backend/src/bootstrap/scalars.js
Normal file
@ -0,0 +1,13 @@
|
||||
import {
|
||||
GraphQLDate,
|
||||
GraphQLTime,
|
||||
GraphQLDateTime
|
||||
} from 'graphql-iso-date'
|
||||
|
||||
export default function applyScalars (augmentedSchema) {
|
||||
augmentedSchema._typeMap.Date = GraphQLDate
|
||||
augmentedSchema._typeMap.Time = GraphQLTime
|
||||
augmentedSchema._typeMap.DateTime = GraphQLDateTime
|
||||
|
||||
return augmentedSchema
|
||||
}
|
||||
27
backend/src/graphql-schema.js
Normal file
@ -0,0 +1,27 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import userManagement from './resolvers/user_management.js'
|
||||
import statistics from './resolvers/statistics.js'
|
||||
import reports from './resolvers/reports.js'
|
||||
import posts from './resolvers/posts.js'
|
||||
import moderation from './resolvers/moderation.js'
|
||||
|
||||
export const typeDefs = fs
|
||||
.readFileSync(
|
||||
process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql')
|
||||
)
|
||||
.toString('utf-8')
|
||||
|
||||
export const resolvers = {
|
||||
Query: {
|
||||
...statistics.Query,
|
||||
...userManagement.Query
|
||||
},
|
||||
Mutation: {
|
||||
...userManagement.Mutation,
|
||||
...reports.Mutation,
|
||||
...moderation.Mutation,
|
||||
...posts.Mutation
|
||||
}
|
||||
}
|
||||
14
backend/src/helpers/asyncForEach.js
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Provide a way to iterate for each element in an array while waiting for async functions to finish
|
||||
*
|
||||
* @param array
|
||||
* @param callback
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function asyncForEach (array, callback) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array)
|
||||
}
|
||||
}
|
||||
|
||||
export default asyncForEach
|
||||
28
backend/src/helpers/walkRecursive.js
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* iterate through all fields and replace it with the callback result
|
||||
* @property data Array
|
||||
* @property fields Array
|
||||
* @property callback Function
|
||||
*/
|
||||
function walkRecursive (data, fields, callback, _key) {
|
||||
if (!Array.isArray(fields)) {
|
||||
throw new Error('please provide an fields array for the walkRecursive helper')
|
||||
}
|
||||
if (data && typeof data === 'string' && fields.includes(_key)) {
|
||||
// well we found what we searched for, lets replace the value with our callback result
|
||||
data = callback(data, _key)
|
||||
} else if (data && Array.isArray(data)) {
|
||||
// go into the rabbit hole and dig through that array
|
||||
data.forEach((res, index) => {
|
||||
data[index] = walkRecursive(data[index], fields, callback, index)
|
||||
})
|
||||
} else if (data && typeof data === 'object') {
|
||||
// lets get some keys and stir them
|
||||
Object.keys(data).forEach(k => {
|
||||
data[k] = walkRecursive(data[k], fields, callback, k)
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
export default walkRecursive
|
||||
17
backend/src/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
import createServer from './server'
|
||||
import ActivityPub from './activitypub/ActivityPub'
|
||||
|
||||
const serverConfig = {
|
||||
port: process.env.GRAPHQL_PORT || 4000
|
||||
// cors: {
|
||||
// credentials: true,
|
||||
// origin: [process.env.CLIENT_URI] // your frontend url.
|
||||
// }
|
||||
}
|
||||
|
||||
const server = createServer()
|
||||
server.start(serverConfig, options => {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.log(`Server ready at ${process.env.GRAPHQL_URI} 🚀`)
|
||||
ActivityPub.init(server)
|
||||
})
|
||||
16
backend/src/jest/helpers.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { request } from 'graphql-request'
|
||||
|
||||
// this is the to-be-tested server host
|
||||
// not to be confused with the seeder host
|
||||
export const host = 'http://127.0.0.1:4123'
|
||||
|
||||
export async function login ({ email, password }) {
|
||||
const mutation = `
|
||||
mutation {
|
||||
login(email:"${email}", password:"${password}")
|
||||
}`
|
||||
const response = await request(host, mutation)
|
||||
return {
|
||||
authorization: `Bearer ${response.login}`
|
||||
}
|
||||
}
|
||||
30
backend/src/jwt/decode.js
Normal file
@ -0,0 +1,30 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
export default async (driver, authorizationHeader) => {
|
||||
if (!authorizationHeader) return null
|
||||
const token = authorizationHeader.replace('Bearer ', '')
|
||||
let id = null
|
||||
try {
|
||||
const decoded = await jwt.verify(token, process.env.JWT_SECRET)
|
||||
id = decoded.sub
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
const session = driver.session()
|
||||
const query = `
|
||||
MATCH (user:User {id: {id} })
|
||||
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
||||
LIMIT 1
|
||||
`
|
||||
const result = await session.run(query, { id })
|
||||
session.close()
|
||||
const [currentUser] = await result.records.map((record) => {
|
||||
return record.get('user')
|
||||
})
|
||||
if (!currentUser) return null
|
||||
if (currentUser.disabled) return null
|
||||
return {
|
||||
token,
|
||||
...currentUser
|
||||
}
|
||||
}
|
||||
17
backend/src/jwt/encode.js
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
import jwt from 'jsonwebtoken'
|
||||
import ms from 'ms'
|
||||
|
||||
// Generate an Access Token for the given User ID
|
||||
export default function encode (user) {
|
||||
const token = jwt.sign(user, process.env.JWT_SECRET, {
|
||||
expiresIn: ms('1d'),
|
||||
issuer: process.env.GRAPHQL_URI,
|
||||
audience: process.env.CLIENT_URI,
|
||||
subject: user.id.toString()
|
||||
})
|
||||
// jwt.verifySignature(token, process.env.JWT_SECRET, (err, data) => {
|
||||
// console.log('token verification:', err, data)
|
||||
// })
|
||||
return token
|
||||
}
|
||||
56
backend/src/middleware/activityPubMiddleware.js
Normal file
@ -0,0 +1,56 @@
|
||||
import { generateRsaKeyPair } from '../activitypub/security'
|
||||
import { activityPub } from '../activitypub/ActivityPub'
|
||||
import as from 'activitystrea.ms'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
const debug = require('debug')('backend:schema')
|
||||
dotenv.config()
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
args.activityId = activityPub.generateStatusId(context.user.slug)
|
||||
args.objectId = activityPub.generateStatusId(context.user.slug)
|
||||
|
||||
const post = await resolve(root, args, context, info)
|
||||
|
||||
const { user: author } = context
|
||||
const actorId = author.actorId
|
||||
debug(`actorId = ${actorId}`)
|
||||
const createActivity = await new Promise((resolve, reject) => {
|
||||
as.create()
|
||||
.id(`${actorId}/status/${args.activityId}`)
|
||||
.actor(`${actorId}`)
|
||||
.object(
|
||||
as.article()
|
||||
.id(`${actorId}/status/${post.id}`)
|
||||
.content(post.content)
|
||||
.to('https://www.w3.org/ns/activitystreams#Public')
|
||||
.publishedNow()
|
||||
.attributedTo(`${actorId}`)
|
||||
).prettyWrite((err, doc) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
debug(doc)
|
||||
const parsedDoc = JSON.parse(doc)
|
||||
parsedDoc.send = true
|
||||
resolve(JSON.stringify(parsedDoc))
|
||||
}
|
||||
})
|
||||
})
|
||||
try {
|
||||
await activityPub.sendActivity(createActivity)
|
||||
} catch (e) {
|
||||
debug(`error sending post activity\n${e}`)
|
||||
}
|
||||
return post
|
||||
},
|
||||
CreateUser: async (resolve, root, args, context, info) => {
|
||||
const keys = generateRsaKeyPair()
|
||||
Object.assign(args, keys)
|
||||
args.actorId = `${process.env.GRAPHQL_URI}/activitypub/users/${args.slug}`
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
44
backend/src/middleware/dateTimeMiddleware.js
Normal file
@ -0,0 +1,44 @@
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateUser: async (resolve, root, args, context, info) => {
|
||||
args.createdAt = (new Date()).toISOString()
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
args.createdAt = (new Date()).toISOString()
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
CreateComment: async (resolve, root, args, context, info) => {
|
||||
args.createdAt = (new Date()).toISOString()
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
CreateOrganization: async (resolve, root, args, context, info) => {
|
||||
args.createdAt = (new Date()).toISOString()
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
UpdateUser: async (resolve, root, args, context, info) => {
|
||||
args.updatedAt = (new Date()).toISOString()
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
UpdatePost: async (resolve, root, args, context, info) => {
|
||||
args.updatedAt = (new Date()).toISOString()
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
UpdateComment: async (resolve, root, args, context, info) => {
|
||||
args.updatedAt = (new Date()).toISOString()
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
UpdateOrganization: async (resolve, root, args, context, info) => {
|
||||
args.updatedAt = (new Date()).toISOString()
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
36
backend/src/middleware/excerptMiddleware.js
Normal file
@ -0,0 +1,36 @@
|
||||
import trunc from 'trunc-html'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 120).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
UpdatePost: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 120).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
CreateComment: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 180).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
UpdateComment: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 180).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
CreateOrganization: async (resolve, root, args, context, info) => {
|
||||
args.descriptionExcerpt = trunc(args.description, 120).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
},
|
||||
UpdateOrganization: async (resolve, root, args, context, info) => {
|
||||
args.descriptionExcerpt = trunc(args.description, 120).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||