Merge pull request #3899 from Human-Connection/export-user-data

feat: Export User Data
This commit is contained in:
Wolfgang Huß 2020-10-14 18:00:48 +02:00 committed by GitHub
commit 5018a572ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 380 additions and 19 deletions

View File

@ -105,6 +105,7 @@ export default shield(
blockedUsers: isAuthenticated,
notifications: isAuthenticated,
Donations: isAuthenticated,
userData: isAuthenticated,
},
Mutation: {
'*': deny,

View File

@ -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()
}
},
},
}

View File

@ -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' },
},
],
},
]),
},
},
})
})
})
})

View File

@ -0,0 +1,10 @@
type UserData {
user: User!
posts: [Post]
}
type Query {
userData(
id: ID
): UserData
}

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>download</title>
<path d="M15 4h2v16.563l5.281-5.281 1.438 1.438-7 7-0.719 0.688-0.719-0.688-7-7 1.438-1.438 5.281 5.281v-16.563zM7 26h18v2h-18v-2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 302 B

View File

@ -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
}
}
}
}
`
}

View File

@ -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": {

View File

@ -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": {

View File

@ -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`
},
} */
]
},
},

View File

@ -1,16 +1,89 @@
<template>
<base-card>
<h2 class="title">{{ $t('settings.download.name') }}</h2>
<hc-empty icon="tasks" message="Coming Soon…" />
<base-button @click="onClick(jsonData)" icon="download" secondary filled :disabled="loading">
{{ $t('settings.download.json') }}
</base-button>
<ds-space margin="large" />
<ds-text>{{ $t('settings.download.description') }}</ds-text>
<ds-space margin="large" />
<base-card v-for="image in imageList" :key="image.key">
<a :href="image.url" @click.prevent="downloadImage(image)">{{ image.title }}</a>
<ds-space margin="xxx-small" />
</base-card>
</base-card>
</template>
<script>
import HcEmpty from '~/components/Empty/Empty'
import { mapGetters } from 'vuex'
import { userDataQuery } from '~/graphql/User'
import BaseButton from '~/components/_new/generic/BaseButton/BaseButton.vue'
import isEmpty from 'lodash/isEmpty'
export default {
components: {
HcEmpty,
BaseButton,
},
data() {
return {
userData: {},
loading: true,
imageList: [],
}
},
computed: {
...mapGetters({
user: 'auth/user',
}),
jsonData() {
return { data: JSON.stringify(this.userData, null, 2), type: 'json' }
},
},
methods: {
onClick(method) {
var fileURL = window.URL.createObjectURL(new Blob([method.data]))
var fileLink = document.createElement('a')
fileLink.href = fileURL
fileLink.setAttribute('download', 'userData.' + method.type)
document.body.appendChild(fileLink)
fileLink.click()
},
downloadImage({ url }) {
this.$axios.get(url, { responseType: 'blob' }).then((response) => {
const blob = new Blob([response.data])
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = url.replace(/^.+\//g, '')
link.click()
URL.revokeObjectURL(link.href)
})
},
},
apollo: {
queryUserData: {
query() {
return userDataQuery()
},
variables() {
return { id: this.user.id }
},
update({ userData }) {
this.userData = userData
this.loading = false
if (isEmpty(this.userData)) return null
const userId = this.userData.user.id
if (isEmpty(userId)) return null
this.imageList = this.userData.posts
.filter((post) => post.author.id === userId && post.image)
.map((post) => {
const obj = {}
obj.key = post.id
obj.url = post.image.url
obj.title = post.title
return obj
})
},
},
},
}
</script>