diff --git a/.travis.yml b/.travis.yml index 6ba9d7f12..42b427a11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,8 @@ before_install: install: - docker-compose -f docker-compose.yml -f docker-compose.travis.yml up --build -d - - wait-on http://localhost:7474 && docker-compose exec neo4j migrate + # avoid "Database constraints have changed after this transaction started" + - wait-on http://localhost:7474 script: # Backend diff --git a/SUMMARY.md b/SUMMARY.md index fdf3600b4..701eac2d0 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -3,6 +3,7 @@ * [Introduction](README.md) * [Edit this Documentation](edit-this-documentation.md) * [Installation](installation.md) +* [Neo4J](neo4j/README.md) * [Backend](backend/README.md) * [GraphQL](backend/graphql.md) * [Webapp](webapp/README.md) diff --git a/backend/Dockerfile b/backend/Dockerfile index 6d3095648..d24f2747e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12.3.1-alpine as base +FROM node:12.4-alpine as base LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)" EXPOSE 4000 diff --git a/backend/README.md b/backend/README.md index 7c4d3a3e9..3cce123ac 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,8 +1,6 @@ # Backend -## Installation -{% tabs %} -{% tab title="Docker" %} +## Installation with Docker Run the following command to install everything through docker. @@ -14,28 +12,15 @@ $ docker-compose up # rebuild the containers for a cleanup $ docker-compose up --build ``` -Open another terminal and create unique indices with: -```bash -$ docker-compose exec neo4j migrate -``` +Wait a little until your backend is up and running at [http://localhost:4000/](http://localhost:4000/). -{% endtab %} - -{% tab title="Without Docker" %} +## Installation without Docker For the local installation you need a recent version of [node](https://nodejs.org/en/) -(>= `v10.12.0`) and [Neo4J](https://neo4j.com/) along with -[Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures) plugin installed -on your system. +(>= `v10.12.0`). -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 -Note that grand-stack-starter does not currently bundle a distribution of Neo4j. You can download [Neo4j Desktop](https://neo4j.com/download/) and run locally for development, spin up a [hosted Neo4j Sandbox instance](https://neo4j.com/download/), run Neo4j in one of the [many cloud options](https://neo4j.com/developer/guide-cloud-deployment/), [spin up Neo4j in a Docker container](https://neo4j.com/developer/docker/) or on Debian-based systems install [Neo4j from the Debian Repository](http://debian.neo4j.org/). Just be sure to update the Neo4j connection string and credentials accordingly in `.env`. -Start Neo4J and confirm the database is running at [http://localhost:7474](http://localhost:7474). - -Now install node dependencies with [yarn](https://yarnpkg.com/en/): +Install node dependencies with [yarn](https://yarnpkg.com/en/): ```bash $ cd backend $ yarn install @@ -46,14 +31,8 @@ Copy Environment Variables: # in backend/ $ cp .env.template .env ``` - -Configure the new files according to your needs and your local setup. - -Create unique indices with: - -```bash -$ ./neo4j/migrate.sh -``` +Configure the new file according to your needs and your local setup. Make sure +a [local Neo4J](http://localhost:7474) instance is up and running. Start the backend for development with: ```bash @@ -65,17 +44,12 @@ or start the backend in production environment with: yarn run start ``` -{% endtab %} -{% endtabs %} - Your backend is up and running at [http://localhost:4000/](http://localhost:4000/) -This will start the GraphQL service \(by default on localhost:4000\) where you can issue GraphQL requests or access GraphQL Playground in the browser. +This will start the GraphQL service \(by default on localhost:4000\) where you +can issue GraphQL requests or access GraphQL Playground in the browser.  -You can access Neo4J through [http://localhost:7474/](http://localhost:7474/) -for an interactive `cypher` shell and a visualization of the graph. - #### Seed Database @@ -114,7 +88,8 @@ $ yarn run db:reset # Testing -**Beware**: We have no multiple database setup at the moment. We clean the database after each test, running the tests will wipe out all your data! +**Beware**: We have no multiple database setup at the moment. We clean the +database after each test, running the tests will wipe out all your data! {% tabs %} diff --git a/backend/package.json b/backend/package.json index d861f9b6c..be5fb086d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,7 +11,7 @@ "lint": "eslint src --config .eslintrc.js", "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 DEBUG=true DISABLED_MIDDLEWARES=permissions,activityPub 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 2> /dev/null", "test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand", "test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/", "test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand", @@ -19,8 +19,8 @@ "test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --", "test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --", "db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js", - "db:reset": "cross-env DEBUG=true babel-node src/seed/reset-db.js", - "db:seed": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DEBUG=true DISABLED_MIDDLEWARES=permissions run-p --race dev db:script:seed" + "db:reset": "cross-env babel-node src/seed/reset-db.js", + "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", @@ -43,8 +43,8 @@ }, "dependencies": { "activitystrea.ms": "~2.1.3", - "apollo-cache-inmemory": "~1.6.0", - "apollo-client": "~2.5.1", + "apollo-cache-inmemory": "~1.6.1", + "apollo-client": "~2.6.1", "apollo-link-context": "~1.0.14", "apollo-link-http": "~1.5.14", "apollo-server": "~2.6.1", @@ -52,7 +52,7 @@ "cheerio": "~1.0.0-rc.3", "cors": "~2.8.5", "cross-env": "~5.2.0", - "date-fns": "2.0.0-alpha.27", + "date-fns": "2.0.0-alpha.29", "debug": "~4.1.1", "dotenv": "~8.0.0", "express": "~4.17.1", @@ -61,7 +61,7 @@ "graphql-custom-directives": "~0.2.14", "graphql-iso-date": "~3.6.1", "graphql-middleware": "~3.0.2", - "graphql-shield": "~5.3.5", + "graphql-shield": "~5.3.6", "graphql-tag": "~2.10.1", "graphql-yoga": "~1.17.4", "helmet": "~3.18.0", @@ -109,4 +109,4 @@ "prettier": "~1.17.1", "supertest": "~4.0.2" } -} \ No newline at end of file +} diff --git a/backend/src/config/index.js b/backend/src/config/index.js index 5cc455495..aed6f7c1c 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -23,7 +23,8 @@ export const serverConfigs = { export const developmentConfigs = { DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true', MOCKS: process.env.MOCKS === 'true', - DISABLED_MIDDLEWARES: process.env.DISABLED_MIDDLEWARES || '', + DISABLED_MIDDLEWARES: + (process.env.NODE_ENV !== 'production' && process.env.DISABLED_MIDDLEWARES) || '', } export default { diff --git a/backend/src/middleware/filterBubble/filterBubble.js b/backend/src/middleware/filterBubble/filterBubble.js new file mode 100644 index 000000000..bfdad5e2c --- /dev/null +++ b/backend/src/middleware/filterBubble/filterBubble.js @@ -0,0 +1,12 @@ +import replaceParams from './replaceParams' + +const replaceFilterBubbleParams = async (resolve, root, args, context, resolveInfo) => { + args = await replaceParams(args, context) + return resolve(root, args, context, resolveInfo) +} + +export default { + Query: { + Post: replaceFilterBubbleParams, + }, +} diff --git a/backend/src/middleware/filterBubble/filterBubble.spec.js b/backend/src/middleware/filterBubble/filterBubble.spec.js new file mode 100644 index 000000000..afe1df1c9 --- /dev/null +++ b/backend/src/middleware/filterBubble/filterBubble.spec.js @@ -0,0 +1,76 @@ +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../../jest/helpers' +import Factory from '../../seed/factories' + +const factory = Factory() + +const currentUserParams = { + email: 'you@example.org', + name: 'This is you', + password: '1234', +} +const followedAuthorParams = { + id: 'u2', + email: 'followed@example.org', + name: 'Followed User', + password: '1234', +} +const randomAuthorParams = { + email: 'someone@example.org', + name: 'Someone else', + password: 'else', +} + +beforeEach(async () => { + await Promise.all([ + factory.create('User', currentUserParams), + factory.create('User', followedAuthorParams), + factory.create('User', randomAuthorParams), + ]) + const [asYourself, asFollowedUser, asSomeoneElse] = await Promise.all([ + Factory().authenticateAs(currentUserParams), + Factory().authenticateAs(followedAuthorParams), + Factory().authenticateAs(randomAuthorParams), + ]) + await asYourself.follow({ id: 'u2', type: 'User' }) + await asFollowedUser.create('Post', { title: 'This is the post of a followed user' }) + await asSomeoneElse.create('Post', { title: 'This is some random post' }) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('FilterBubble middleware', () => { + describe('given an authenticated user', () => { + let authenticatedClient + + beforeEach(async () => { + const headers = await login(currentUserParams) + authenticatedClient = new GraphQLClient(host, { headers }) + }) + + describe('no filter bubble', () => { + it('returns all posts', async () => { + const query = '{ Post( filterBubble: {}) { title } }' + const expected = { + Post: [ + { title: 'This is some random post' }, + { title: 'This is the post of a followed user' }, + ], + } + await expect(authenticatedClient.request(query)).resolves.toEqual(expected) + }) + }) + + describe('filtering for posts of followed users only', () => { + it('returns only posts authored by followed users', async () => { + const query = '{ Post( filterBubble: { author: following }) { title } }' + const expected = { + Post: [{ title: 'This is the post of a followed user' }], + } + await expect(authenticatedClient.request(query)).resolves.toEqual(expected) + }) + }) + }) +}) diff --git a/backend/src/middleware/filterBubble/replaceParams.js b/backend/src/middleware/filterBubble/replaceParams.js new file mode 100644 index 000000000..a10b6c29d --- /dev/null +++ b/backend/src/middleware/filterBubble/replaceParams.js @@ -0,0 +1,31 @@ +import { UserInputError } from 'apollo-server' + +export default async function replaceParams(args, context) { + const { author = 'all' } = args.filterBubble || {} + const { user } = context + + if (author === 'following') { + if (!user) + throw new UserInputError( + "You are unauthenticated - I don't know any users you are following.", + ) + + const session = context.driver.session() + let { records } = await session.run( + 'MATCH(followed:User)<-[:FOLLOWS]-(u {id: $userId}) RETURN followed.id', + { userId: context.user.id }, + ) + const followedIds = records.map(record => record.get('followed.id')) + + // carefully override `id_in` + args.filter = args.filter || {} + args.filter.author = args.filter.author || {} + args.filter.author.id_in = followedIds + + session.close() + } + + delete args.filterBubble + + return args +} diff --git a/backend/src/middleware/filterBubble/replaceParams.spec.js b/backend/src/middleware/filterBubble/replaceParams.spec.js new file mode 100644 index 000000000..e14fda416 --- /dev/null +++ b/backend/src/middleware/filterBubble/replaceParams.spec.js @@ -0,0 +1,129 @@ +import replaceParams from './replaceParams.js' + +describe('replaceParams', () => { + let args + let context + let run + + let action = () => { + return replaceParams(args, context) + } + + beforeEach(() => { + args = {} + run = jest.fn().mockResolvedValue({ + records: [{ get: () => 1 }, { get: () => 2 }, { get: () => 3 }], + }) + context = { + driver: { + session: () => { + return { + run, + close: () => {}, + } + }, + }, + } + }) + + describe('args == ', () => { + describe('{}', () => { + it('does not crash', async () => { + await expect(action()).resolves.toEqual({}) + }) + }) + + describe('unauthenticated user', () => { + beforeEach(() => { + context.user = null + }) + + describe('{ filterBubble: { author: following } }', () => { + it('throws error', async () => { + args = { filterBubble: { author: 'following' } } + await expect(action()).rejects.toThrow('You are unauthenticated') + }) + }) + + describe('{ filterBubble: { author: all } }', () => { + it('removes filterBubble param', async () => { + const expected = {} + await expect(action()).resolves.toEqual(expected) + }) + + it('does not make database calls', async () => { + await action() + expect(run).not.toHaveBeenCalled() + }) + }) + }) + + describe('authenticated user', () => { + beforeEach(() => { + context.user = { id: 'u4711' } + }) + + describe('{ filterBubble: { author: following } }', () => { + beforeEach(() => { + args = { filterBubble: { author: 'following' } } + }) + + it('returns args object with resolved ids of followed users', async () => { + const expected = { filter: { author: { id_in: [1, 2, 3] } } } + await expect(action()).resolves.toEqual(expected) + }) + + it('makes database calls', async () => { + await action() + expect(run).toHaveBeenCalledTimes(1) + }) + + describe('given any additional filter args', () => { + describe('merges', () => { + it('empty filter object', async () => { + args.filter = {} + const expected = { filter: { author: { id_in: [1, 2, 3] } } } + await expect(action()).resolves.toEqual(expected) + }) + + it('filter.title', async () => { + args.filter = { title: 'bla' } + const expected = { filter: { title: 'bla', author: { id_in: [1, 2, 3] } } } + await expect(action()).resolves.toEqual(expected) + }) + + it('filter.author', async () => { + args.filter = { author: { name: 'bla' } } + const expected = { filter: { author: { name: 'bla', id_in: [1, 2, 3] } } } + await expect(action()).resolves.toEqual(expected) + }) + }) + }) + }) + + describe('{ filterBubble: { } }', () => { + it('removes filterBubble param', async () => { + const expected = {} + await expect(action()).resolves.toEqual(expected) + }) + + it('does not make database calls', async () => { + await action() + expect(run).not.toHaveBeenCalled() + }) + }) + + describe('{ filterBubble: { author: all } }', () => { + it('removes filterBubble param', async () => { + const expected = {} + await expect(action()).resolves.toEqual(expected) + }) + + it('does not make database calls', async () => { + await action() + expect(run).not.toHaveBeenCalled() + }) + }) + }) + }) +}) diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 754c59bbb..6bc7be000 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -13,6 +13,7 @@ import includedFields from './includedFieldsMiddleware' import orderBy from './orderByMiddleware' import validation from './validation' import notifications from './notifications' +import filterBubble from './filterBubble/filterBubble' export default schema => { const middlewares = { @@ -30,11 +31,13 @@ export default schema => { user: user, includedFields: includedFields, orderBy: orderBy, + filterBubble: filterBubble, } let order = [ 'permissions', 'activityPub', + 'filterBubble', 'password', 'dateTime', 'validation', @@ -50,7 +53,7 @@ export default schema => { ] // add permisions middleware at the first position (unless we're seeding) - if (CONFIG.DEBUG) { + if (CONFIG.DISABLED_MIDDLEWARES) { const disabledMiddlewares = CONFIG.DISABLED_MIDDLEWARES.split(',') order = order.filter(key => { return !disabledMiddlewares.includes(key) diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 6e23862ed..1179c3e20 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -1,3 +1,40 @@ +enum FilterBubbleAuthorEnum { + following + all +} + +input FilterBubble { + author: FilterBubbleAuthorEnum +} + +type Query { + Post( + id: ID + activityId: String + objectId: String + title: String + slug: String + content: String + contentExcerpt: String + image: String + imageUpload: Upload + visibility: Visibility + deleted: Boolean + disabled: Boolean + createdAt: String + updatedAt: String + commentsCount: Int + shoutedCount: Int + shoutedByCurrentUser: Boolean + _id: String + first: Int + offset: Int + orderBy: [_PostOrdering] + filter: _PostFilter + filterBubble: FilterBubble + ): [Post] +} + type Post { id: ID! activityId: String @@ -40,4 +77,4 @@ type Post { RETURN COUNT(u) >= 1 """ ) -} \ No newline at end of file +} diff --git a/backend/src/seed/reset-db.js b/backend/src/seed/reset-db.js index 5f4319f73..125d135d8 100644 --- a/backend/src/seed/reset-db.js +++ b/backend/src/seed/reset-db.js @@ -1,8 +1,7 @@ import { cleanDatabase } from './factories' -import CONFIG from './../config' -if (!CONFIG.DEBUG) { - throw new Error(`YOU CAN'T CLEAN THE DATABASE WITH DEBUG=${CONFIG.DEBUG}`) +if (process.env.NODE_ENV === 'production') { + throw new Error(`You cannot clean the database in production environment!`) } ;(async function() { diff --git a/backend/yarn.lock b/backend/yarn.lock index d56871d83..9c8fdc3c5 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1119,10 +1119,10 @@ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0" integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA== -"@types/yup@0.26.13": - version "0.26.13" - resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.13.tgz#0aeeba85231a34ddc68c74b3a2c64eeb2ccf68bf" - integrity sha512-sMMtb+c2xxf/FcK0kW36+0uuSWpNwvCBZYI7vpnD9J9Z6OYk09P4TmDkMWV+NWdi9Nzt2tUJjtpnPpkiUklBaw== +"@types/yup@0.26.14": + version "0.26.14" + resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.14.tgz#d31f3b9a04039cca70ebb4db4d6c7fc3f694e80b" + integrity sha512-OcBtVLHvYULVSltpuBdhFiVOKoSsOS58D872HydO93oBf3OdGq5zb+LnqGo18TNNSV2aW8hjIdS6H+wp68zFtQ== "@types/zen-observable@^0.5.3": version "0.5.4" @@ -1296,45 +1296,36 @@ apollo-cache-control@^0.1.0: dependencies: graphql-extensions "^0.0.x" -apollo-cache-inmemory@~1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.0.tgz#a106cdc520f0a043be2575372d5dbb7e4790254c" - integrity sha512-Mr86ucMsXnRH9YRvcuuy6kc3dtyRBuVSo8gdxp2sJVuUAtvQ6r/8E+ok2qX84em9ZBAYxoyvPnKeShhvcKiiDw== +apollo-cache-inmemory@~1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.1.tgz#536b6f366461f6264250041f9146363e2faa1d4c" + integrity sha512-c/WJjh9MTWcdussCTjLKufpPjTx3qOFkBPHIDOOpQ+U0B7K1PczPl9N0LaC4ir3wAWL7s4A0t2EKtoR+6UP92g== dependencies: - apollo-cache "^1.3.0" - apollo-utilities "^1.3.0" + apollo-cache "^1.3.1" + apollo-utilities "^1.3.1" optimism "^0.9.0" ts-invariant "^0.4.0" tslib "^1.9.3" -apollo-cache@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.2.1.tgz#aae71eb4a11f1f7322adc343f84b1a39b0693644" - integrity sha512-nzFmep/oKlbzUuDyz6fS6aYhRmfpcHWqNkkA9Bbxwk18RD6LXC4eZkuE0gXRX0IibVBHNjYVK+Szi0Yied4SpQ== +apollo-cache@1.3.1, apollo-cache@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.1.tgz#c015f93a9a7f32b3eeea0c471addd6e854da754c" + integrity sha512-BJ/Mehr3u6XCaHYSmgZ6DM71Fh30OkW6aEr828WjHvs+7i0RUuP51/PM7K6T0jPXtuw7UbArFFPZZsNgXnyyJA== dependencies: - apollo-utilities "^1.2.1" + apollo-utilities "^1.3.1" tslib "^1.9.3" -apollo-cache@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.0.tgz#de5c907cbd329440c9b0aafcbe8436391b9e6142" - integrity sha512-voPlvSIDA2pY3+7QwtXPs7o5uSNAVjUKwimyHWoiW0MIZtPxawtOV/Y+BL85R227JqcjPic1El+QToVR8l4ytQ== - dependencies: - apollo-utilities "^1.3.0" - tslib "^1.9.3" - -apollo-client@~2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.5.1.tgz#36126ed1d32edd79c3713c6684546a3bea80e6d1" - integrity sha512-MNcQKiqLHdGmNJ0rZ0NXaHrToXapJgS/5kPk0FygXt+/FmDCdzqcujI7OPxEC6e9Yw5S/8dIvOXcRNuOMElHkA== +apollo-client@~2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.1.tgz#fcf328618d6ad82b750a988bec113fe6edc8ba94" + integrity sha512-Tb6ZthPZUHlGqeoH1WC8Qg/tLnkk9H5+xj4e5nzOAC6dCOW3pVU9tYXscrWdmZ65UDUg1khvTNjrQgPhdf4aTQ== dependencies: "@types/zen-observable" "^0.8.0" - apollo-cache "1.2.1" + apollo-cache "1.3.1" apollo-link "^1.0.0" - apollo-link-dedup "^1.0.0" - apollo-utilities "1.2.1" + apollo-utilities "1.3.1" symbol-observable "^1.0.2" - ts-invariant "^0.2.1" + ts-invariant "^0.4.0" tslib "^1.9.3" zen-observable "^0.8.0" @@ -1398,13 +1389,6 @@ apollo-link-context@~1.0.14: apollo-link "^1.2.11" tslib "^1.9.3" -apollo-link-dedup@^1.0.0: - version "1.0.11" - resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.11.tgz#6f34ea748d2834850329ad03111ef18445232b05" - integrity sha512-RcvkXR0CNbQcsw6LdrPksGa+9YjZ1ghk0k2PKal6rSBCyyqzokcBawXOtoMN8q+0FLR1dGs5GnAQVeucQuY28g== - dependencies: - apollo-link "^1.2.4" - apollo-link-http-common@^0.2.13: version "0.2.13" resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.13.tgz#c688f6baaffdc7b269b2db7ae89dae7c58b5b350" @@ -1423,7 +1407,7 @@ apollo-link-http@~1.5.14: apollo-link-http-common "^0.2.13" tslib "^1.9.3" -apollo-link@^1.0.0, apollo-link@^1.2.11, apollo-link@^1.2.3, apollo-link@^1.2.4: +apollo-link@^1.0.0, apollo-link@^1.2.11, apollo-link@^1.2.3: version "1.2.11" resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.11.tgz#493293b747ad3237114ccd22e9f559e5e24a194d" integrity sha512-PQvRCg13VduLy3X/0L79M6uOpTh5iHdxnxYuo8yL7sJlWybKRJwsv4IcRBJpMFbChOOaHY7Og9wgPo6DLKDKDA== @@ -1575,21 +1559,13 @@ apollo-upload-server@^7.0.0: http-errors "^1.7.0" object-path "^0.11.4" -apollo-utilities@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.2.1.tgz#1c3a1ebf5607d7c8efe7636daaf58e7463b41b3c" - integrity sha512-Zv8Udp9XTSFiN8oyXOjf6PMHepD4yxxReLsl6dPUy5Ths7jti3nmlBzZUOxuTWRwZn0MoclqL7RQ5UEJN8MAxg== - dependencies: - fast-json-stable-stringify "^2.0.0" - ts-invariant "^0.2.1" - tslib "^1.9.3" - -apollo-utilities@^1.0.1, apollo-utilities@^1.2.1, apollo-utilities@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.0.tgz#9803724c07ac94ca11dc26397edb58735d2b0211" - integrity sha512-wQjV+FdWcTWmWUFlChG5rS0vHKy5OsXC6XlV9STRstQq6VbXANwHy6DHnTEQAfLXWAbNcPgBu+nBUpR3dFhwrA== +apollo-utilities@1.3.1, apollo-utilities@^1.0.1, apollo-utilities@^1.2.1, apollo-utilities@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.1.tgz#4c45f9b52783c324e2beef822700bdea374f82d1" + integrity sha512-P5cJ75rvhm9hcx9V/xCW0vlHhRd0S2icEcYPoRYNTc5djbynpuO+mQuJ4zMHgjNDpvvDxDfZxXTJ6ZUuJZodiQ== dependencies: fast-json-stable-stringify "^2.0.0" + lodash.isequal "^4.5.0" ts-invariant "^0.4.0" tslib "^1.9.3" @@ -2603,10 +2579,10 @@ data-urls@^1.0.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -date-fns@2.0.0-alpha.27: - version "2.0.0-alpha.27" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.27.tgz#5ecd4204ef0e7064264039570f6e8afbc014481c" - integrity sha512-cqfVLS+346P/Mpj2RpDrBv0P4p2zZhWWvfY5fuWrXNR/K38HaAGEkeOwb47hIpQP9Jr/TIxjZ2/sNMQwdXuGMg== +date-fns@2.0.0-alpha.29: + version "2.0.0-alpha.29" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.29.tgz#9d4a36e3ebba63d009e957fea8fdfef7921bc6cb" + integrity sha512-AIFZ0hG/1fdb7HZHTDyiEJdNiaFyZxXcx/kF8z3I9wxbhkN678KrrLSneKcsb0Xy5KqCA4wCIxmGpdVWSNZnpA== debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" @@ -3789,12 +3765,12 @@ graphql-request@~1.8.2: dependencies: cross-fetch "2.2.2" -graphql-shield@~5.3.5: - version "5.3.5" - resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.3.5.tgz#cba409f4c1714e107212cff0a1cb2d934273392b" - integrity sha512-3kmL9x+b85NK2ipH3VGudUgUo1vXy0Z44WXhnGi3b0T0peg53DOSlXBbZOO4PNh1AcULnUjYf+DpDrP8Uc97Gw== +graphql-shield@~5.3.6: + version "5.3.6" + resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.3.6.tgz#20061b02f77056c0870a623c530ef28a1bf4fff4" + integrity sha512-ihw/i4X+d1kpj1SVA6iBkVl2DZhPsI+xV08geR2TX3FWhpU7zakk/16yBzDRJTTCUgKsWfgyebrgIBsuhTwMnA== dependencies: - "@types/yup" "0.26.13" + "@types/yup" "0.26.14" lightercollective "^0.3.0" object-hash "^1.3.1" yup "^0.27.0" @@ -5254,6 +5230,11 @@ lodash.isboolean@^3.0.3: resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + lodash.isinteger@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" @@ -7560,13 +7541,6 @@ trunc-text@1.0.1: resolved "https://registry.yarnpkg.com/trunc-text/-/trunc-text-1.0.1.tgz#58f876d8ac59b224b79834bb478b8656e69622b5" integrity sha1-WPh22KxZsiS3mDS7R4uGVuaWIrU= -ts-invariant@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.2.1.tgz#3d587f9d6e3bded97bf9ec17951dd9814d5a9d3f" - integrity sha512-Z/JSxzVmhTo50I+LKagEISFJW3pvPCqsMWLamCTX8Kr3N5aMrnGOqcflbe5hLUzwjvgPfnLzQtHZv0yWQ+FIHg== - dependencies: - tslib "^1.9.3" - ts-invariant@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.3.2.tgz#89a2ffeb70879b777258df1df1c59383c35209b0" diff --git a/cypress/integration/common/post.js b/cypress/integration/common/post.js index f6a1bbedd..814159a34 100644 --- a/cypress/integration/common/post.js +++ b/cypress/integration/common/post.js @@ -1,25 +1,28 @@ -import { When, Then } from 'cypress-cucumber-preprocessor/steps' +import { When, Then } from "cypress-cucumber-preprocessor/steps"; -const narratorAvatar = 'https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg' +const narratorAvatar = + "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg"; -Then('I click on the {string} button', text => { - cy.get('button').contains(text).click() -}) +Then("I click on the {string} button", text => { + cy.get("button") + .contains(text) + .click(); +}); -Then('my comment should be successfully created', () => { - cy.get('.iziToast-message') - .contains('Comment Submitted') -}) +Then("my comment should be successfully created", () => { + cy.get(".iziToast-message").contains("Comment Submitted"); +}); -Then('I should see my comment', () => { - cy.get('div.comment p') - .should('contain', 'Human Connection rocks') - .get('.ds-avatar img') - .should('have.attr', 'src') - .and('contain', narratorAvatar) -}) +Then("I should see my comment", () => { + cy.get("div.comment p") + .should("contain", "Human Connection rocks") + .get(".ds-avatar img") + .should("have.attr", "src") + .and("contain", narratorAvatar) + .get("div p.ds-text span") + .should("contain", "today at"); +}); -Then('the editor should be cleared', () => { - cy.get('.ProseMirror p') - .should('have.class', 'is-empty') -}) +Then("the editor should be cleared", () => { + cy.get(".ProseMirror p").should("have.class", "is-empty"); +}); diff --git a/cypress/integration/common/profile.js b/cypress/integration/common/profile.js index cb5689f63..1df1e2652 100644 --- a/cypress/integration/common/profile.js +++ b/cypress/integration/common/profile.js @@ -12,14 +12,16 @@ Then('I should be able to change my profile picture', () => { cy.fixture(avatarUpload, 'base64').then(fileContent => { cy.get('#customdropzone').upload( { fileContent, fileName: avatarUpload, mimeType: 'image/png' }, - { subjectType: 'drag-n-drop' }, + { subjectType: 'drag-n-drop' } ) }) - cy.get('#customdropzone') - .should('have.attr', 'style') + cy.get('.profile-avatar img') + .should('have.attr', 'src') .and('contains', 'onourjourney') - cy.contains('.iziToast-message', 'Upload successful') - .should('have.length', 1) + cy.contains('.iziToast-message', 'Upload successful').should( + 'have.length', + 1 + ) }) When("I visit another user's profile page", () => { @@ -31,4 +33,4 @@ Then('I cannot upload a picture', () => { .children() .should('not.have.id', 'customdropzone') .should('have.class', 'ds-avatar') -}) \ No newline at end of file +}) diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 387f33ac0..8f5bcc8ea 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -230,7 +230,7 @@ When('I type in the following text:', text => { Then('the post shows up on the landing page at position {int}', index => { cy.openPage('landing') - const selector = `:nth-child(${index}) > .ds-card > .ds-card-content` + const selector = `.post-card:nth-child(${index}) > .ds-card-content` cy.get(selector).should('contain', lastPost.title) cy.get(selector).should('contain', lastPost.content) }) diff --git a/neo4j/.env.template b/neo4j/.env.template new file mode 100644 index 000000000..c58edee0e --- /dev/null +++ b/neo4j/.env.template @@ -0,0 +1,2 @@ +NEO4J_USERNAME=neo4j +NEO4J_PASSWORD=letmein diff --git a/neo4j/.gitignore b/neo4j/.gitignore new file mode 100644 index 000000000..4c49bd78f --- /dev/null +++ b/neo4j/.gitignore @@ -0,0 +1 @@ +.env diff --git a/neo4j/Dockerfile b/neo4j/Dockerfile index e94a89431..2c106882f 100644 --- a/neo4j/Dockerfile +++ b/neo4j/Dockerfile @@ -1,3 +1,11 @@ FROM neo4j:3.5.5 +LABEL Description="Neo4J database of the Social Network Human-Connection.org with preinstalled database constraints and indices" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)" + +ARG BUILD_COMMIT +ENV BUILD_COMMIT=$BUILD_COMMIT + RUN wget https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.5.0.1/apoc-3.5.0.1-all.jar -P plugins/ -COPY migrate.sh /usr/local/bin/migrate +RUN apk add --no-cache --quiet procps +COPY db_setup.sh /usr/local/bin/db_setup +COPY entrypoint.sh /docker-entrypoint-wrapper.sh +ENTRYPOINT ["/docker-entrypoint-wrapper.sh"] diff --git a/neo4j/README.md b/neo4j/README.md new file mode 100644 index 000000000..379a89eec --- /dev/null +++ b/neo4j/README.md @@ -0,0 +1,64 @@ +# Neo4J + +Human Connection is a social network. Using a graph based database which can +model nodes and edges natively - a network - feels like an obvious choice. We +decided to use [Neo4j](https://neo4j.com/), the currently most used graph +database available. The community edition of Neo4J is Free and Open Source and +we try our best to keep our application compatible with the community edition +only. + +## Installation with Docker + +Run: + +```bash +docker-compose up +``` + +You can access Neo4J through [http://localhost:7474/](http://localhost:7474/) +for an interactive cypher shell and a visualization of the graph. + +## Installation without Docker + +Install community edition of [Neo4J]() along with the plugin +[Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures) on your system. + +To do so, go to [releases](https://neo4j.com/download-center/#releases), choose +"Community Server", download the installation files for you operation system +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. + +### Alternatives + +You can download [Neo4j Desktop](https://neo4j.com/download/) and run locally +for development, spin up a +[hosted Neo4j Sandbox instance](https://neo4j.com/download/), run Neo4j in one +of the [many cloud options](https://neo4j.com/developer/guide-cloud-deployment/), +[spin up Neo4j in a Docker container](https://neo4j.com/developer/docker/), +on Archlinux you can install [neo4j-community from AUR](https://aur.archlinux.org/packages/neo4j-community/) +or on Debian-based systems install [Neo4j from the Debian Repository](http://debian.neo4j.org/). +Just be sure to update the Neo4j connection string and credentials accordingly +in `backend/.env`. + +Start Neo4J and confirm the database is running at [http://localhost:7474](http://localhost:7474). + +## Database Indices and Constraints + +If you are not running our dedicated Neo4J [docker image](https://hub.docker.com/r/humanconnection/neo4j), +which is the case if you setup Neo4J locally without docker, then you have to +setup unique indices and database constraints manually. + +If you have `cypher-shell` available with your local installation of neo4j you +can run: + +```bash +# in folder neo4j/ +$ cp .env.template .env +$ ./db_setup.sh +``` + +Otherwise if you don't have `cypher-shell` available, simply copy the cypher +statements [from the script](./neo4j/db_setup.sh) and paste the scripts into your +database [browser frontend](http://localhost:7474). diff --git a/neo4j/db_setup.sh b/neo4j/db_setup.sh new file mode 100755 index 000000000..21ed54571 --- /dev/null +++ b/neo4j/db_setup.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +ENV_FILE=$(dirname "$0")/.env +[[ -f "$ENV_FILE" ]] && source "$ENV_FILE" + +if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then + echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables." + echo "Setting up database constraints and indexes will probably fail because of authentication errors." + echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container" +fi + +until echo 'RETURN "Connection successful" as info;' | cypher-shell +do + echo "Connecting to neo4j failed, trying again..." + sleep 1 +done + +echo ' +RETURN "Here is a list of indexes and constraints BEFORE THE SETUP:" as info; +CALL db.indexes(); +' | cypher-shell + +echo ' +CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]); +CREATE CONSTRAINT ON (p:Post) ASSERT p.id IS UNIQUE; +CREATE CONSTRAINT ON (c:Comment) ASSERT c.id IS UNIQUE; +CREATE CONSTRAINT ON (c:Category) ASSERT c.id IS UNIQUE; +CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE; +CREATE CONSTRAINT ON (o:Organization) ASSERT o.id IS UNIQUE; +CREATE CONSTRAINT ON (t:Tag) ASSERT t.id IS UNIQUE; + + +CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE; +CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE; +CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE; +CREATE CONSTRAINT ON (o:Organization) ASSERT o.slug IS UNIQUE; +' | cypher-shell + +echo ' +RETURN "Setting up all the indexes and constraints seems to have been successful. Here is a list AFTER THE SETUP:" as info; +CALL db.indexes(); +' | cypher-shell diff --git a/neo4j/entrypoint.sh b/neo4j/entrypoint.sh new file mode 100755 index 000000000..f9c1afbe1 --- /dev/null +++ b/neo4j/entrypoint.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# credits: https://github.com/javamonkey79 +# https://github.com/neo4j/docker-neo4j/issues/166 + +# turn on bash's job control +set -m + +# Start the primary process and put it in the background +/docker-entrypoint.sh neo4j & + +# Start the helper process +db_setup + +# the my_helper_process might need to know how to wait on the +# primary process to start before it does its work and returns + + +# now we bring the primary process back into the foreground +# and leave it there +fg %1 diff --git a/neo4j/migrate.sh b/neo4j/migrate.sh deleted file mode 100755 index 6f3361b8a..000000000 --- a/neo4j/migrate.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -# If the user has the password `neo4j` this is a strong indicator, that we are -# the initial default user. Before we can create constraints, we have to change -# the default password. This is a security feature of neo4j. -if echo ":exit" | cypher-shell --password neo4j 2> /dev/null ; then - if [[ -z "${NEO4J_PASSWORD}" ]]; then - echo "NEO4J_PASSWORD environment variable is undefined. I cannot set the initial password." - else - echo "CALL dbms.security.changePassword('${NEO4J_PASSWORD}');" | cypher-shell --password neo4j - fi -fi - -set -e - -echo ' -CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]); -CREATE CONSTRAINT ON (p:Post) ASSERT p.id IS UNIQUE; -CREATE CONSTRAINT ON (c:Comment) ASSERT c.id IS UNIQUE; -CREATE CONSTRAINT ON (c:Category) ASSERT c.id IS UNIQUE; -CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE; -CREATE CONSTRAINT ON (o:Organization) ASSERT o.id IS UNIQUE; -CREATE CONSTRAINT ON (t:Tag) ASSERT t.id IS UNIQUE; - - -CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE; -CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE; -CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE; -CREATE CONSTRAINT ON (o:Organization) ASSERT o.slug IS UNIQUE; -' | cypher-shell - -echo "Successfully created all indices and unique constraints:" -echo 'CALL db.indexes();' | cypher-shell diff --git a/scripts/docker_push.sh b/scripts/docker_push.sh index 9aa9a8e2a..c70367005 100755 --- a/scripts/docker_push.sh +++ b/scripts/docker_push.sh @@ -2,7 +2,9 @@ echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-backend:latest $TRAVIS_BUILD_DIR/backend docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-web:latest $TRAVIS_BUILD_DIR/webapp -docker build -t humanconnection/nitro-maintenance-worker:latest $TRAVIS_BUILD_DIR/deployment/legacy-migration/maintenance-worker +docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT -t humanconnection/neo4j:latest $TRAVIS_BUILD_DIR/neo4j +docker build -t humanconnection/maintenance-worker:latest $TRAVIS_BUILD_DIR/deployment/legacy-migration/maintenance-worker docker push humanconnection/nitro-backend:latest docker push humanconnection/nitro-web:latest -docker push humanconnection/nitro-maintenance-worker:latest +docker push humanconnection/neo4j:latest +docker push humanconnection/maintenance-worker:latest \ No newline at end of file diff --git a/webapp/Dockerfile b/webapp/Dockerfile index fc3b17779..feba44c36 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12.3.1-alpine as base +FROM node:12.4-alpine as base LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)" EXPOSE 3000 diff --git a/webapp/components/Comment.vue b/webapp/components/Comment.vue index 862b95545..4796dd783 100644 --- a/webapp/components/Comment.vue +++ b/webapp/components/Comment.vue @@ -7,7 +7,7 @@