diff --git a/backend/public/uploads/.gitkeep b/backend/public/uploads/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/middleware/notifications/extractMentions.js b/backend/src/middleware/notifications/extractIds/index.js similarity index 93% rename from backend/src/middleware/notifications/extractMentions.js rename to backend/src/middleware/notifications/extractIds/index.js index f2b28444f..d5b1823f2 100644 --- a/backend/src/middleware/notifications/extractMentions.js +++ b/backend/src/middleware/notifications/extractIds/index.js @@ -2,6 +2,7 @@ import cheerio from 'cheerio' const ID_REGEX = /\/profile\/([\w\-.!~*'"(),]+)/g export default function (content) { + if (!content) return [] const $ = cheerio.load(content) const urls = $('.mention').map((_, el) => { return $(el).attr('href') diff --git a/backend/src/middleware/notifications/extractMentions.spec.js b/backend/src/middleware/notifications/extractIds/spec.js similarity index 93% rename from backend/src/middleware/notifications/extractMentions.spec.js rename to backend/src/middleware/notifications/extractIds/spec.js index 625b1d8fe..73c0ce0a1 100644 --- a/backend/src/middleware/notifications/extractMentions.spec.js +++ b/backend/src/middleware/notifications/extractIds/spec.js @@ -1,6 +1,12 @@ -import extractIds from './extractMentions' +import extractIds from '.' + +describe('extractIds', () => { + describe('content undefined', () => { + it('returns empty array', () => { + expect(extractIds()).toEqual([]) + }) + }) -describe('extract', () => { describe('searches through links', () => { it('ignores links without .mention class', () => { const content = '
Something inspirational about @bob-der-baumeister and @jenny-rostock.
' diff --git a/backend/src/middleware/notifications/index.js b/backend/src/middleware/notifications/index.js index 942eb588d..65cebe253 100644 --- a/backend/src/middleware/notifications/index.js +++ b/backend/src/middleware/notifications/index.js @@ -1,4 +1,4 @@ -import extractIds from './extractMentions' +import extractIds from './extractIds' const notify = async (resolve, root, args, context, resolveInfo) => { // extract user ids before xss-middleware removes link classes diff --git a/backend/src/resolvers/fileUpload/index.js b/backend/src/resolvers/fileUpload/index.js new file mode 100644 index 000000000..85bdf920b --- /dev/null +++ b/backend/src/resolvers/fileUpload/index.js @@ -0,0 +1,27 @@ +import { createWriteStream } from 'fs' +import path from 'path' +import slug from 'slug' + +const storeUpload = ({ createReadStream, fileLocation }) => + new Promise((resolve, reject) => + createReadStream() + .pipe(createWriteStream(`public${fileLocation}`)) + .on('finish', resolve) + .on('error', reject) + ) + +export default async function fileUpload (params, { file, url }, uploadCallback = storeUpload) { + const upload = params[file] + + if (upload) { + const { createReadStream, filename } = await upload + const { name } = path.parse(filename) + const fileLocation = `/uploads/${Date.now()}-${slug(name)}` + await uploadCallback({ createReadStream, fileLocation }) + delete params[file] + + params[url] = fileLocation + } + + return params +} diff --git a/backend/src/resolvers/fileUpload/spec.js b/backend/src/resolvers/fileUpload/spec.js new file mode 100644 index 000000000..798e4f9c5 --- /dev/null +++ b/backend/src/resolvers/fileUpload/spec.js @@ -0,0 +1,56 @@ +import fileUpload from '.' + +describe('fileUpload', () => { + let params + let uploadCallback + + beforeEach(() => { + params = { + uploadAttribute: { + filename: 'avatar.jpg', + mimetype: 'image/jpeg', + encoding: '7bit', + createReadStream: jest.fn() + } + } + uploadCallback = jest.fn() + }) + + it('calls uploadCallback', async () => { + await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback) + expect(uploadCallback).toHaveBeenCalled() + }) + + describe('file name', () => { + it('saves the upload url in params[url]', async () => { + await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback) + expect(params.attribute).toMatch(/^\/uploads\/\d+-avatar$/) + }) + + it('uses the name without file ending', async () => { + params.uploadAttribute.filename = 'somePng.png' + await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback) + expect(params.attribute).toMatch(/^\/uploads\/\d+-somePng/) + }) + + it('creates a url safe name', async () => { + params.uploadAttribute.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg?foo- bar' + await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback) + expect(params.attribute).toMatch(/^\/uploads\/\d+-foo-bar-avatar$/) + }) + + describe('in case of duplicates', () => { + it('creates unique names to avoid overwriting existing files', async () => { + const { attribute: first } = await fileUpload({ + ...params + }, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback) + + await new Promise(resolve => setTimeout(resolve, 1000)) + const { attribute: second } = await fileUpload({ + ...params + }, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback) + expect(first).not.toEqual(second) + }) + }) + }) +}) diff --git a/backend/src/resolvers/posts.js b/backend/src/resolvers/posts.js index 5b06c38fa..128a83fc3 100644 --- a/backend/src/resolvers/posts.js +++ b/backend/src/resolvers/posts.js @@ -1,8 +1,15 @@ import { neo4jgraphql } from 'neo4j-graphql-js' +import fileUpload from './fileUpload' export default { Mutation: { + UpdatePost: async (object, params, context, resolveInfo) => { + params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) + return neo4jgraphql(object, params, context, resolveInfo, false) + }, + CreatePost: async (object, params, context, resolveInfo) => { + params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) const result = await neo4jgraphql(object, params, context, resolveInfo, false) const session = context.driver.session() diff --git a/backend/src/resolvers/users.js b/backend/src/resolvers/users.js index 896d8aa00..01beae522 100644 --- a/backend/src/resolvers/users.js +++ b/backend/src/resolvers/users.js @@ -1,28 +1,14 @@ import { neo4jgraphql } from 'neo4j-graphql-js' -import { createWriteStream } from 'fs' - -const storeUpload = ({ stream, fileLocation }) => - new Promise((resolve, reject) => - stream - .pipe(createWriteStream(`public${fileLocation}`)) - .on('finish', resolve) - .on('error', reject) - ) +import fileUpload from './fileUpload' export default { Mutation: { UpdateUser: async (object, params, context, resolveInfo) => { - const { avatarUpload } = params - - if (avatarUpload) { - const { createReadStream, filename } = await avatarUpload - const stream = createReadStream() - const fileLocation = `/uploads/${filename}` - await storeUpload({ stream, fileLocation }) - delete params.avatarUpload - - params.avatar = fileLocation - } + params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' }) + return neo4jgraphql(object, params, context, resolveInfo, false) + }, + CreateUser: async (object, params, context, resolveInfo) => { + params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' }) return neo4jgraphql(object, params, context, resolveInfo, false) } } diff --git a/backend/src/schema.graphql b/backend/src/schema.graphql index d364ba4b3..a581d287c 100644 --- a/backend/src/schema.graphql +++ b/backend/src/schema.graphql @@ -179,6 +179,7 @@ type Post { content: String! contentExcerpt: String image: String + imageUpload: Upload visibility: VisibilityEnum deleted: Boolean disabled: Boolean diff --git a/webapp/components/Upload/index.vue b/webapp/components/Upload/index.vue index 9bda28f34..783b91b91 100644 --- a/webapp/components/Upload/index.vue +++ b/webapp/components/Upload/index.vue @@ -37,7 +37,7 @@ export default { backgroundImage() { const { avatar } = this.user || {} return { - backgroundImage: `url(/api/${avatar})` + backgroundImage: `url(/api${avatar})` } } }, @@ -119,6 +119,10 @@ export default { padding: 40px; } +#customdropzone:hover { + cursor: pointer; +} + #customdropzone .dz-preview { width: 160px; display: flex;