Merge branch 'master' into dependabot/docker/backend/neo4j/neo4j-3.5.3

This commit is contained in:
Ulf Gebhardt 2019-04-05 20:01:07 +02:00 committed by GitHub
commit 63a646c6b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 2923 additions and 1739 deletions

11
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,11 @@
<!--
Please take a look at the issue templates at https://github.com/Human-Connection/Human-Connection/issues/new/choose
before submitting a new issue. Following one of the issue templates will ensure maintainers can route your request efficiently.
Thanks!
-->
## Issue
<!-- Describe your Issue in detail. -->
<!-- Attach screenshots and drawings if needed. -->

View File

@ -1,35 +1,31 @@
---
name: Bug report
name: 🐛 Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
## :bug: Bugreport
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the bug is.-->
**To Reproduce**
Steps to reproduce the behavior:
1. Go to 'http...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
### Steps to reproduce the behavior
1.
2.
3.
4. ...
5. Profit
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
### Expected behavior
<!-- A clear and concise description of what you expected to happen. -->
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
### Version & Environment
Type: [] <!-- [Desktop|Smartphone] -->
- OS: [] <!-- [e.g. iOS8.1 or Windows] -->
- Browser: [] <!-- [e.g. stock browser, safari, chrome] -->
- Version [] <!-- [e.g. 22] -->
- Device: [] <!-- [e.g. iPhone6] -->
### Additional context
<!-- Add any other context about the problem here. -->

View File

@ -1,17 +1,26 @@
---
name: Feature request
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 [...]
## :rocket: Feature
<!-- Describe the Feature. -->
**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.
### 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 [...] -->
**Additional context**
Add any other context or screenshots about the feature request here.
### Describe the prefered solution and alternatives you've considered
<!-- A clear and concise description of what you want to happen.
Are there any alternative solutions or features you've considered? -->
### Design & Layout
<!-- Attach Screenshots and Drawings. -->
### Additional context
<!-- Add any other context or screenshots about the feature request here.-->

10
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: 💬 Question
about: If you need help understanding HumanConnection.
---
<!-- Chat with Team HumanConnection -->
<!-- If you need an answer right away, visit the HumanConnection Discord:
https://discord.gg/Q3mpcgr -->
## :speech_balloon: Question
<!-- Describe your Question in detail. Include screenshots and drawings if needed. -->

28
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,28 @@
## Pullrequest
<!-- Describe the Pullrequest. -->
### Issues
<!-- Which Issues does this fix, which are related?
- fixes #XXX
- relates #XXX
-->
- [X] None
### Checklist
<!-- Anything important to be thought of when deploying?
- [ ] Env-Variables adjustment needed
- [ ] Breaking/critical change
-->
- [X] None
### How2Test
<!-- Give a detailed description how to test your PR and confirm it is working as expected. -->
<!-- Maintainers will check the Tests
- [ ] Test1
- [ ] Test2
-->
- [X] None
### Todo
<!-- In case some parts are still missing, list them here. -->
- [X] None

4
.gitignore vendored
View File

@ -1,5 +1,6 @@
.env
.idea
*.iml
.vscode
.DS_Store
npm-debug.log*
@ -7,8 +8,7 @@ yarn-debug.log*
yarn-error.log*
.yarn-integrity
.eslintcache
/.github
kubeconfig.yaml
node_modules/
cypress/videos

View File

@ -1,13 +1,12 @@
dist: xenial
language: generic
services:
- docker
addons:
chrome: stable
apt:
sources:
- google-chrome
packages:
- google-chrome-stable
- libgconf-2-4
snaps:
- docker
- chromium
before_install:
- yarn global add wait-on
@ -20,16 +19,15 @@ install:
script:
- docker-compose exec backend yarn run lint
- docker-compose exec backend yarn run test --ci
- docker-compose exec backend yarn run test:jest --ci --verbose=false
- 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
- docker-compose exec webapp yarn run lint
- docker-compose exec webapp yarn run test --ci
- docker-compose exec -d backend yarn run test:cypress
- docker-compose exec webapp yarn run test --ci --verbose=false
- docker-compose exec -d backend yarn run test:before:seeder
- yarn run cypress:run --record --key $CYPRESS_TOKEN
after_success:
@ -47,8 +45,15 @@ after_failure:
- chmod +x send.sh
- ./send.sh failure $WEBHOOK_URL
before_deploy:
- ./scripts/setup_kubernetes.sh
deploy:
- provider: script
script: scripts/docker_push.sh
on:
branch: master
- provider: script
script: scripts/deploy.sh
on:
branch: master

View File

