mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #3899 from Human-Connection/export-user-data
feat: Export User Data
This commit is contained in:
commit
5018a572ae
@ -105,6 +105,7 @@ export default shield(
|
||||
blockedUsers: isAuthenticated,
|
||||
notifications: isAuthenticated,
|
||||
Donations: isAuthenticated,
|
||||
userData: isAuthenticated,
|
||||
},
|
||||
Mutation: {
|
||||
'*': deny,
|
||||
|
||||
30
backend/src/schema/resolvers/userData.js
Normal file
30
backend/src/schema/resolvers/userData.js
Normal 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()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
211
backend/src/schema/resolvers/userData.spec.js
Normal file
211
backend/src/schema/resolvers/userData.spec.js
Normal 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' },
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
10
backend/src/schema/types/type/UserData.gql
Normal file
10
backend/src/schema/types/type/UserData.gql
Normal file
@ -0,0 +1,10 @@
|
||||
type UserData {
|
||||
user: User!
|
||||
posts: [Post]
|
||||
}
|
||||
|
||||
type Query {
|
||||
userData(
|
||||
id: ID
|
||||
): UserData
|
||||
}
|
||||
5
webapp/assets/_new/icons/svgs/download.svg
Normal file
5
webapp/assets/_new/icons/svgs/download.svg
Normal 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 |
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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`
|
||||
},
|
||||
} */
|
||||
]
|
||||
},
|
||||
},
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user