diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js
index 2c8d7ff63..f4f8c654b 100644
--- a/backend/src/middleware/permissionsMiddleware.js
+++ b/backend/src/middleware/permissionsMiddleware.js
@@ -105,6 +105,7 @@ export default shield(
blockedUsers: isAuthenticated,
notifications: isAuthenticated,
Donations: isAuthenticated,
+ userData: isAuthenticated,
},
Mutation: {
'*': deny,
diff --git a/backend/src/schema/resolvers/userData.js b/backend/src/schema/resolvers/userData.js
new file mode 100644
index 000000000..c73c46962
--- /dev/null
+++ b/backend/src/schema/resolvers/userData.js
@@ -0,0 +1,30 @@
+export default {
+ Query: {
+ userData: async (object, args, context, resolveInfo) => {
+ const id = context.user.id
+ const cypher = `
+ MATCH (user:User { id: $id })
+ WITH user
+ OPTIONAL MATCH (p:Post)
+ WHERE (p)<-[:COMMENTS]-(:Comment)<-[:WROTE]-(user)
+ OR (user)-[:WROTE]->(p)
+ RETURN { user: properties(user), posts: collect(properties(p)) }
+ AS result
+ `
+ const session = context.driver.session()
+ const resultPromise = session.readTransaction(async (transaction) => {
+ const transactionResponse = transaction.run(cypher, {
+ id,
+ })
+ return transactionResponse
+ })
+
+ try {
+ const result = await resultPromise
+ return result.records[0].get('result')
+ } finally {
+ session.close()
+ }
+ },
+ },
+}
diff --git a/backend/src/schema/resolvers/userData.spec.js b/backend/src/schema/resolvers/userData.spec.js
new file mode 100644
index 000000000..db235f5e4
--- /dev/null
+++ b/backend/src/schema/resolvers/userData.spec.js
@@ -0,0 +1,211 @@
+import Factory, { cleanDatabase } from '../../db/factories'
+import { gql } from '../../helpers/jest'
+import { getNeode, getDriver } from '../../db/neo4j'
+import createServer from '../../server'
+import { createTestClient } from 'apollo-server-testing'
+
+let query, authenticatedUser
+
+const driver = getDriver()
+const neode = getNeode()
+
+beforeAll(async () => {
+ await cleanDatabase()
+ const user = await Factory.build('user', {
+ id: 'a-user',
+ name: 'John Doe',
+ slug: 'john-doe',
+ })
+ await Factory.build('user', {
+ id: 'o-user',
+ name: 'Unauthenticated User',
+ slug: 'unauthenticated-user',
+ })
+ authenticatedUser = await user.toJson()
+ const { server } = createServer({
+ context: () => {
+ return {
+ driver,
+ neode,
+ user: authenticatedUser,
+ }
+ },
+ })
+ query = createTestClient(server).query
+})
+
+afterAll(async () => {
+ await cleanDatabase()
+})
+
+const userDataQuery = gql`
+ query($id: ID!) {
+ userData(id: $id) {
+ user {
+ id
+ name
+ slug
+ }
+ posts {
+ id
+ title
+ content
+ comments {
+ content
+ author {
+ slug
+ }
+ }
+ }
+ }
+ }
+`
+
+describe('resolvers/userData', () => {
+ let variables = { id: 'a-user' }
+
+ describe('given one authenticated user who did not write anything so far', () => {
+ it("returns the user's data and no posts", async () => {
+ await expect(query({ query: userDataQuery, variables })).resolves.toMatchObject({
+ data: {
+ userData: {
+ user: {
+ id: 'a-user',
+ name: 'John Doe',
+ slug: 'john-doe',
+ },
+ posts: [],
+ },
+ },
+ })
+ })
+
+ describe('the user writes a post', () => {
+ beforeAll(async () => {
+ await Factory.build(
+ 'post',
+ {
+ id: 'a-post',
+ title: 'A post',
+ content: 'A post',
+ },
+ { authorId: 'a-user' },
+ )
+ })
+
+ it("returns the user's data and the post", async () => {
+ await expect(query({ query: userDataQuery, variables })).resolves.toMatchObject({
+ data: {
+ userData: {
+ user: {
+ id: 'a-user',
+ name: 'John Doe',
+ slug: 'john-doe',
+ },
+ posts: [
+ {
+ id: 'a-post',
+ title: 'A post',
+ content: 'A post',
+ },
+ ],
+ },
+ },
+ })
+ })
+
+ describe('the user comments another post', () => {
+ beforeAll(async () => {
+ await Factory.build(
+ 'post',
+ {
+ id: 'b-post',
+ title: 'B post',
+ content: 'B post',
+ },
+ { authorId: 'o-user' },
+ )
+ await Factory.build(
+ 'comment',
+ {
+ content: 'A comment to post B',
+ },
+ {
+ postId: 'b-post',
+ authorId: 'a-user',
+ },
+ )
+ })
+
+ it('returns the written post and the commented post', async () => {
+ await expect(query({ query: userDataQuery, variables })).resolves.toMatchObject({
+ data: {
+ userData: {
+ user: {
+ id: 'a-user',
+ name: 'John Doe',
+ slug: 'john-doe',
+ },
+ posts: expect.arrayContaining([
+ {
+ id: 'a-post',
+ title: 'A post',
+ content: 'A post',
+ comments: [],
+ },
+ {
+ id: 'b-post',
+ title: 'B post',
+ content: 'B post',
+ comments: [
+ {
+ content: 'A comment to post B',
+ author: { slug: 'john-doe' },
+ },
+ ],
+ },
+ ]),
+ },
+ },
+ })
+ })
+ })
+ })
+ })
+
+ describe('try to request data of another user', () => {
+ variables = { id: 'o-user' }
+ it('returns the data of the authenticated user', async () => {
+ await expect(query({ query: userDataQuery, variables })).resolves.toMatchObject({
+ data: {
+ userData: {
+ user: {
+ id: 'a-user',
+ name: 'John Doe',
+ slug: 'john-doe',
+ },
+ posts: expect.arrayContaining([
+ {
+ id: 'a-post',
+ title: 'A post',
+ content: 'A post',
+ comments: [],
+ },
+ {
+ id: 'b-post',
+ title: 'B post',
+ content: 'B post',
+ comments: [
+ {
+ content: 'A comment to post B',
+ author: { slug: 'john-doe' },
+ },
+ ],
+ },
+ ]),
+ },
+ },
+ })
+ })
+ })
+})
diff --git a/backend/src/schema/types/type/UserData.gql b/backend/src/schema/types/type/UserData.gql
new file mode 100644
index 000000000..60ad5c12f
--- /dev/null
+++ b/backend/src/schema/types/type/UserData.gql
@@ -0,0 +1,10 @@
+type UserData {
+ user: User!
+ posts: [Post]
+}
+
+type Query {
+ userData(
+ id: ID
+ ): UserData
+}
diff --git a/webapp/assets/_new/icons/svgs/download.svg b/webapp/assets/_new/icons/svgs/download.svg
new file mode 100644
index 000000000..b988fa171
--- /dev/null
+++ b/webapp/assets/_new/icons/svgs/download.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js
index 3b015dacc..4b3a67775 100644
--- a/webapp/graphql/User.js
+++ b/webapp/graphql/User.js
@@ -292,3 +292,32 @@ export const currentUserCountQuery = () => gql`
}
}
`
+
+export const userDataQuery = (i18n) => {
+ return gql`
+ ${userFragment}
+ ${postFragment}
+ ${commentFragment}
+ query($id: ID!) {
+ userData(id: $id) {
+ user {
+ ...user
+ }
+ posts {
+ ...post
+ categories {
+ id
+ name
+ }
+ comments {
+ author {
+ id
+ slug
+ }
+ ...comment
+ }
+ }
+ }
+ }
+ `
+}
diff --git a/webapp/locales/de.json b/webapp/locales/de.json
index adee8921c..cff6af3f7 100644
--- a/webapp/locales/de.json
+++ b/webapp/locales/de.json
@@ -648,6 +648,8 @@
"success": "Konto erfolgreich gelöscht!"
},
"download": {
+ "description": "Klicke auf den Knopf oben, um den Inhalt deiner Beiträge und Kommentare herunterzuladen. Um die Bilder der Beiträge herunterzuladen, musst du auf den jeweiligen Link unten klicken.",
+ "json": "als JSON",
"name": "Daten herunterladen"
},
"email": {
diff --git a/webapp/locales/en.json b/webapp/locales/en.json
index 8959e3830..bbb779d2d 100644
--- a/webapp/locales/en.json
+++ b/webapp/locales/en.json
@@ -648,6 +648,8 @@
"success": "Account successfully deleted!"
},
"download": {
+ "description": "Click on the button above to download the content of your posts and comments. To download the images of your posts, you have to click on the corresponding link below.",
+ "json": "as JSON",
"name": "Download Data"
},
"email": {
diff --git a/webapp/pages/settings.vue b/webapp/pages/settings.vue
index 950652028..6bd78b701 100644
--- a/webapp/pages/settings.vue
+++ b/webapp/pages/settings.vue
@@ -51,32 +51,30 @@ export default {
name: this.$t('settings.embeds.name'),
path: `/settings/embeds`,
},
+ {
+ name: this.$t('settings.download.name'),
+ path: `/settings/data-download`,
+ },
{
name: this.$t('settings.deleteUserAccount.name'),
path: `/settings/delete-account`,
},
// TODO implement
/* {
- name: this.$t('settings.invites.name'),
- path: `/settings/invites`
- }, */
+ name: this.$t('settings.invites.name'),
+ path: `/settings/invites`
+ }, */
// TODO implement
/* {
- name: this.$t('settings.download.name'),
- path: `/settings/data-download`
- }, */
- // TODO implement
+ name: this.$t('settings.organizations.name'),
+ path: `/settings/my-organizations`
+ }, */
// TODO implement
/* {
- name: this.$t('settings.organizations.name'),
- path: `/settings/my-organizations`
- }, */
- // TODO implement
- /* {
- name: this.$t('settings.languages.name'),
- path: `/settings/languages`
- },
- } */
+ name: this.$t('settings.languages.name'),
+ path: `/settings/languages`
+ },
+ } */
]
},
},
diff --git a/webapp/pages/settings/data-download.vue b/webapp/pages/settings/data-download.vue
index b7951182d..9cf1b1f8e 100644
--- a/webapp/pages/settings/data-download.vue
+++ b/webapp/pages/settings/data-download.vue
@@ -1,16 +1,89 @@
{{ $t('settings.download.name') }}
-
+
+ {{ $t('settings.download.json') }}
+
+
+ {{ $t('settings.download.description') }}
+
+
+ {{ image.title }}
+
+