@ -34,6 +34,13 @@ Connect with other developers over [Discord](https://discord.gg/6ub73U3)
## Quick Start
### Requirements
Node >= `v10.12.0`
```
node --version
```
### 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`
@ -63,6 +70,17 @@ docker-compose down -v
Install dependencies:
Download [Neo4j Community Edition](https://neo4j.com/download-center/#releases) and unpack the files.
Download [Neo4j Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases) and drop the file into the `plugins` folder of the just extracted Neo4j-Server
Start Neo4j
```
neo4j\bin\neo4j start
```
and confirm it's running [here](http://localhost:7474)
```bash
yarn install
# -or-

View File

@ -3,29 +3,25 @@
"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": "run-s test:jest test:cucumber",
"test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 yarn run dev 2> /dev/null",
"test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions,activityPub yarn run dev",
"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:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:server test:cucumber:before:seeder 'test:cucumber:cmd {@}' --",
"test:cucumber:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions yarn run dev",
"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"
"db:seed": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions run-p --race dev db:script:seed"
},
"author": "Human Connection gGmbH",
"license": "MIT",
@ -40,7 +36,7 @@
"apollo-cache-inmemory": "~1.5.1",
"apollo-client": "~2.5.1",
"apollo-link-context": "~1.0.14",
"apollo-link-http": "~1.5.13",
"apollo-link-http": "~1.5.14",
"apollo-server": "~2.4.8",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.2",
@ -51,14 +47,14 @@
"dotenv": "~7.0.0",
"express": "~4.16.4",
"faker": "~4.1.0",
"graphql": "~14.1.1",
"graphql": "~14.2.1",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.2",
"graphql-shield": "~5.3.0",
"graphql-shield": "~5.3.1",
"graphql-tag": "~2.10.1",
"graphql-yoga": "~1.17.4",
"helmet": "~3.15.1",
"helmet": "~3.16.0",
"jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.11",
@ -69,35 +65,34 @@
"npm-run-all": "~4.1.5",
"request": "~2.88.0",
"sanitize-html": "~1.20.0",
"slug": "~1.0.0",
"slug": "~1.1.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/core": "~7.4.3",
"@babel/node": "~7.2.2",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.3.4",
"@babel/register": "~7.0.0",
"@babel/preset-env": "~7.4.3",
"@babel/register": "~7.4.0",
"apollo-server-testing": "~2.4.8",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.1",
"babel-jest": "~24.5.0",
"babel-jest": "~24.7.1",
"chai": "~4.2.0",
"cucumber": "~5.1.0",
"eslint": "~5.15.1",
"eslint": "~5.16.0",
"eslint-config-standard": "~12.0.0",
"eslint-plugin-import": "~2.16.0",
"eslint-plugin-jest": "~22.3.2",
"eslint-plugin-jest": "~22.4.1",
"eslint-plugin-node": "~8.0.1",
"eslint-plugin-promise": "~4.0.1",
"eslint-plugin-promise": "~4.1.1",
"eslint-plugin-standard": "~4.0.0",
"graphql-request": "~1.8.2",
"jest": "~24.5.0",
"jest": "~24.7.1",
"nodemon": "~1.18.10",
"nyc": "~13.3.0",
"supertest": "~4.0.0"
"supertest": "~4.0.2"
}
}

View File

@ -22,22 +22,19 @@ 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)
constructor (activityPubEndpointUri, internalGraphQlUri) {
this.endpoint = activityPubEndpointUri
this.dataSource = new NitroDataSource(internalGraphQlUri)
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)
activityPub = new ActivityPub(process.env.CLIENT_URI || 'http://localhost:3000', process.env.GRAPHQL_URI || 'http://localhost:4000')
// 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 {
@ -59,7 +56,6 @@ export default class ActivityPub {
}
}, 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)
@ -184,7 +180,7 @@ export default class ActivityPub {
}
generateStatusId (slug) {
return `http://${this.domain}/activitypub/users/${slug}/status/${uuid()}`
return `https://${this.host}/activitypub/users/${slug}/status/${uuid()}`
}
async sendActivity (activity) {

View File

@ -12,15 +12,20 @@ router.get('/', async function (req, res) {
const nameAndDomain = resource.replace('acct:', '')
const name = nameAndDomain.split('@')[0]
const result = await req.app.get('ap').dataSource.client.query({
query: gql`
let result
try {
result = await req.app.get('ap').dataSource.client.query({
query: gql`
query {
User(slug: "${name}") {
slug
}
}
`
})
})
} catch (error) {
return res.status(500).json({ error })
}
if (result.data && result.data.User.length > 0) {
const webFinger = createWebFinger(name)

View File

@ -11,14 +11,14 @@ export function createNoteObject (text, name, id, published) {
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${createUuid}`,
'id': `${activityPub.endpoint}/activitypub/users/${name}/status/${createUuid}`,
'type': 'Create',
'actor': `https://${activityPub.domain}/activitypub/users/${name}`,
'actor': `${activityPub.endpoint}/activitypub/users/${name}`,
'object': {
'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${id}`,
'id': `${activityPub.endpoint}/activitypub/users/${name}/status/${id}`,
'type': 'Note',
'published': published,
'attributedTo': `https://${activityPub.domain}/activitypub/users/${name}`,
'attributedTo': `${activityPub.endpoint}/activitypub/users/${name}`,
'content': text,
'to': 'https://www.w3.org/ns/activitystreams#Public'
}
@ -64,8 +64,8 @@ export async function getActorId (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}`)
.id(`${activityPub.endpoint}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
.object(theBody)
.prettyWrite((err, doc) => {
if (!err) {
@ -79,8 +79,8 @@ export function sendAcceptActivity (theBody, name, targetDomain, url) {
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}`)
.id(`${activityPub.endpoint}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
.object(theBody)
.prettyWrite((err, doc) => {
if (!err) {

View File

@ -6,34 +6,35 @@ export function createActor (name, pubkey) {
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1'
],
'id': `https://${activityPub.domain}/activitypub/users/${name}`,
'id': `${activityPub.endpoint}/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}`,
'following': `${activityPub.endpoint}/activitypub/users/${name}/following`,
'followers': `${activityPub.endpoint}/activitypub/users/${name}/followers`,
'inbox': `${activityPub.endpoint}/activitypub/users/${name}/inbox`,
'outbox': `${activityPub.endpoint}/activitypub/users/${name}/outbox`,
'url': `${activityPub.endpoint}/activitypub/@${name}`,
'endpoints': {
'sharedInbox': `https://${activityPub.domain}/activitypub/inbox`
'sharedInbox': `${activityPub.endpoint}/activitypub/inbox`
},
'publicKey': {
'id': `https://${activityPub.domain}/activitypub/users/${name}#main-key`,
'owner': `https://${activityPub.domain}/activitypub/users/${name}`,
'id': `${activityPub.endpoint}/activitypub/users/${name}#main-key`,
'owner': `${activityPub.endpoint}/activitypub/users/${name}`,
'publicKeyPem': pubkey
}
}
}
export function createWebFinger (name) {
const { host } = new URL(activityPub.endpoint)
return {
'subject': `acct:${name}@${activityPub.domain}`,
'subject': `acct:${name}@${host}`,
'links': [
{
'rel': 'self',
'type': 'application/activity+json',
'href': `https://${activityPub.domain}/users/${name}`
'href': `${activityPub.endpoint}/activitypub/users/${name}`
}
]
}

View File

@ -5,10 +5,10 @@ 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}`,
'id': `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`,
'summary': `${name}s ${collectionName} collection`,
'type': 'OrderedCollection',
'first': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}?page=true`,
'first': `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`,
'totalItems': 0
}
}
@ -16,11 +16,11 @@ export function createOrderedCollection (name, collectionName) {
export function createOrderedCollectionPage (name, collectionName) {
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}?page=true`,
'id': `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`,
'summary': `${name}s ${collectionName} collection`,
'type': 'OrderedCollectionPage',
'totalItems': 0,
'partOf': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}`,
'partOf': `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`,
'orderedItems': []
}
}

View File

@ -19,13 +19,12 @@ export function extractIdFromActivityId (uri) {
return splitted[splitted.indexOf('status') + 1]
}
export function constructIdFromName (name, fromDomain = activityPub.domain) {
return `http://${fromDomain}/activitypub/users/${name}`
export function constructIdFromName (name, fromDomain = activityPub.endpoint) {
return `${fromDomain}/activitypub/users/${name}`
}
export function extractDomainFromUrl (url) {
return new URL(url).hostname
return new URL(url).host
}
export function throwErrorIfApolloErrorOccurred (result) {
@ -76,7 +75,7 @@ export function signAndSend (activity, fromName, targetDomain, url) {
'Host': targetDomain,
'Date': date,
'Signature': createSignature({ privateKey,
keyId: `http://${activityPub.domain}/activitypub/users/${fromName}#main-key`,
keyId: `${activityPub.endpoint}/activitypub/users/${fromName}#main-key`,
url,
headers: {
'Host': targetDomain,

View File

@ -6,6 +6,7 @@ import statistics from './resolvers/statistics.js'
import reports from './resolvers/reports.js'
import posts from './resolvers/posts.js'
import moderation from './resolvers/moderation.js'
import rewards from './resolvers/rewards.js'
export const typeDefs = fs
.readFileSync(
@ -21,7 +22,8 @@ export const resolvers = {
Mutation: {
...userManagement.Mutation,
...reports.Mutation,
...posts.Mutation,
...moderation.Mutation,
...posts.Mutation
...rewards.Mutation
}
}

View File

@ -12,6 +12,6 @@ const serverConfig = {
const server = createServer()
server.start(serverConfig, options => {
/* eslint-disable-next-line no-console */
console.log(`Server ready at ${process.env.GRAPHQL_URI} 🚀`)
console.log(`GraphQLServer ready at ${process.env.GRAPHQL_URI} 🚀`)
ActivityPub.init(server)
})

View File

@ -49,7 +49,7 @@ export default {
CreateUser: async (resolve, root, args, context, info) => {
const keys = generateRsaKeyPair()
Object.assign(args, keys)
args.actorId = `${process.env.GRAPHQL_URI}/activitypub/users/${args.slug}`
args.actorId = `${activityPub.host}/activitypub/users/${args.slug}`
return resolve(root, args, context, info)
}
}

View File

@ -1,44 +1,23 @@
const setCreatedAt = (resolve, root, args, context, info) => {
args.createdAt = (new Date()).toISOString()
return resolve(root, args, context, info)
}
const setUpdatedAt = (resolve, root, args, context, info) => {
args.updatedAt = (new Date()).toISOString()
return resolve(root, args, context, info)
}
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
}
CreateUser: setCreatedAt,
CreatePost: setCreatedAt,
CreateComment: setCreatedAt,
CreateOrganization: setCreatedAt,
CreateNotification: setCreatedAt,
UpdateUser: setUpdatedAt,
UpdatePost: setUpdatedAt,
UpdateComment: setUpdatedAt,
UpdateOrganization: setUpdatedAt,
UpdateNotification: setUpdatedAt
}
}

View File

@ -24,6 +24,6 @@ const includeFieldsRecursively = (includedFields) => {
}
export default {
Query: includeFieldsRecursively(['id', 'disabled', 'deleted']),
Mutation: includeFieldsRecursively(['id', 'disabled', 'deleted'])
Query: includeFieldsRecursively(['id', 'createdAt', 'disabled', 'deleted']),
Mutation: includeFieldsRecursively(['id', 'createdAt', 'disabled', 'deleted'])
}

View File

@ -9,6 +9,7 @@ import xssMiddleware from './xssMiddleware'
import permissionsMiddleware from './permissionsMiddleware'
import userMiddleware from './userMiddleware'
import includedFieldsMiddleware from './includedFieldsMiddleware'
import orderByMiddleware from './orderByMiddleware'
export default schema => {
let middleware = [
@ -20,14 +21,17 @@ export default schema => {
fixImageUrlsMiddleware,
softDeleteMiddleware,
userMiddleware,
includedFieldsMiddleware
includedFieldsMiddleware,
orderByMiddleware
]
// add permisions middleware at the first position (unless we're seeding)
// NOTE: DO NOT SET THE PERMISSION FLAT YOUR SELF
if (process.env.PERMISSIONS !== 'disabled' && process.env.NODE_ENV !== 'production') {
middleware.unshift(activityPubMiddleware)
middleware.unshift(permissionsMiddleware.generate(schema))
if (process.env.NODE_ENV !== 'production') {
const DISABLED_MIDDLEWARES = process.env.DISABLED_MIDDLEWARES || ''
const disabled = DISABLED_MIDDLEWARES.split(',')
if (!disabled.includes('activityPub')) middleware.unshift(activityPubMiddleware)
if (!disabled.includes('permissions')) middleware.unshift(permissionsMiddleware.generate(schema))
}
return middleware
}

View File

@ -0,0 +1,19 @@
import cloneDeep from 'lodash/cloneDeep'
const defaultOrderBy = (resolve, root, args, context, resolveInfo) => {
const copy = cloneDeep(resolveInfo)
const newestFirst = {
kind: 'Argument',
name: { kind: 'Name', value: 'orderBy' },
value: { kind: 'EnumValue', value: 'createdAt_desc' }
}
const [fieldNode] = copy.fieldNodes
if (fieldNode) fieldNode.arguments.push(newestFirst)
return resolve(root, args, context, copy)
}
export default {
Query: {
Post: defaultOrderBy
}
}

View File

@ -0,0 +1,62 @@
import Factory from '../seed/factories'
import { host } from '../jest/helpers'
import { GraphQLClient } from 'graphql-request'
let client
let headers
let query
const factory = Factory()
beforeEach(async () => {
const userParams = { name: 'Author', email: 'author@example.org', password: '1234' }
await factory.create('User', userParams)
await factory.authenticateAs(userParams)
await factory.create('Post', { title: 'first' })
await factory.create('Post', { title: 'second' })
await factory.create('Post', { title: 'third' })
await factory.create('Post', { title: 'last' })
headers = {}
client = new GraphQLClient(host, { headers })
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('Query', () => {
describe('Post', () => {
beforeEach(() => {
query = '{ Post { title } }'
})
describe('orderBy', () => {
it('createdAt descending is default', async () => {
const posts = [
{ title: 'last' },
{ title: 'third' },
{ title: 'second' },
{ title: 'first' }
]
const expected = { Post: posts }
await expect(client.request(query)).resolves.toEqual(expected)
})
describe('(orderBy: createdAt_asc)', () => {
beforeEach(() => {
query = '{ Post(orderBy: createdAt_asc) { title } }'
})
it('orders by createdAt ascending', async () => {
const posts = [
{ title: 'first' },
{ title: 'second' },
{ title: 'third' },
{ title: 'last' }
]
const expected = { Post: posts }
await expect(client.request(query)).resolves.toEqual(expected)
})
})
})
})
})

View File

@ -44,6 +44,7 @@ const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver
// Permissions
const permissions = shield({
Query: {
Notification: isAdmin,
statistics: allow,
currentUser: allow,
Post: or(onlyEnabledContent, isModerator)
@ -56,6 +57,11 @@ const permissions = shield({
CreateBadge: isAdmin,
UpdateBadge: isAdmin,
DeleteBadge: isAdmin,
AddUserBadges: isAdmin,
// AddBadgeRewarded: isAdmin,
// RemoveBadgeRewarded: isAdmin,
reward: isAdmin,
unreward: isAdmin,
// addFruitToBasket: isAuthenticated
follow: isAuthenticated,
unfollow: isAuthenticated,

View File

@ -0,0 +1,120 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
let client
beforeEach(async () => {
await factory.create('User', {
id: 'you',
email: 'test@example.org',
password: '1234'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('Notification', () => {
const query = `{
Notification {
id
}
}`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(query)).rejects.toThrow('Not Authorised')
})
})
})
describe('currentUser { notifications }', () => {
let variables = {}
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
describe('given some notifications', () => {
beforeEach(async () => {
const neighborParams = {
email: 'neighbor@example.org',
password: '1234',
id: 'neighbor'
}
await Promise.all([
factory.create('User', neighborParams),
factory.create('Notification', { id: 'not-for-you' }),
factory.create('Notification', { id: 'already-seen', read: true })
])
await factory.create('Notification', { id: 'unseen' })
await factory.authenticateAs(neighborParams)
await factory.create('Post', { id: 'p1' })
await Promise.all([
factory.relate('Notification', 'User', { from: 'not-for-you', to: 'neighbor' }),
factory.relate('Notification', 'Post', { from: 'p1', to: 'not-for-you' }),
factory.relate('Notification', 'User', { from: 'unseen', to: 'you' }),
factory.relate('Notification', 'Post', { from: 'p1', to: 'unseen' }),
factory.relate('Notification', 'User', { from: 'already-seen', to: 'you' }),
factory.relate('Notification', 'Post', { from: 'p1', to: 'already-seen' })
])
})
describe('filter for read: false', () => {
const query = `query($read: Boolean) {
currentUser {
notifications(read: $read, orderBy: createdAt_desc) {
id
post {
id
}
}
}
}`
let variables = { read: false }
it('returns only unread notifications of current user', async () => {
const expected = {
currentUser: {
notifications: [
{ id: 'unseen', post: { id: 'p1' } }
]
}
}
await expect(client.request(query, variables)).resolves.toEqual(expected)
})
})
describe('no filters', () => {
const query = `{
currentUser {
notifications(orderBy: createdAt_desc) {
id
post {
id
}
}
}
}`
it('returns all notifications of current user', async () => {
const expected = {
currentUser: {
notifications: [
{ id: 'unseen', post: { id: 'p1' } },
{ id: 'already-seen', post: { id: 'p1' } }
]
}
}
await expect(client.request(query, variables)).resolves.toEqual(expected)
})
})
})
})
})

View File

@ -0,0 +1,47 @@
export default {
Mutation: {
reward: async (_object, params, context, _resolveInfo) => {
const { fromBadgeId, toUserId } = params
const session = context.driver.session()
let sessionRes = await session.run(
`MATCH (badge:Badge {id: $badgeId}), (rewardedUser:User {id: $rewardedUserId})
MERGE (badge)-[:REWARDED]->(rewardedUser)
RETURN rewardedUser {.id}`,
{
badgeId: fromBadgeId,
rewardedUserId: toUserId
}
)
const [rewardedUser] = sessionRes.records.map(record => {
return record.get('rewardedUser')
})
session.close()
return rewardedUser.id
},
unreward: async (_object, params, context, _resolveInfo) => {
const { fromBadgeId, toUserId } = params
const session = context.driver.session()
let sessionRes = await session.run(
`MATCH (badge:Badge {id: $badgeId})-[reward:REWARDED]->(rewardedUser:User {id: $rewardedUserId})
DELETE reward
RETURN rewardedUser {.id}`,
{
badgeId: fromBadgeId,
rewardedUserId: toUserId
}
)
const [rewardedUser] = sessionRes.records.map(record => {
return record.get('rewardedUser')
})
session.close()
return rewardedUser.id
}
}
}

View File

@ -0,0 +1,232 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
describe('rewards', () => {
beforeEach(async () => {
await factory.create('User', {
id: 'u1',
role: 'user',
email: 'user@example.org',
password: '1234'
})
await factory.create('User', {
id: 'u2',
role: 'moderator',
email: 'moderator@example.org'
})
await factory.create('User', {
id: 'u3',
role: 'admin',
email: 'admin@example.org'
})
await factory.create('Badge', {
id: 'b6',
key: 'indiegogo_en_rhino',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_rhino.svg'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('RewardBadge', () => {
const mutation = `
mutation(
$from: ID!
$to: ID!
) {
reward(fromBadgeId: $from, toUserId: $to)
}
`
describe('unauthenticated', () => {
const variables = {
from: 'b6',
to: 'u1'
}
let client
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
})
})
describe('authenticated admin', () => {
let client
beforeEach(async () => {
const headers = await login({ email: 'admin@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('rewards a badge to user', async () => {
const variables = {
from: 'b6',
to: 'u1'
}
const expected = {
reward: 'u1'
}
await expect(
client.request(mutation, variables)
).resolves.toEqual(expected)
})
it('rewards a second different badge to same user', async () => {
await factory.create('Badge', {
id: 'b1',
key: 'indiegogo_en_racoon',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_racoon.svg'
})
const variables = {
from: 'b1',
to: 'u1'
}
const expected = {
reward: 'u1'
}
await expect(
client.request(mutation, variables)
).resolves.toEqual(expected)
})
it('rewards the same badge as well to another user', async () => {
const variables1 = {
from: 'b6',
to: 'u1'
}
await client.request(mutation, variables1)
const variables2 = {
from: 'b6',
to: 'u2'
}
const expected = {
reward: 'u2'
}
await expect(
client.request(mutation, variables2)
).resolves.toEqual(expected)
})
it('returns the original reward if a reward is attempted a second time', async () => {
const variables = {
from: 'b6',
to: 'u1'
}
await client.request(mutation, variables)
await client.request(mutation, variables)
const query = `{
User( id: "u1" ) {
badgesCount
}
}
`
const expected = { User: [{ badgesCount: 1 }] }
await expect(
client.request(query)
).resolves.toEqual(expected)
})
})
describe('authenticated moderator', () => {
const variables = {
from: 'b6',
to: 'u1'
}
let client
beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
describe('rewards bage to user', () => {
it('throws authorization error', async () => {
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
})
})
})
})
describe('RemoveReward', () => {
beforeEach(async () => {
await factory.relate('User', 'Badges', { from: 'b6', to: 'u1' })
})
const variables = {
from: 'b6',
to: 'u1'
}
const expected = {
unreward: 'u1'
}
const mutation = `
mutation(
$from: ID!
$to: ID!
) {
unreward(fromBadgeId: $from, toUserId: $to)
}
`
describe('unauthenticated', () => {
let client
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
})
})
describe('authenticated admin', () => {
let client
beforeEach(async () => {
const headers = await login({ email: 'admin@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('removes a badge from user', async () => {
await expect(
client.request(mutation, variables)
).resolves.toEqual(expected)
})
it('fails to remove a not existing badge from user', async () => {
await client.request(mutation, variables)
await expect(
client.request(mutation, variables)
).rejects.toThrow('Cannot read property \'id\' of undefined')
})
})
describe('authenticated moderator', () => {
let client
beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
describe('removes bage from user', () => {
it('throws authorization error', async () => {
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
})
})
})
})
})

View File

@ -25,6 +25,8 @@ type Mutation {
report(id: ID!, description: String): Report
disable(id: ID!): ID
enable(id: ID!): ID
reward(fromBadgeId: ID!, toUserId: ID!): ID
unreward(fromBadgeId: ID!, toUserId: ID!): ID
"Shout the given Type and ID"
shout(id: ID!, type: ShoutTypeEnum): Boolean! @cypher(statement: """
MATCH (n {id: $id})<-[:WROTE]-(wu:User), (u:User {id: $cypherParams.currentUserId})
@ -67,6 +69,14 @@ type Statistics {
countShouts: Int!
}
type Notification {
id: ID!
read: Boolean,
user: User @relation(name: "NOTIFIED", direction: "OUT")
post: Post @relation(name: "NOTIFIED", direction: "IN")
createdAt: String
}
scalar Date
scalar Time
scalar DateTime
@ -122,6 +132,8 @@ type User {
createdAt: String
updatedAt: String
notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN")
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
@ -269,6 +281,14 @@ enum FollowTypeEnum {
Project
}
type Reward {
id: ID!
user: User @relation(name: "REWARDED", direction: "IN")
rewarderId: ID
createdAt: String
badge: Badge @relation(name: "REWARDED", direction: "OUT")
}
type Organization {
id: ID!
createdBy: User @relation(name: "CREATED_ORGA", direction: "IN")

View File

@ -8,6 +8,7 @@ import createComment from './comments.js'
import createCategory from './categories.js'
import createTag from './tags.js'
import createReport from './reports.js'
import createNotification from './notifications.js'
export const seedServerHost = 'http://127.0.0.1:4001'
@ -29,7 +30,8 @@ const factories = {
Comment: createComment,
Category: createCategory,
Tag: createTag,
Report: createReport
Report: createReport,
Notification: createNotification
}
export const cleanDatabase = async (options = {}) => {

View File

@ -0,0 +1,17 @@
import uuid from 'uuid/v4'
export default function (params) {
const {
id = uuid(),
read = false
} = params
return `
mutation {
CreateNotification(
id: "${id}",
read: ${read},
) { id, read }
}
`
}

View File

@ -4,6 +4,7 @@ import uuid from 'uuid/v4'
export default function (params) {
const {
id = uuid(),
slug = '',
title = faker.lorem.sentence(),
content = [
faker.lorem.sentence(),
@ -21,6 +22,7 @@ export default function (params) {
mutation {
CreatePost(
id: "${id}",
slug: "${slug}",
title: "${title}",
content: "${content}",
image: "${image}",

View File

@ -5,6 +5,7 @@ export default function create (params) {
const {
id = uuid(),
name = faker.name.findName(),
slug = '',
email = faker.internet.email(),
password = '1234',
role = 'user',
@ -19,6 +20,7 @@ export default function create (params) {
CreateUser(
id: "${id}",
name: "${name}",
slug: "${slug}",
password: "${password}",
email: "${email}",
avatar: "${avatar}",
@ -29,6 +31,7 @@ export default function create (params) {
) {
id
name
slug
email
avatar
role

View File

@ -13,7 +13,7 @@ import decode from './jwt/decode'
dotenv.config()
// check env and warn
const requiredEnvVars = ['MAPBOX_TOKEN', 'JWT_SECRET']
const requiredEnvVars = ['MAPBOX_TOKEN', 'JWT_SECRET', 'PRIVATE_KEY_PASSPHRASE']
requiredEnvVars.forEach(env => {
if (!process.env[env]) {
throw new Error(`ERROR: "${env}" env variable is missing.`)

View File

@ -29,7 +29,7 @@ Feature: Delete an object
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://localhost:4123/activitypub/users/karl-heinz/status/a4DJ2afdg323v32641vna42lkj685kasd2",
"id": "http://localhost:4123/activitypub/users/karl-heinz/status/a4DJ2afdg323v32641vna42lkj685kasd2",
"type": "Delete",
"object": {
"id": "https://aronda.org/activitypub/users/bernd-das-brot/status/kljsdfg9843jknsdf234",

View File

@ -15,7 +15,7 @@ Feature: Follow a user
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83",
"id": "http://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83",
"type": "Follow",
"actor": "http://localhost:4123/activitypub/users/stuart-little",
"object": "http://localhost:4123/activitypub/users/tero-vota"
@ -32,11 +32,11 @@ Feature: Follow a user
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://localhost:4123/activitypub/users/tero-vota/status/a4DJ2afdg323v32641vna42lkj685kasd2",
"id": "http://localhost:4123/activitypub/users/tero-vota/status/a4DJ2afdg323v32641vna42lkj685kasd2",
"type": "Undo",
"actor": "http://localhost:4123/activitypub/users/tero-vota",
"object": {
"id": "https://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83",
"id": "http://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83",
"type": "Follow",
"actor": "http://localhost:4123/activitypub/users/stuart-little",
"object": "http://localhost:4123/activitypub/users/tero-vota"

View File

@ -13,14 +13,14 @@ Feature: Like an object like an article or note
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://localhost:4123/activitypub/users/karl-heinz/status/faslkasa7dasfzkjn2398hsfd",
"id": "http://localhost:4123/activitypub/users/karl-heinz/status/faslkasa7dasfzkjn2398hsfd",
"type": "Create",
"actor": "https://localhost:4123/activitypub/users/karl-heinz",
"actor": "http://localhost:4123/activitypub/users/karl-heinz",
"object": {
"id": "https://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf",
"id": "http://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf",
"type": "Article",
"published": "2019-02-07T19:37:55.002Z",
"attributedTo": "https://localhost:4123/activitypub/users/karl-heinz",
"attributedTo": "http://localhost:4123/activitypub/users/karl-heinz",
"content": "Hi Max, how are you?",
"to": "https://www.w3.org/ns/activitystreams#Public"
}
@ -32,7 +32,7 @@ Feature: Like an object like an article or note
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://localhost:4123/activitypub/users/peter-lustiger/status/83J23549sda1k72fsa4567na42312455kad83",
"id": "http://localhost:4123/activitypub/users/peter-lustiger/status/83J23549sda1k72fsa4567na42312455kad83",
"type": "Like",
"actor": "http://localhost:4123/activitypub/users/peter-lustiger",
"object": "http://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf"

View File

@ -14,10 +14,10 @@ Feature: Receiving collections
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://localhost:4123/activitypub/users/renate-oberdorfer/outbox",
"id": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox",
"summary": "renate-oberdorfers outbox collection",
"type": "OrderedCollection",
"first": "https://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true",
"first": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true",
"totalItems": 0
}
"""
@ -29,10 +29,10 @@ Feature: Receiving collections
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://localhost:4123/activitypub/users/renate-oberdorfer/following",
"id": "http://localhost:4123/activitypub/users/renate-oberdorfer/following",
"summary": "renate-oberdorfers following collection",
"type": "OrderedCollection",
"first": "https://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true",
"first": "http://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true",
"totalItems": 0
}
"""
@ -44,10 +44,10 @@ Feature: Receiving collections
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://localhost:4123/activitypub/users/renate-oberdorfer/followers",
"id": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers",
"summary": "renate-oberdorfers followers collection",
"type": "OrderedCollection",
"first": "https://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true",
"first": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true",
"totalItems": 0
}
"""
@ -59,11 +59,11 @@ Feature: Receiving collections
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true",
"id": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true",
"summary": "renate-oberdorfers outbox collection",
"type": "OrderedCollectionPage",
"totalItems": 0,
"partOf": "https://localhost:4123/activitypub/users/renate-oberdorfer/outbox",
"partOf": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox",
"orderedItems": []
}
"""
@ -75,11 +75,11 @@ Feature: Receiving collections
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true",
"id": "http://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true",
"summary": "renate-oberdorfers following collection",
"type": "OrderedCollectionPage",
"totalItems": 0,
"partOf": "https://localhost:4123/activitypub/users/renate-oberdorfer/following",
"partOf": "http://localhost:4123/activitypub/users/renate-oberdorfer/following",
"orderedItems": []
}
"""
@ -91,11 +91,11 @@ Feature: Receiving collections
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true",
"id": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true",
"summary": "renate-oberdorfers followers collection",
"type": "OrderedCollectionPage",
"totalItems": 0,
"partOf": "https://localhost:4123/activitypub/users/renate-oberdorfer/followers",
"partOf": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers",
"orderedItems": []
}
"""

View File

@ -4,7 +4,7 @@ Feature: Webfinger discovery
In order to follow the actor
Background:
Given our own server runs at "http://localhost:4100"
Given our own server runs at "http://localhost:4123"
And we have the following users in our database:
| Slug |
| peter-lustiger |
@ -19,7 +19,7 @@ Feature: Webfinger discovery
{
"rel": "self",
"type": "application/activity+json",
"href": "https://localhost:4123/users/peter-lustiger"
"href": "http://localhost:4123/activitypub/users/peter-lustiger"
}
]
}
@ -44,21 +44,21 @@ Feature: Webfinger discovery
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"id": "https://localhost:4123/activitypub/users/peter-lustiger",
"id": "http://localhost:4123/activitypub/users/peter-lustiger",
"type": "Person",
"preferredUsername": "peter-lustiger",
"name": "peter-lustiger",
"following": "https://localhost:4123/activitypub/users/peter-lustiger/following",
"followers": "https://localhost:4123/activitypub/users/peter-lustiger/followers",
"inbox": "https://localhost:4123/activitypub/users/peter-lustiger/inbox",
"outbox": "https://localhost:4123/activitypub/users/peter-lustiger/outbox",
"url": "https://localhost:4123/activitypub/@peter-lustiger",
"following": "http://localhost:4123/activitypub/users/peter-lustiger/following",
"followers": "http://localhost:4123/activitypub/users/peter-lustiger/followers",
"inbox": "http://localhost:4123/activitypub/users/peter-lustiger/inbox",
"outbox": "http://localhost:4123/activitypub/users/peter-lustiger/outbox",
"url": "http://localhost:4123/activitypub/@peter-lustiger",
"endpoints": {
"sharedInbox": "https://localhost:4123/activitypub/inbox"
"sharedInbox": "http://localhost:4123/activitypub/inbox"
},
"publicKey": {
"id": "https://localhost:4123/activitypub/users/peter-lustiger#main-key",
"owner": "https://localhost:4123/activitypub/users/peter-lustiger",
"id": "http://localhost:4123/activitypub/users/peter-lustiger#main-key",
"owner": "http://localhost:4123/activitypub/users/peter-lustiger",
"publicKeyPem": "adglkjlk89235kjn8obn2384f89z5bv9..."
}
}

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ Feature: Search
And we have the following posts in our database:
| Author | id | title | content |
| Brianna Wiest | p1 | 101 Essays that will change the way you think | 101 Essays, of course! |
| Brianna Wiest | p1 | No searched for content | will be found in this post, I guarantee |
| Brianna Wiest | p2 | No searched for content | will be found in this post, I guarantee |
Given I am logged in
Scenario: Search for specific words

View File

@ -17,7 +17,7 @@ Feature: Create a post
for active citizenship.
"""
And I click on "Save"
Then I get redirected to "/post/my-first-post/"
Then I get redirected to ".../my-first-post"
And the post was saved successfully
Scenario: See a post on the landing page

View File

@ -21,8 +21,8 @@ Feature: Tags and Categories
Scenario: See an overview of categories
When I navigate to the administration dashboard
And I click on the menu item "Categories"
Then I can see a list of categories ordered by post count:
| Icon | Name | Posts |
Then I can see the following table:
| | Name | Posts |
| | Just For Fun | 2 |
| | Happyness & Values | 1 |
| | Health & Wellbeing | 0 |
@ -30,11 +30,8 @@ Feature: Tags and Categories
Scenario: See an overview of tags
When I navigate to the administration dashboard
And I click on the menu item "Tags"
Then I can see a list of tags ordered by user count:
| # | Name | Users | Posts |
Then I can see the following table:
| | Name | Users | Posts |
| 1 | Democracy | 2 | 3 |
| 2 | Ecology | 1 | 1 |
| 3 | Nature | 1 | 2 |

View File

@ -9,38 +9,13 @@ When('I navigate to the administration dashboard', () => {
.click()
})
Then('I can see a list of categories ordered by post count:', table => {
cy.get('thead')
.find('tr th')
.should('have.length', 3)
table.hashes().forEach(({ Name, Posts }, index) => {
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(2)`).should(
'contain',
Name.trim()
)
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(3)`).should(
'contain',
Posts
)
})
})
Then('I can see a list of tags ordered by user count:', table => {
cy.get('thead')
.find('tr th')
.should('have.length', 4)
table.hashes().forEach(({ Name, Users, Posts }, index) => {
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(2)`).should(
'contain',
Name.trim()
)
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(3)`).should(
'contain',
Users
)
cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(4)`).should(
'contain',
Posts
)
Then('I can see the following table:', table => {
const headers = table.raw()[0]
headers.forEach((expected, i) => {
cy.get('thead th').eq(i).should('contain', expected)
})
const flattened = [].concat.apply([], table.rows())
flattened.forEach((expected, i) => {
cy.get('tbody td').eq(i).should('contain', expected)
})
})

View File

@ -39,10 +39,10 @@ Given('I am logged in with a {string} role', role => {
})
})
When('I click on "Report Post" from the triple dot menu of the post', () => {
When('I click on "Report Post" from the content menu of the post', () => {
cy.contains('.ds-card', davidIrvingPostTitle)
.find('.content-menu-trigger')
.click()
.click({force: true})
cy.get('.popover .ds-menu-item-link')
.contains('Report Post')
@ -50,7 +50,7 @@ When('I click on "Report Post" from the triple dot menu of the post', () => {
})
When(
'I click on "Report User" from the triple dot menu in the user info box',
'I click on "Report User" from the content menu in the user info box',
() => {
cy.contains('.ds-card', davidIrvingName)
.find('.content-menu-trigger')
@ -71,7 +71,7 @@ When('I click on the author', () => {
})
When('I report the author', () => {
cy.get('.page-name-profile-slug').then(() => {
cy.get('.page-name-profile-id-slug').then(() => {
invokeReportOnElement('.ds-card').then(() => {
cy.get('button')
.contains('Send')

View File

@ -42,9 +42,13 @@ When('I select an entry', () => {
})
Then("I should be on the post's page", () => {
cy.location('pathname').should(
'contain',
'/post/'
)
cy.location('pathname').should(
'eq',
'/post/101-essays-that-will-change-the-way-you-think/'
'/post/p1/101-essays-that-will-change-the-way-you-think'
)
})

View File

@ -5,7 +5,7 @@ import { getLangByName } from '../../support/helpers'
let lastPost = {}
const loginCredentials = {
let loginCredentials = {
email: 'peterpan@example.org',
password: '1234'
}
@ -86,6 +86,10 @@ Given('my user account has the role {string}', role => {
When('I log out', cy.logout)
When('I visit {string}', page => {
cy.openPage(page)
})
When('I visit the {string} page', page => {
cy.openPage(page)
})
@ -220,7 +224,7 @@ Then('the post shows up on the landing page at position {int}', index => {
})
Then('I get redirected to {string}', route => {
cy.location('pathname').should('contain', route)
cy.location('pathname').should('contain', route.replace('...', ''))
})
Then('the post was saved successfully', () => {
@ -244,3 +248,48 @@ Then(
cy.get('.error').should('contain', message)
}
)
Given('my user account has the following login credentials:', table => {
loginCredentials = table.hashes()[0]
cy.debug()
cy.factory().create('User', loginCredentials)
})
When('I fill the password form with:', table => {
table = table.rowsHash()
cy.get('input[id=oldPassword]')
.type(table['Your old password'])
.get('input[id=newPassword]')
.type(table['Your new passsword'])
.get('input[id=confirmPassword]')
.type(table['Confirm new password'])
})
When('submit the form', () => {
cy.get('form').submit()
})
Then('I cannot login anymore with password {string}', password => {
cy.reload()
const { email } = loginCredentials
cy.visit(`/login`)
cy.get('input[name=email]')
.trigger('focus')
.type(email)
cy.get('input[name=password]')
.trigger('focus')
.type(password)
cy.get('button[name=submit]')
.as('submitButton')
.click()
cy.get('.iziToast-wrapper').should('contain', 'Incorrect email address or password.')
})
Then('I can login successfully with password {string}', password => {
cy.reload()
cy.login({
...loginCredentials,
...{password}
})
cy.get('.iziToast-wrapper').should('contain', "You are logged in!")
})

View File

@ -0,0 +1,41 @@
Feature: Persistent Links
As a user
I want all links to carry permanent information that identifies the linked resource
In order to have persistent links even if a part of the URL might change
| | Modifiable | Referenceable | Unique | Purpose |
| -- | -- | -- | -- | -- |
| ID | no | yes | yes | Identity, Traceability, Links |
| Slug | yes | yes | yes | @-Mentions, SEO-friendly URL |
| Name | yes | no | no | Search, self-description |
Background:
Given we have the following user accounts:
| id | name | slug |
| MHNqce98y1 | Stephen Hawking | thehawk |
And we have the following posts in our database:
| id | title | slug |
| bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays |
And I have a user account
And I am logged in
Scenario Outline: Link with slug only is valid and gets auto-completed
When I visit "<url>"
Then I get redirected to "<redirectUrl>"
Examples:
| url | redirectUrl |
| /profile/thehawk | /profile/MHNqce98y1/thehawk |
| /post/101-essays | /post/bWBjpkTKZp/101-essays |
Scenario: Link with id only will always point to the same user
When I visit "/profile/MHNqce98y1"
Then I get redirected to "/profile/MHNqce98y1/thehawk"
Scenario Outline: ID takes precedence over slug
When I visit "<url>"
Then I get redirected to "<redirectUrl>"
Examples:
| url | redirectUrl |
| /profile/MHNqce98y1/stephen-hawking | /profile/MHNqce98y1/thehawk |
| /post/bWBjpkTKZp/the-way-you-think | /post/bWBjpkTKZp/101-essays |

View File

@ -15,7 +15,7 @@ Feature: Report and Moderate
Scenario Outline: Report a post from various pages
Given I am logged in with a "user" role
When I see David Irving's post on the <Page>
And I click on "Report Post" from the triple dot menu of the post
And I click on "Report Post" from the content menu of the post
And I confirm the reporting dialog because it is a criminal act under German law:
"""
Do you really want to report the contribution "The Truth about the Holocaust"?
@ -33,7 +33,7 @@ Feature: Report and Moderate
Given I am logged in with a "user" role
And I see David Irving's post on the post page
When I click on the author
And I click on "Report User" from the triple dot menu in the user info box
And I click on "Report User" from the content menu in the user info box
And I confirm the reporting dialog because he is a holocaust denier:
"""
Do you really want to report the user "David Irving"?

View File

@ -0,0 +1,31 @@
Feature: Change password
As a user
I want to change my password in my settings
For security, e.g. if I exposed my password by accident
Login via email and password is a well-known authentication procedure and you
can assure to the server that you are who you claim to be. Either if you
exposed your password by acccident and you want to invalidate the exposed
password or just out of an good habit, you want to change your password.
Background:
Given my user account has the following login credentials:
| email | password |
| user@example.org | exposed |
And I am logged in
Scenario: Change my password
Given I am on the "settings" page
And I click on "Security"
When I fill the password form with:
| Your old password | exposed |
| Your new passsword | secure |
| Confirm new password | secure |
And submit the form
And I see a success message:
"""
Password successfully changed!
"""
And I log out through the menu in the top right corner
Then I cannot login anymore with password "exposed"
But I can login successfully with password "secure"

View File

@ -1,25 +0,0 @@
language: generic
before_install:
- openssl aes-256-cbc -K $encrypted_87342d90efbe_key -iv $encrypted_87342d90efbe_iv
-in kubeconfig.yaml.enc -out kubeconfig.yaml -d
install:
- curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
- chmod +x ./kubectl
- sudo mv ./kubectl /usr/local/bin/kubectl
- mkdir ${HOME}/.kube
- cp kubeconfig.yaml ${HOME}/.kube/config
script:
- kubectl get nodes
deploy:
provider: script
# TODO: fix downtime
# instead of deleting all pods, update the deployment and make a rollout
# TODO: fix multiple access error on volumes
# this happens if more than two pods access a volume
script: kubectl --namespace=human-connection delete pods --all
on:
branch: master

View File

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

View File

@ -4,8 +4,6 @@
metadata:
name: nitro-backend
namespace: human-connection
labels:
commit: "COMMIT"
spec:
replicas: 1
minReadySeconds: 15
@ -20,6 +18,7 @@
template:
metadata:
labels:
human-connection.org/commit: COMMIT
human-connection.org/selector: deployment-human-connection-backend
name: "nitro-backend"
spec:

View File

@ -3,8 +3,6 @@ kind: Deployment
metadata:
name: nitro-web
namespace: human-connection
labels:
commit: "COMMIT"
spec:
replicas: 2
minReadySeconds: 15
@ -15,6 +13,7 @@ spec:
template:
metadata:
labels:
human-connection.org/commit: COMMIT
human-connection.org/selector: deployment-human-connection-web
name: nitro-web
spec:

Binary file not shown.

View File

@ -12,7 +12,7 @@ services:
context: webapp
target: build-and-test
environment:
- GRAPHQL_URI=http://backend:4123
- GRAPHQL_URI=http://backend:4000
backend:
image: humanconnection/nitro-backend:builder
build:

View File

@ -8,15 +8,21 @@
"nonGlobalStepDefinitions": true
},
"scripts": {
"cypress:run": "cypress run --browser chrome",
"cypress:open": "cypress open --browser chrome"
"cypress:backend:server": "cd backend && yarn run test:before:server",
"cypress:backend:seeder": "cd backend && yarn run test:before:seeder",
"cypress:webapp": "cd webapp && cross-env GRAPHQL_URI=http://localhost:4123 yarn run dev",
"cypress:setup": "run-p cypress:backend:* cypress:webapp",
"cypress:run": "cypress run --browser chromium",
"cypress:open": "cypress open --browser chromium"
},
"devDependencies": {
"cross-env": "^5.2.0",
"cypress": "^3.2.0",
"cypress-cucumber-preprocessor": "^1.11.0",
"dotenv": "^7.0.0",
"faker": "^4.1.0",
"graphql-request": "^1.8.2",
"neo4j-driver": "^1.7.3"
"neo4j-driver": "^1.7.3",
"npm-run-all": "^4.1.5"
}
}

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash
sed -i "s/<COMMIT>/${TRAVIS_COMMIT}/g" patch-deployment.yaml
kubectl --namespace=human-connection patch deployment nitro-backend -p "$(cat patch-deployment.yaml)"
kubectl --namespace=human-connection patch deployment nitro-web -p "$(cat patch-deployment.yaml)"
sed -i "s/<COMMIT>/${TRAVIS_COMMIT}/g" $TRAVIS_BUILD_DIR/scripts/patch-deployment.yaml
kubectl --namespace=human-connection patch deployment nitro-backend -p "$(cat $TRAVIS_BUILD_DIR/scripts/patch-deployment.yaml)"
kubectl --namespace=human-connection patch deployment nitro-web -p "$(cat $TRAVIS_BUILD_DIR/scripts/patch-deployment.yaml)"

View File

@ -1,3 +1,5 @@
metadata:
labels:
commit: <COMMIT>
spec:
template:
metadata:
labels:
human-connection.org/commit: <COMMIT>

18
scripts/setup_kubernetes.sh Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# This script can be called multiple times for each `before_deploy` hook
# so let's exit successfully if kubectl is already installed:
command -v kubectl && exit 0
curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectl
curl -LO https://github.com/digitalocean/doctl/releases/download/v1.14.0/doctl-1.14.0-linux-amd64.tar.gz
tar xf doctl-1.14.0-linux-amd64.tar.gz
chmod +x ./doctl
sudo mv ./doctl /usr/local/bin/doctl
doctl auth init --access-token $DOCTL_ACCESS_TOKEN
mkdir -p ~/.kube/
doctl kubernetes cluster kubeconfig show nitro-staging > ~/.kube/config

View File

@ -0,0 +1,154 @@
import { mount, createLocalVue } from '@vue/test-utils'
import ChangePassword from './ChangePassword.vue'
import Vue from 'vue'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('ChangePassword.vue', () => {
let mocks
let wrapper
beforeEach(() => {
mocks = {
validate: jest.fn(),
$toast: {
error: jest.fn(),
success: jest.fn()
},
$t: jest.fn(),
$store: {
commit: jest.fn()
},
$apollo: {
mutate: jest
.fn()
.mockRejectedValue({ message: 'Ouch!' })
.mockResolvedValueOnce({ data: { changePassword: 'NEWTOKEN' } })
}
}
})
describe('mount', () => {
let wrapper
const Wrapper = () => {
return mount(ChangePassword, { mocks, localVue })
}
beforeEach(() => {
wrapper = Wrapper()
})
it('renders three input fields', () => {
expect(wrapper.findAll('input')).toHaveLength(3)
})
describe('validations', () => {
it('invalid', () => {
expect(wrapper.vm.disabled).toBe(true)
})
describe('old password and new password', () => {
describe('match', () => {
beforeEach(() => {
wrapper.find('input#oldPassword').setValue('some secret')
wrapper.find('input#newPassword').setValue('some secret')
})
it('invalid', () => {
expect(wrapper.vm.disabled).toBe(true)
})
it.skip('displays a warning', () => {
const calls = mocks.validate.mock.calls
const expected = [
['change-password.validations.old-and-new-password-match']
]
expect(calls).toEqual(expect.arrayContaining(expected))
})
})
})
describe('new password and confirmation', () => {
describe('mismatch', () => {
it.todo('invalid')
it.todo('displays a warning')
})
describe('match', () => {
describe('and old password mismatch', () => {
it.todo('valid')
})
describe('clicked', () => {
it.todo('sets loading')
})
})
})
})
describe('given valid input', () => {
beforeEach(() => {
wrapper.find('input#oldPassword').setValue('supersecret')
wrapper.find('input#newPassword').setValue('superdupersecret')
wrapper.find('input#confirmPassword').setValue('superdupersecret')
})
describe('submit form', () => {
beforeEach(() => {
wrapper.find('form').trigger('submit')
})
it('calls changePassword mutation', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalled()
})
it('passes form data as variables', () => {
expect(mocks.$apollo.mutate.mock.calls[0][0]).toEqual(
expect.objectContaining({
variables: {
oldPassword: 'supersecret',
newPassword: 'superdupersecret',
confirmPassword: 'superdupersecret'
}
})
)
})
describe('mutation resolves', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest.fn().mockResolvedValue()
wrapper = Wrapper()
})
it('calls auth/SET_TOKEN with response', () => {
expect(mocks.$store.commit).toHaveBeenCalledWith(
'auth/SET_TOKEN',
'NEWTOKEN'
)
})
it('displays success message', () => {
expect(mocks.$t).toHaveBeenCalledWith(
'settings.security.change-password.success'
)
expect(mocks.$toast.success).toHaveBeenCalled()
})
})
describe('mutation rejects', () => {
beforeEach(() => {
// second call will reject
wrapper.find('form').trigger('submit')
})
it('displays error message', () => {
expect(mocks.$toast.error).toHaveBeenCalledWith('Ouch!')
})
})
})
})
})
})

View File

@ -0,0 +1,83 @@
<template>
<ds-form
v-model="formData"
:schema="formSchema"
@submit="handleSubmit"
>
<template>
<ds-input
id="oldPassword"
model="oldPassword"
type="password"
label="Your old password"
/>
<ds-input
id="newPassword"
model="newPassword"
type="password"
label="Your new password"
/>
<ds-input
id="confirmPassword"
model="confirmPassword"
type="password"
label="Confirm new password"
/>
<ds-space margin-top="base">
<ds-button
:loading="loading"
primary
>
{{ $t('settings.security.change-password.button') }}
</ds-button>
</ds-space>
</template>
</ds-form>
</template>
<script>
import gql from 'graphql-tag'
export default {
name: 'ChangePassword',
data() {
return {
formData: {
oldPassword: '',
newPassword: '',
confirmPassword: ''
},
formSchema: {
oldPassword: { required: true },
newPassword: { required: true },
confirmPassword: { required: true }
},
loading: false,
disabled: true
}
},
methods: {
async handleSubmit(data) {
this.loading = true
const mutation = gql`
mutation($oldPassword: String!, $newPassword: String!) {
changePassword(oldPassword: $oldPassword, newPassword: $newPassword)
}
`
const variables = this.formData
try {
const { data } = await this.$apollo.mutate({ mutation, variables })
this.$store.commit('auth/SET_TOKEN', data.changePassword)
this.$toast.success(
this.$t('settings.security.change-password.success')
)
} catch (err) {
this.$toast.error(err.message)
} finally {
this.loading = false
}
}
}
}
</script>

View File

@ -111,8 +111,8 @@ export default {
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
this.$router.push({
name: 'post-slug',
params: { slug: result.slug }
name: 'post-id-slug',
params: { id: result.id, slug: result.slug }
})
})
.catch(err => {

View File

@ -106,8 +106,8 @@ export default {
methods: {
href(post) {
return this.$router.resolve({
name: 'post-slug',
params: { slug: post.slug }
name: 'post-id-slug',
params: { id: post.id, slug: post.slug }
}).href
}
}

View File

@ -153,9 +153,9 @@ export default {
return count
},
userLink() {
const { slug } = this.user
if (!slug) return ''
return { name: 'profile-slug', params: { slug } }
const { id, slug } = this.user
if (!(id && slug)) return ''
return { name: 'profile-id-slug', params: { slug, id } }
}
}
}

View File

@ -9,58 +9,69 @@ export default app => {
type
createdAt
submitter {
id
slug
name
disabled
deleted
name
slug
}
user {
name
id
slug
name
disabled
deleted
disabledBy {
id
slug
name
disabled
deleted
}
}
comment {
contentExcerpt
author {
name
id
slug
name
disabled
deleted
}
post {
id
slug
title
disabled
deleted
title
slug
}
disabledBy {
disabled
deleted
id
slug
name
disabled
deleted
}
}
post {
title
id
slug
title
disabled
deleted
author {
id
slug
name
disabled
deleted
name
slug
}
disabledBy {
disabled
deleted
id
slug
name
disabled
deleted
}
}
}

View File

@ -6,6 +6,7 @@ export default app => {
query User($slug: String!, $first: Int, $offset: Int) {
User(slug: $slug) {
id
slug
name
avatar
about
@ -27,8 +28,8 @@ export default app => {
followingCount
following(first: 7) {
id
name
slug
name
avatar
disabled
deleted
@ -49,10 +50,10 @@ export default app => {
followedByCurrentUser
followedBy(first: 7) {
id
slug
name
disabled
deleted
slug
avatar
followedByCount
followedByCurrentUser
@ -87,6 +88,7 @@ export default app => {
}
author {
id
slug
avatar
name
disabled

View File

@ -39,7 +39,7 @@
>
<a
class="avatar-menu-trigger"
:href="$router.resolve({name: 'profile-slug', params: {slug: user.slug}}).href"
:href="$router.resolve({name: 'profile-id-slug', params: {id: user.id, slug: user.slug}}).href"
@click.prevent="toggleMenu"
>
<ds-avatar
@ -182,8 +182,8 @@ export default {
goToPost(item) {
this.$nextTick(() => {
this.$router.push({
name: 'post-slug',
params: { slug: item.slug }
name: 'post-id-slug',
params: { id: item.id, slug: item.slug }
})
})
},

View File

@ -31,7 +31,11 @@
"labelBio": "Über dich"
},
"security": {
"name": "Sicherheit"
"name": "Sicherheit",
"change-password": {
"button": "Passwort ändern",
"success": "Passwort erfolgreich geändert!"
}
},
"invites": {
"name": "Einladungen"

View File

@ -31,7 +31,11 @@
"labelBio": "About You"
},
"security": {
"name": "Security"
"name": "Security",
"change-password": {
"button": "Change password",
"success": "Password successfully changed!"
}
},
"invites": {
"name": "Invites"

View File

@ -0,0 +1,32 @@
export default function(options = {}) {
const { queryId, querySlug, path, message = 'Page not found.' } = options
return {
asyncData: async context => {
const {
params: { id, slug },
redirect,
error,
app: { apolloProvider }
} = context
const idOrSlug = id || slug
const variables = { idOrSlug }
const client = apolloProvider.defaultClient
let response
let resource
response = await client.query({ query: queryId, variables })
resource = response.data[Object.keys(response.data)[0]][0]
if (resource && resource.slug === slug) return // all good
if (resource && resource.slug !== slug) {
return redirect(`/${path}/${resource.id}/${resource.slug}`)
}
response = await client.query({ query: querySlug, variables })
resource = response.data[Object.keys(response.data)[0]][0]
if (resource) return redirect(`/${path}/${resource.id}/${resource.slug}`)
return error({ statusCode: 404, message })
}
}
}

View File

@ -121,12 +121,30 @@ module.exports = {
proxy: true
},
proxy: {
'/.well-known/webfinger': {
target: process.env.GRAPHQL_URI || 'http://localhost:4000',
toProxy: true, // cloudflare needs that
headers: {
Accept: 'application/json',
'X-UI-Request': true,
'X-API-TOKEN': process.env.BACKEND_TOKEN || 'NULL'
}
},
'/activitypub': {
// make this configurable (nuxt-dotenv)
target: process.env.GRAPHQL_URI || 'http://localhost:4000',
toProxy: true, // cloudflare needs that
headers: {
Accept: 'application/json',
'X-UI-Request': true,
'X-API-TOKEN': process.env.BACKEND_TOKEN || 'NULL'
}
},
'/api': {
// make this configurable (nuxt-dotenv)
target: process.env.GRAPHQL_URI || 'http://localhost:4000',
pathRewrite: { '^/api': '' },
toProxy: true, // cloudflare needs that
changeOrigin: true,
headers: {
Accept: 'application/json',
'X-UI-Request': true,

View File

@ -11,10 +11,8 @@
"start": "cross-env node server/index.js",
"generate": "nuxt generate",
"lint": "eslint --ext .js,.vue .",
"test": "jest",
"precommit": "yarn lint",
"e2e:local": "cypress run --headed",
"e2e:ci": "npm-run-all --parallel --race start:ci 'cypress:ci --config baseUrl=http://localhost:3000'",
"test": "jest",
"test:unit:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --no-cache --runInBand"
},
"jest": {
@ -46,7 +44,7 @@
"cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.27",
"express": "~4.16.4",
"graphql": "~14.1.1",
"graphql": "~14.2.1",
"jsonwebtoken": "~8.5.1",
"linkify-it": "~2.1.0",
"nuxt": "~2.4.5",
@ -61,26 +59,26 @@
"vuex-i18n": "~1.11.0"
},
"devDependencies": {
"@babel/core": "~7.3.4",
"@babel/preset-env": "~7.3.4",
"@vue/cli-shared-utils": "~3.4.1",
"@babel/core": "~7.4.3",
"@babel/preset-env": "~7.4.3",
"@vue/cli-shared-utils": "~3.5.1",
"@vue/eslint-config-prettier": "~4.0.1",
"@vue/server-test-utils": "~1.0.0-beta.29",
"@vue/test-utils": "~1.0.0-beta.29",
"babel-core": "~7.0.0-bridge.0",
"babel-eslint": "~10.0.1",
"babel-jest": "~24.5.0",
"eslint": "~5.15.1",
"eslint-config-prettier": "~3.6.0",
"babel-jest": "~24.7.1",
"eslint": "~5.16.0",
"eslint-config-prettier": "~4.1.0",
"eslint-loader": "~2.1.2",
"eslint-plugin-prettier": "~3.0.1",
"eslint-plugin-vue": "~5.2.2",
"jest": "~24.5.0",
"jest": "~24.7.1",
"node-sass": "~4.11.0",
"nodemon": "~1.18.10",
"prettier": "~1.14.3",
"sass-loader": "~7.1.0",
"vue-jest": "~3.0.4",
"vue-svg-loader": "~0.11.0"
"vue-svg-loader": "~0.12.0"
}
}

View File

@ -64,8 +64,8 @@ export default {
},
href(post) {
return this.$router.resolve({
name: 'post-slug',
params: { slug: post.slug }
name: 'post-id-slug',
params: { id: post.id, slug: post.slug }
}).href
},
showMoreContributions() {

View File

@ -14,7 +14,7 @@
slot-scope="scope"
>
<div v-if="scope.row.type === 'Post'">
<nuxt-link :to="{ name: 'post-slug', params: { slug: scope.row.post.slug } }">
<nuxt-link :to="{ name: 'post-id-slug', params: { id: scope.row.post.id, slug: scope.row.post.slug } }">
<b>{{ scope.row.post.title | truncate(50) }}</b>
</nuxt-link><br>
<ds-text
@ -25,7 +25,7 @@
</ds-text>
</div>
<div v-else-if="scope.row.type === 'Comment'">
<nuxt-link :to="{ name: 'post-slug', params: { slug: scope.row.comment.post.slug } }">
<nuxt-link :to="{ name: 'post-id-slug', params: { id: scope.row.comment.post.id, slug: scope.row.comment.post.slug } }">
<b>{{ scope.row.comment.contentExcerpt | truncate(50) }}</b>
</nuxt-link><br>
<ds-text
@ -36,7 +36,7 @@
</ds-text>
</div>
<div v-else>
<nuxt-link :to="{ name: 'profile-slug', params: { slug: scope.row.user.slug } }">
<nuxt-link :to="{ name: 'profile-id-slug', params: { id: scope.row.user.id, slug: scope.row.user.slug } }">
<b>{{ scope.row.user.name | truncate(50) }}</b>
</nuxt-link>
</div>
@ -69,7 +69,7 @@
slot="submitter"
slot-scope="scope"
>
<nuxt-link :to="{ name: 'profile-slug', params: { slug: scope.row.submitter.slug } }">
<nuxt-link :to="{ name: 'profile-id-slug', params: { id: scope.row.submitter.id, slug: scope.row.submitter.slug } }">
{{ scope.row.submitter.name }}
</nuxt-link>
</template>
@ -79,19 +79,19 @@
>
<nuxt-link
v-if="scope.row.type === 'Post' && scope.row.post.disabledBy"
:to="{ name: 'profile-slug', params: { slug: scope.row.post.disabledBy.slug } }"
:to="{ name: 'profile-id-slug', params: { id: scope.row.post.disabledBy.id, slug: scope.row.post.disabledBy.slug } }"
>
<b>{{ scope.row.post.disabledBy.name | truncate(50) }}</b>
</nuxt-link>
<nuxt-link
v-else-if="scope.row.type === 'Comment' && scope.row.comment.disabledBy"
:to="{ name: 'profile-slug', params: { slug: scope.row.comment.disabledBy.slug } }"
:to="{ name: 'profile-id-slug', params: { id: scope.row.comment.disabledBy.id, slug: scope.row.comment.disabledBy.slug } }"
>
<b>{{ scope.row.comment.disabledBy.name | truncate(50) }}</b>
</nuxt-link>
<nuxt-link
v-else-if="scope.row.type === 'User' && scope.row.user.disabledBy"
:to="{ name: 'profile-slug', params: { slug: scope.row.user.disabledBy.slug } }"
:to="{ name: 'profile-id-slug', params: { id: scope.row.user.disabledBy.id, slug: scope.row.user.disabledBy.slug } }"
>
<b>{{ scope.row.user.disabledBy.name | truncate(50) }}</b>
</nuxt-link>

View File

@ -17,35 +17,62 @@
</template>
<script>
import gql from 'graphql-tag'
import PersistentLinks from '~/mixins/persistentLinks.js'
const options = {
queryId: gql`
query($idOrSlug: ID) {
Post(id: $idOrSlug) {
id
slug
}
}
`,
querySlug: gql`
query($idOrSlug: String) {
Post(slug: $idOrSlug) {
id
slug
}
}
`,
path: 'post',
message: 'This post could not be found'
}
const persistentLinks = PersistentLinks(options)
export default {
mixins: [persistentLinks],
computed: {
routes() {
const { slug, id } = this.$route.params
return [
{
name: this.$t('common.post', null, 1),
path: `/post/${this.$route.params.slug}`,
path: `/post/${id}/${slug}`,
children: [
{
name: this.$t('common.comment', null, 2),
path: `/post/${this.$route.params.slug}#comments`
path: `/post/${id}/${slug}#comments`
},
{
name: this.$t('common.letsTalk'),
path: `/post/${this.$route.params.slug}#lets-talk`
path: `/post/${id}/${slug}#lets-talk`
},
{
name: this.$t('common.versus'),
path: `/post/${this.$route.params.slug}#versus`
path: `/post/${id}/${slug}#versus`
}
]
},
{
name: this.$t('common.moreInfo'),
path: `/post/${this.$route.params.slug}/more-info`
path: `/post/${id}/${slug}/more-info`
},
{
name: this.$t('common.takeAction'),
path: `/post/${this.$route.params.slug}/take-action`
path: `/post/${id}/${slug}/take-action`
}
]
}

View File

@ -263,7 +263,7 @@ export default {
</script>
<style lang="scss">
.page-name-post-slug {
.page-name-post-id-slug {
.content-menu {
float: right;
margin-right: -$space-x-small;

View File

@ -0,0 +1,34 @@
<template>
<nuxt-child />
</template>
<script>
import gql from 'graphql-tag'
import PersistentLinks from '~/mixins/persistentLinks.js'
const options = {
queryId: gql`
query($idOrSlug: ID) {
User(id: $idOrSlug) {
id
slug
}
}
`,
querySlug: gql`
query($idOrSlug: String) {
User(slug: $idOrSlug) {
id
slug
}
}
`,
message: 'This user could not be found',
path: 'profile'
}
const persistentLinks = PersistentLinks(options)
export default {
mixins: [persistentLinks]
}
</script>

View File

@ -423,7 +423,7 @@ export default {
border: #fff 5px solid;
}
.page-name-profile-slug {
.page-name-profile-id-slug {
.ds-flex-item:first-child .content-menu {
position: absolute;
top: $space-x-small;

View File

@ -1,8 +0,0 @@
<script>
export default {
layout: 'blank',
asyncData({ error }) {
error({ statusCode: 404, message: 'Profile slug missing' })
}
}
</script>

View File

@ -1,18 +1,16 @@
<template>
<ds-card :header="$t('settings.security.name')">
<hc-empty
icon="tasks"
message="Coming Soon…"
/>
<change-password />
</ds-card>
</template>
<script>
import HcEmpty from '~/components/Empty.vue'
import ChangePassword from '~/components/ChangePassword.vue'
export default {
components: {
HcEmpty
ChangePassword
}
}
</script>

File diff suppressed because it is too large Load Diff

206
yarn.lock
View File

@ -1356,7 +1356,7 @@ chai@^4.1.2:
pathval "^1.1.0"
type-detect "^4.0.5"
chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1:
chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@ -1673,6 +1673,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
cross-env@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2"
integrity sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==
dependencies:
cross-spawn "^6.0.5"
is-windows "^1.0.0"
cross-fetch@2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.2.2.tgz#a47ff4f7fc712daba8f6a695a11c948440d45723"
@ -1681,7 +1689,7 @@ cross-fetch@2.2.2:
node-fetch "2.1.2"
whatwg-fetch "2.0.4"
cross-spawn@^6.0.0:
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
@ -1896,6 +1904,13 @@ deep-extend@^0.6.0:
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
define-properties@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
dependencies:
object-keys "^1.0.12"
define-property@^0.2.5:
version "0.2.5"
resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
@ -2049,6 +2064,27 @@ error-stack-parser@^2.0.1:
dependencies:
stackframe "^1.0.4"
es-abstract@^1.4.3:
version "1.13.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==
dependencies:
es-to-primitive "^1.2.0"
function-bind "^1.1.1"
has "^1.0.3"
is-callable "^1.1.4"
is-regex "^1.0.4"
object-keys "^1.0.12"
es-to-primitive@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==
dependencies:
is-callable "^1.1.4"
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.46:
version "0.10.49"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.49.tgz#059a239de862c94494fec28f8150c977028c6c5e"
@ -2357,7 +2393,7 @@ fsevents@^1.0.0, fsevents@^1.2.7:
nan "^2.9.2"
node-pre-gyp "^0.10.0"
function-bind@^1.1.1:
function-bind@^1.0.2, function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
@ -2499,6 +2535,11 @@ has-flag@^3.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
has-symbols@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=
has-unicode@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
@ -2535,7 +2576,7 @@ has-values@^1.0.0:
is-number "^3.0.0"
kind-of "^4.0.0"
has@^1.0.0:
has@^1.0.0, has@^1.0.1, has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
@ -2567,6 +2608,11 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hosted-git-info@^2.1.4:
version "2.7.1"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==
htmlescape@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
@ -2701,6 +2747,11 @@ is-buffer@^1.1.0, is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-callable@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==
is-ci@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
@ -2722,6 +2773,11 @@ is-data-descriptor@^1.0.0:
dependencies:
kind-of "^6.0.0"
is-date-object@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=
is-descriptor@^0.1.0:
version "0.1.6"
resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
@ -2880,17 +2936,31 @@ is-promise@^2.1.0:
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
is-regex@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=
dependencies:
has "^1.0.1"
is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
is-symbol@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38"
integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==
dependencies:
has-symbols "^1.0.0"
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-windows@^1.0.2:
is-windows@^1.0.0, is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
@ -3122,6 +3192,16 @@ listr@0.12.0:
stream-to-observable "^0.1.0"
strip-ansi "^3.0.1"
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
dependencies:
graceful-fs "^4.1.2"
parse-json "^4.0.0"
pify "^3.0.0"
strip-bom "^3.0.0"
lodash.memoize@~3.0.3:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
@ -3197,6 +3277,11 @@ md5.js@^1.3.4:
inherits "^2.0.1"
safe-buffer "^5.1.2"
memorystream@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
micromatch@^2.1.5:
version "2.3.11"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
@ -3455,6 +3540,16 @@ nopt@^4.0.1:
abbrev "1"
osenv "^0.1.4"
normalize-package-data@^2.3.2:
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
dependencies:
hosted-git-info "^2.1.4"
resolve "^1.10.0"
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
@ -3480,6 +3575,21 @@ npm-packlist@^1.1.6:
ignore-walk "^3.0.1"
npm-bundled "^1.0.1"
npm-run-all@^4.1.5:
version "4.1.5"
resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==
dependencies:
ansi-styles "^3.2.1"
chalk "^2.4.1"
cross-spawn "^6.0.5"
memorystream "^0.3.1"
minimatch "^3.0.4"
pidtree "^0.3.0"
read-pkg "^3.0.0"
shell-quote "^1.6.1"
string.prototype.padend "^3.0.0"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@ -3521,6 +3631,11 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
object-keys@^1.0.12:
version "1.1.0"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.0.tgz#11bd22348dd2e096a045ab06f6c85bcc340fa032"
integrity sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==
object-visit@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
@ -3694,6 +3809,13 @@ path-platform@~0.11.15:
resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2"
integrity sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=
path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
dependencies:
pify "^3.0.0"
pathval@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
@ -3720,11 +3842,21 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
pidtree@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.0.tgz#f6fada10fccc9f99bf50e90d0b23d72c9ebc2e6b"
integrity sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==
pify@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
pify@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@ -3848,6 +3980,15 @@ read-only-stream@^2.0.0:
dependencies:
readable-stream "^2.0.2"
read-pkg@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
dependencies:
load-json-file "^4.0.0"
normalize-package-data "^2.3.2"
path-type "^3.0.0"
readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.6, readable-stream@~2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
@ -4009,7 +4150,7 @@ resolve@1.1.7:
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
resolve@^1.1.4, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.8.1:
resolve@^1.1.4, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.8.1:
version "1.10.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==
@ -4078,6 +4219,11 @@ seed-random@~2.2.0:
resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54"
integrity sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=
"semver@2 || 3 || 4 || 5":
version "5.7.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1:
version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
@ -4222,6 +4368,32 @@ source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.3:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
spdx-correct@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"
integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==
dependencies:
spdx-expression-parse "^3.0.0"
spdx-license-ids "^3.0.0"
spdx-exceptions@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==
spdx-expression-parse@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==
dependencies:
spdx-exceptions "^2.1.0"
spdx-license-ids "^3.0.0"
spdx-license-ids@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e"
integrity sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@ -4353,6 +4525,15 @@ string-width@^1.0.1:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
string.prototype.padend@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0"
integrity sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=
dependencies:
define-properties "^1.1.2"
es-abstract "^1.4.3"
function-bind "^1.0.2"
string_decoder@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
@ -4381,6 +4562,11 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
strip-eof@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@ -4695,6 +4881,14 @@ uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
dependencies:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
verror@1.10.0, verror@^1.9.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"