mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #740 from Human-Connection/404-delete-user-account-and-data
Delete my User Account functionality
This commit is contained in:
commit
b5fb7cb34b
@ -93,6 +93,12 @@ const isAuthor = rule({
|
|||||||
return authorId === user.id
|
return authorId === user.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isDeletingOwnAccount = rule({
|
||||||
|
cache: 'no_cache',
|
||||||
|
})(async (parent, args, context, info) => {
|
||||||
|
return context.user.id === args.id
|
||||||
|
})
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
const permissions = shield(
|
const permissions = shield(
|
||||||
{
|
{
|
||||||
@ -140,6 +146,7 @@ const permissions = shield(
|
|||||||
disable: isModerator,
|
disable: isModerator,
|
||||||
CreateComment: isAuthenticated,
|
CreateComment: isAuthenticated,
|
||||||
DeleteComment: isAuthor,
|
DeleteComment: isAuthor,
|
||||||
|
DeleteUser: isDeletingOwnAccount,
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
email: isMyOwn,
|
email: isMyOwn,
|
||||||
|
|||||||
@ -11,5 +11,27 @@ export default {
|
|||||||
params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
|
params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
|
||||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||||
},
|
},
|
||||||
|
DeleteUser: async (object, params, context, resolveInfo) => {
|
||||||
|
const { resource } = params
|
||||||
|
const session = context.driver.session()
|
||||||
|
|
||||||
|
if (resource && resource.length) {
|
||||||
|
await Promise.all(
|
||||||
|
resource.map(async node => {
|
||||||
|
await session.run(
|
||||||
|
`
|
||||||
|
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
|
||||||
|
SET resource.deleted = true
|
||||||
|
RETURN author`,
|
||||||
|
{
|
||||||
|
userId: context.user.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
import { GraphQLClient } from 'graphql-request'
|
||||||
import { login, host } from '../../jest/helpers'
|
import { login, host } from '../../jest/helpers'
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
let client
|
let client
|
||||||
@ -137,4 +138,141 @@ describe('users', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('DeleteUser', () => {
|
||||||
|
let deleteUserVariables
|
||||||
|
let asAuthor
|
||||||
|
const deleteUserMutation = gql`
|
||||||
|
mutation($id: ID!, $resource: [String]) {
|
||||||
|
DeleteUser(id: $id, resource: $resource) {
|
||||||
|
id
|
||||||
|
contributions {
|
||||||
|
id
|
||||||
|
deleted
|
||||||
|
}
|
||||||
|
comments {
|
||||||
|
id
|
||||||
|
deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
beforeEach(async () => {
|
||||||
|
asAuthor = await factory.create('User', {
|
||||||
|
email: 'test@example.org',
|
||||||
|
password: '1234',
|
||||||
|
id: 'u343',
|
||||||
|
})
|
||||||
|
await factory.create('User', {
|
||||||
|
email: 'friendsAccount@example.org',
|
||||||
|
password: '1234',
|
||||||
|
id: 'u565',
|
||||||
|
})
|
||||||
|
deleteUserVariables = { id: 'u343', resource: [] }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
client = new GraphQLClient(host)
|
||||||
|
await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow(
|
||||||
|
'Not Authorised',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
let headers
|
||||||
|
beforeEach(async () => {
|
||||||
|
headers = await login({
|
||||||
|
email: 'test@example.org',
|
||||||
|
password: '1234',
|
||||||
|
})
|
||||||
|
client = new GraphQLClient(host, { headers })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("attempting to delete another user's account", () => {
|
||||||
|
it('throws an authorization error', async () => {
|
||||||
|
deleteUserVariables = { id: 'u565' }
|
||||||
|
await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow(
|
||||||
|
'Not Authorised',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('attempting to delete my own account', () => {
|
||||||
|
let expectedResponse
|
||||||
|
beforeEach(async () => {
|
||||||
|
await asAuthor.authenticateAs({
|
||||||
|
email: 'test@example.org',
|
||||||
|
password: '1234',
|
||||||
|
})
|
||||||
|
await asAuthor.create('Post', {
|
||||||
|
id: 'p139',
|
||||||
|
content: 'Post by user u343',
|
||||||
|
})
|
||||||
|
await asAuthor.create('Comment', {
|
||||||
|
id: 'c155',
|
||||||
|
postId: 'p139',
|
||||||
|
content: 'Comment by user u343',
|
||||||
|
})
|
||||||
|
expectedResponse = {
|
||||||
|
DeleteUser: {
|
||||||
|
id: 'u343',
|
||||||
|
contributions: [{ id: 'p139', deleted: false }],
|
||||||
|
comments: [{ id: 'c155', deleted: false }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it("deletes my account, but doesn't delete posts or comments by default", async () => {
|
||||||
|
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
|
||||||
|
expectedResponse,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("deletes a user's", () => {
|
||||||
|
it('posts on request', async () => {
|
||||||
|
deleteUserVariables = { id: 'u343', resource: ['Post'] }
|
||||||
|
expectedResponse = {
|
||||||
|
DeleteUser: {
|
||||||
|
id: 'u343',
|
||||||
|
contributions: [{ id: 'p139', deleted: true }],
|
||||||
|
comments: [{ id: 'c155', deleted: false }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
|
||||||
|
expectedResponse,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('comments on request', async () => {
|
||||||
|
deleteUserVariables = { id: 'u343', resource: ['Comment'] }
|
||||||
|
expectedResponse = {
|
||||||
|
DeleteUser: {
|
||||||
|
id: 'u343',
|
||||||
|
contributions: [{ id: 'p139', deleted: false }],
|
||||||
|
comments: [{ id: 'c155', deleted: true }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
|
||||||
|
expectedResponse,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('posts and comments on request', async () => {
|
||||||
|
deleteUserVariables = { id: 'u343', resource: ['Post', 'Comment'] }
|
||||||
|
expectedResponse = {
|
||||||
|
DeleteUser: {
|
||||||
|
id: 'u343',
|
||||||
|
contributions: [{ id: 'p139', deleted: true }],
|
||||||
|
comments: [{ id: 'c155', deleted: true }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
|
||||||
|
expectedResponse,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,7 +4,8 @@ type Query {
|
|||||||
currentUser: User
|
currentUser: User
|
||||||
# Get the latest Network Statistics
|
# Get the latest Network Statistics
|
||||||
statistics: Statistics!
|
statistics: Statistics!
|
||||||
findPosts(filter: String!, limit: Int = 10): [Post]! @cypher(
|
findPosts(filter: String!, limit: Int = 10): [Post]!
|
||||||
|
@cypher(
|
||||||
statement: """
|
statement: """
|
||||||
CALL db.index.fulltext.queryNodes('full_text_search', $filter)
|
CALL db.index.fulltext.queryNodes('full_text_search', $filter)
|
||||||
YIELD node as post, score
|
YIELD node as post, score
|
||||||
@ -23,7 +24,7 @@ type Mutation {
|
|||||||
# Get a JWT Token for the given Email and password
|
# Get a JWT Token for the given Email and password
|
||||||
login(email: String!, password: String!): String!
|
login(email: String!, password: String!): String!
|
||||||
signup(email: String!, password: String!): Boolean!
|
signup(email: String!, password: String!): Boolean!
|
||||||
changePassword(oldPassword:String!, newPassword: String!): String!
|
changePassword(oldPassword: String!, newPassword: String!): String!
|
||||||
report(id: ID!, description: String): Report
|
report(id: ID!, description: String): Report
|
||||||
disable(id: ID!): ID
|
disable(id: ID!): ID
|
||||||
enable(id: ID!): ID
|
enable(id: ID!): ID
|
||||||
@ -37,6 +38,7 @@ type Mutation {
|
|||||||
follow(id: ID!, type: FollowTypeEnum): Boolean!
|
follow(id: ID!, type: FollowTypeEnum): Boolean!
|
||||||
# Unfollow the given Type and ID
|
# Unfollow the given Type and ID
|
||||||
unfollow(id: ID!, type: FollowTypeEnum): Boolean!
|
unfollow(id: ID!, type: FollowTypeEnum): Boolean!
|
||||||
|
DeleteUser(id: ID!, resource: [String]): User
|
||||||
}
|
}
|
||||||
|
|
||||||
type Statistics {
|
type Statistics {
|
||||||
@ -53,7 +55,7 @@ type Statistics {
|
|||||||
|
|
||||||
type Notification {
|
type Notification {
|
||||||
id: ID!
|
id: ID!
|
||||||
read: Boolean,
|
read: Boolean
|
||||||
user: User @relation(name: "NOTIFIED", direction: "OUT")
|
user: User @relation(name: "NOTIFIED", direction: "OUT")
|
||||||
post: Post @relation(name: "NOTIFIED", direction: "IN")
|
post: Post @relation(name: "NOTIFIED", direction: "IN")
|
||||||
createdAt: String
|
createdAt: String
|
||||||
@ -80,7 +82,8 @@ type Report {
|
|||||||
id: ID!
|
id: ID!
|
||||||
submitter: User @relation(name: "REPORTED", direction: "IN")
|
submitter: User @relation(name: "REPORTED", direction: "IN")
|
||||||
description: String
|
description: String
|
||||||
type: String! @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]")
|
type: String!
|
||||||
|
@cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]")
|
||||||
createdAt: String
|
createdAt: String
|
||||||
comment: Comment @relation(name: "REPORTED", direction: "OUT")
|
comment: Comment @relation(name: "REPORTED", direction: "OUT")
|
||||||
post: Post @relation(name: "REPORTED", direction: "OUT")
|
post: Post @relation(name: "REPORTED", direction: "OUT")
|
||||||
@ -131,4 +134,3 @@ type SocialMedia {
|
|||||||
url: String
|
url: String
|
||||||
ownedBy: [User]! @relation(name: "OWNED", direction: "IN")
|
ownedBy: [User]! @relation(name: "OWNED", direction: "IN")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ $easeOut: cubic-bezier(0.19, 1, 0.22, 1);
|
|||||||
&::before {
|
&::before {
|
||||||
@include border-radius($border-radius-x-large);
|
@include border-radius($border-radius-x-large);
|
||||||
box-shadow: inset 0 0 0 5px $color-danger;
|
box-shadow: inset 0 0 0 5px $color-danger;
|
||||||
content: "";
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -102,10 +102,10 @@ hr {
|
|||||||
height: 1px !important;
|
height: 1px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[class$=menu-trigger] {
|
[class$='menu-trigger'] {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
[class$=menu-popover] {
|
[class$='menu-popover'] {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
@ -145,10 +145,11 @@ hr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[class$="menu-popover"] {
|
[class$='menu-popover'] {
|
||||||
min-width: 130px;
|
min-width: 130px;
|
||||||
|
|
||||||
a, button {
|
a,
|
||||||
|
button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
183
webapp/components/DeleteData/DeleteData.spec.js
Normal file
183
webapp/components/DeleteData/DeleteData.spec.js
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import DeleteData from './DeleteData.vue'
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Vuex)
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
|
||||||
|
describe('DeleteData.vue', () => {
|
||||||
|
let mocks
|
||||||
|
let wrapper
|
||||||
|
let getters
|
||||||
|
let actions
|
||||||
|
let deleteAccountBtn
|
||||||
|
let enableDeletionInput
|
||||||
|
let enableContributionDeletionCheckbox
|
||||||
|
let enableCommentDeletionCheckbox
|
||||||
|
const deleteAccountName = 'Delete MyAccount'
|
||||||
|
const deleteContributionsMessage = 'Delete my 2 posts'
|
||||||
|
const deleteCommentsMessage = 'Delete my 3 comments'
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$t: jest.fn(),
|
||||||
|
$apollo: {
|
||||||
|
mutate: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
DeleteData: {
|
||||||
|
id: 'u343',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mockRejectedValue({ message: 'Not authorised!' }),
|
||||||
|
},
|
||||||
|
$toast: {
|
||||||
|
error: jest.fn(),
|
||||||
|
success: jest.fn(),
|
||||||
|
},
|
||||||
|
$router: {
|
||||||
|
history: {
|
||||||
|
push: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
getters = {
|
||||||
|
'auth/user': () => {
|
||||||
|
return { id: 'u343', name: deleteAccountName, contributionsCount: 2, commentsCount: 3 }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
actions = { 'auth/logout': jest.fn() }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
const Wrapper = () => {
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
})
|
||||||
|
return mount(DeleteData, { mocks, localVue, store })
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults to deleteContributions to false', () => {
|
||||||
|
expect(wrapper.vm.deleteContributions).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults to deleteComments to false', () => {
|
||||||
|
expect(wrapper.vm.deleteComments).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults to deleteEnabled to false', () => {
|
||||||
|
expect(wrapper.vm.deleteEnabled).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not call the delete user mutation if deleteEnabled is false', () => {
|
||||||
|
deleteAccountBtn = wrapper.find('.ds-button-danger')
|
||||||
|
deleteAccountBtn.trigger('click')
|
||||||
|
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('calls the delete user mutation', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
enableDeletionInput = wrapper.find('.enable-deletion-input input')
|
||||||
|
enableDeletionInput.setValue(deleteAccountName)
|
||||||
|
deleteAccountBtn = wrapper.find('.ds-button-danger')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('if deleteEnabled is true and only deletes user by default', () => {
|
||||||
|
deleteAccountBtn.trigger('click')
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: {
|
||||||
|
id: 'u343',
|
||||||
|
resource: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("deletes a user's posts if requested", () => {
|
||||||
|
mocks.$t.mockImplementation(() => deleteContributionsMessage)
|
||||||
|
enableContributionDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(0)
|
||||||
|
enableContributionDeletionCheckbox.trigger('click')
|
||||||
|
deleteAccountBtn.trigger('click')
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: {
|
||||||
|
id: 'u343',
|
||||||
|
resource: ['Post'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("deletes a user's comments if requested", () => {
|
||||||
|
mocks.$t.mockImplementation(() => deleteCommentsMessage)
|
||||||
|
enableCommentDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(1)
|
||||||
|
enableCommentDeletionCheckbox.trigger('click')
|
||||||
|
deleteAccountBtn.trigger('click')
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: {
|
||||||
|
id: 'u343',
|
||||||
|
resource: ['Comment'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("deletes a user's posts and comments if requested", () => {
|
||||||
|
mocks.$t.mockImplementation(() => deleteContributionsMessage)
|
||||||
|
enableContributionDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(0)
|
||||||
|
enableContributionDeletionCheckbox.trigger('click')
|
||||||
|
mocks.$t.mockImplementation(() => deleteCommentsMessage)
|
||||||
|
enableCommentDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(1)
|
||||||
|
enableCommentDeletionCheckbox.trigger('click')
|
||||||
|
deleteAccountBtn.trigger('click')
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: {
|
||||||
|
id: 'u343',
|
||||||
|
resource: ['Post', 'Comment'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows a success toaster after successful mutation', async () => {
|
||||||
|
await deleteAccountBtn.trigger('click')
|
||||||
|
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirect the user to the homepage', async () => {
|
||||||
|
await deleteAccountBtn.trigger('click')
|
||||||
|
expect(mocks.$router.history.push).toHaveBeenCalledWith('/')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('shows an error toaster when the mutation rejects', async () => {
|
||||||
|
enableDeletionInput = wrapper.find('.enable-deletion-input input')
|
||||||
|
enableDeletionInput.setValue(deleteAccountName)
|
||||||
|
deleteAccountBtn = wrapper.find('.ds-button-danger')
|
||||||
|
await deleteAccountBtn.trigger('click')
|
||||||
|
// second submission causes mutation to reject
|
||||||
|
await deleteAccountBtn.trigger('click')
|
||||||
|
await mocks.$apollo.mutate
|
||||||
|
expect(mocks.$toast.error).toHaveBeenCalledWith('Not authorised!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
224
webapp/components/DeleteData/DeleteData.vue
Normal file
224
webapp/components/DeleteData/DeleteData.vue
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ds-card hover>
|
||||||
|
<ds-space />
|
||||||
|
<ds-container>
|
||||||
|
<ds-flex>
|
||||||
|
<ds-flex-item :width="{ base: '22%', sm: '12%', md: '12%', lg: '8%' }">
|
||||||
|
<ds-icon name="warning" size="xxx-large" class="delete-warning-icon" />
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-flex-item :width="{ base: '78%', sm: '88%', md: '88%', lg: '92%' }">
|
||||||
|
<ds-heading>{{ $t('settings.deleteUserAccount.name') }}</ds-heading>
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-space />
|
||||||
|
<ds-heading tag="h4">
|
||||||
|
{{ $t('settings.deleteUserAccount.accountDescription') }}
|
||||||
|
</ds-heading>
|
||||||
|
</ds-flex>
|
||||||
|
</ds-container>
|
||||||
|
<ds-space />
|
||||||
|
<ds-container>
|
||||||
|
<transition name="slide-up">
|
||||||
|
<div v-if="deleteEnabled">
|
||||||
|
<label v-if="currentUser.contributionsCount" class="checkbox-container">
|
||||||
|
<input type="checkbox" v-model="deleteContributions" />
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
{{
|
||||||
|
$t('settings.deleteUserAccount.contributionsCount', {
|
||||||
|
count: currentUser.contributionsCount,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
<ds-space margin-bottom="small" />
|
||||||
|
<label v-if="currentUser.commentsCount" class="checkbox-container">
|
||||||
|
<input type="checkbox" v-model="deleteComments" />
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
{{
|
||||||
|
$t('settings.deleteUserAccount.commentsCount', {
|
||||||
|
count: currentUser.commentsCount,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
<ds-space margin-bottom="small" />
|
||||||
|
<ds-section id="delete-user-account-warning">
|
||||||
|
<div v-html="$t('settings.deleteUserAccount.accountWarning')"></div>
|
||||||
|
</ds-section>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</ds-container>
|
||||||
|
<template slot="footer" class="delete-data-footer">
|
||||||
|
<ds-container>
|
||||||
|
<div
|
||||||
|
class="delete-input-label"
|
||||||
|
v-html="$t('settings.deleteUserAccount.pleaseConfirm', { confirm: currentUser.name })"
|
||||||
|
></div>
|
||||||
|
<ds-space margin-bottom="xx-small" />
|
||||||
|
<ds-flex :gutter="{ base: 'xx-small', md: 'small', lg: 'large' }">
|
||||||
|
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '100%', lg: 1.75 }">
|
||||||
|
<ds-input
|
||||||
|
v-model="enableDeletionValue"
|
||||||
|
@input="enableDeletion"
|
||||||
|
class="enable-deletion-input"
|
||||||
|
/>
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '100%', lg: 1 }">
|
||||||
|
<ds-button icon="trash" danger :disabled="!deleteEnabled" @click="handleSubmit">
|
||||||
|
{{ $t('settings.deleteUserAccount.name') }}
|
||||||
|
</ds-button>
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
</ds-container>
|
||||||
|
</template>
|
||||||
|
</ds-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapActions } from 'vuex'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DeleteData',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
deleteContributions: false,
|
||||||
|
deleteComments: false,
|
||||||
|
deleteEnabled: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
currentUser: 'auth/user',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions({
|
||||||
|
logout: 'auth/logout',
|
||||||
|
}),
|
||||||
|
enableDeletion() {
|
||||||
|
if (this.enableDeletionValue === this.currentUser.name) {
|
||||||
|
this.deleteEnabled = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSubmit() {
|
||||||
|
let resourceArgs = []
|
||||||
|
if (this.deleteContributions) {
|
||||||
|
resourceArgs.push('Post')
|
||||||
|
}
|
||||||
|
if (this.deleteComments) {
|
||||||
|
resourceArgs.push('Comment')
|
||||||
|
}
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation($id: ID!, $resource: [String]) {
|
||||||
|
DeleteUser(id: $id, resource: $resource) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { id: this.currentUser.id, resource: resourceArgs },
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success(this.$t('settings.deleteUserAccount.success'))
|
||||||
|
this.logout()
|
||||||
|
this.$router.history.push('/')
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.$toast.error(error.message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.delete-warning-icon {
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-container {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 35px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: $font-size-large;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-container input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border: 2px solid $background-color-inverse-softer;
|
||||||
|
background-color: $background-color-base;
|
||||||
|
border-radius: $border-radius-x-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-container:hover input ~ .checkmark {
|
||||||
|
background-color: $background-color-softest;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-container input:checked ~ .checkmark {
|
||||||
|
background-color: $background-color-danger-active;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-container input:checked ~ .checkmark:after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-container .checkmark:after {
|
||||||
|
left: 6px;
|
||||||
|
top: 3px;
|
||||||
|
width: 5px;
|
||||||
|
height: 10px;
|
||||||
|
border: solid $background-color-base;
|
||||||
|
border-width: 0 $border-size-large $border-size-large 0;
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enable-deletion-input input:focus {
|
||||||
|
border-color: $border-color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-input-label {
|
||||||
|
font-size: $font-size-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
b.is-danger {
|
||||||
|
color: $text-color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-data-footer {
|
||||||
|
border-top: $border-size-base solid $border-color-softest;
|
||||||
|
background-color: $background-color-danger-inverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
#delete-user-account-warning {
|
||||||
|
background-color: $background-color-danger-inverse;
|
||||||
|
border-left: $border-size-x-large solid $background-color-danger-active;
|
||||||
|
color: $text-color-danger;
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 0px;
|
||||||
|
border-radius: $border-radius-x-large;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -82,8 +82,14 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"name": "Daten herunterladen"
|
"name": "Daten herunterladen"
|
||||||
},
|
},
|
||||||
"delete": {
|
"deleteUserAccount": {
|
||||||
"name": "Konto löschen"
|
"name": "Daten löschen",
|
||||||
|
"contributionsCount": "Meine {count} Beiträge löschen",
|
||||||
|
"commentsCount": "Meine {count} Kommentare löschen",
|
||||||
|
"accountDescription": "Sei dir bewusst, dass deine Beiträge und Kommentare für unsere Community wichtig sind. Wenn du sie trotzdem löschen möchtest, musst du sie unten markieren.",
|
||||||
|
"accountWarning": "Dein Konto, deine Beiträge oder Kommentare kannst du nach dem Löschen <b>WEDER VERWALTEN NOCH WIEDERHERSTELLEN!</b>",
|
||||||
|
"success": "Konto erfolgreich gelöscht",
|
||||||
|
"pleaseConfirm": "<b class='is-danger'>Zerstörerische Aktion!</b> Gib <b>{confirm}</b> ein, um zu bestätigen."
|
||||||
},
|
},
|
||||||
"organizations": {
|
"organizations": {
|
||||||
"name": "Meine Organisationen"
|
"name": "Meine Organisationen"
|
||||||
|
|||||||
@ -82,8 +82,14 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"name": "Download Data"
|
"name": "Download Data"
|
||||||
},
|
},
|
||||||
"delete": {
|
"deleteUserAccount": {
|
||||||
"name": "Delete Account"
|
"name": "Delete Data",
|
||||||
|
"contributionsCount": "Delete my {count} posts",
|
||||||
|
"commentsCount": "Delete my {count} comments",
|
||||||
|
"accountDescription": "Be aware that your Post and Comments are important to our community. If you still choose to delete them, you have to mark them below.",
|
||||||
|
"accountWarning": "You <b>CAN'T MANAGE</b> and <b>CAN'T RECOVER</b> your Account, Posts, or Comments after deleting your account!",
|
||||||
|
"success": "Account successfully deleted",
|
||||||
|
"pleaseConfirm": "<b class='is-danger'>Destructive action!</b> Type <b>{confirm}</b> to confirm"
|
||||||
},
|
},
|
||||||
"organizations": {
|
"organizations": {
|
||||||
"name": "My Organizations"
|
"name": "My Organizations"
|
||||||
|
|||||||
@ -13,13 +13,11 @@
|
|||||||
resource-type="contribution"
|
resource-type="contribution"
|
||||||
:resource="post"
|
:resource="post"
|
||||||
:callbacks="{ confirm: () => deletePostCallback('page'), cancel: null }"
|
:callbacks="{ confirm: () => deletePostCallback('page'), cancel: null }"
|
||||||
:is-owner="isAuthor(post.author.id)"
|
:is-owner="isAuthor(post.author ? post.author.id : null)"
|
||||||
/>
|
/>
|
||||||
</no-ssr>
|
</no-ssr>
|
||||||
<ds-space margin-bottom="small" />
|
<ds-space margin-bottom="small" />
|
||||||
<ds-heading tag="h3" no-margin>
|
<ds-heading tag="h3" no-margin>{{ post.title }}</ds-heading>
|
||||||
{{ post.title }}
|
|
||||||
</ds-heading>
|
|
||||||
<ds-space margin-bottom="small" />
|
<ds-space margin-bottom="small" />
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
|||||||
@ -228,7 +228,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import uniqBy from 'lodash/uniqBy'
|
import uniqBy from 'lodash/uniqBy'
|
||||||
|
|
||||||
import User from '~/components/User'
|
import User from '~/components/User'
|
||||||
import HcPostCard from '~/components/PostCard'
|
import HcPostCard from '~/components/PostCard'
|
||||||
import HcFollowButton from '~/components/FollowButton.vue'
|
import HcFollowButton from '~/components/FollowButton.vue'
|
||||||
@ -240,10 +239,8 @@ import ContentMenu from '~/components/ContentMenu'
|
|||||||
import HcUpload from '~/components/Upload'
|
import HcUpload from '~/components/Upload'
|
||||||
import HcAvatar from '~/components/Avatar/Avatar.vue'
|
import HcAvatar from '~/components/Avatar/Avatar.vue'
|
||||||
import PostMutationHelpers from '~/mixins/PostMutationHelpers'
|
import PostMutationHelpers from '~/mixins/PostMutationHelpers'
|
||||||
|
|
||||||
import PostQuery from '~/graphql/UserProfile/Post.js'
|
import PostQuery from '~/graphql/UserProfile/Post.js'
|
||||||
import UserQuery from '~/graphql/UserProfile/User.js'
|
import UserQuery from '~/graphql/UserProfile/User.js'
|
||||||
|
|
||||||
const tabToFilterMapping = ({ tab, id }) => {
|
const tabToFilterMapping = ({ tab, id }) => {
|
||||||
return {
|
return {
|
||||||
post: { author: { id } },
|
post: { author: { id } },
|
||||||
@ -251,7 +248,6 @@ const tabToFilterMapping = ({ tab, id }) => {
|
|||||||
shout: { shoutedBy_some: { id } },
|
shout: { shoutedBy_some: { id } },
|
||||||
}[tab]
|
}[tab]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
User,
|
User,
|
||||||
@ -397,7 +393,6 @@ export default {
|
|||||||
.pointer {
|
.pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Tab {
|
.Tab {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
@ -405,7 +400,6 @@ export default {
|
|||||||
.Tab:hover {
|
.Tab:hover {
|
||||||
border-bottom: 2px solid #c9c6ce;
|
border-bottom: 2px solid #c9c6ce;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Tabs {
|
.Tabs {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
@ -441,14 +435,12 @@ export default {
|
|||||||
transition: left 0.25s;
|
transition: left 0.25s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar.ds-avatar {
|
.profile-avatar.ds-avatar {
|
||||||
display: block;
|
display: block;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
margin-top: -60px;
|
margin-top: -60px;
|
||||||
border: #fff 5px solid;
|
border: #fff 5px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-name-profile-id-slug {
|
.page-name-profile-id-slug {
|
||||||
.ds-flex-item:first-child .content-menu {
|
.ds-flex-item:first-child .content-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -456,17 +448,14 @@ export default {
|
|||||||
right: $space-x-small;
|
right: $space-x-small;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-top-navigation {
|
.profile-top-navigation {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 53px;
|
top: 53px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ds-tab-nav {
|
.ds-tab-nav {
|
||||||
.ds-card-content {
|
.ds-card-content {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|
||||||
.ds-tab-nav-item {
|
.ds-tab-nav-item {
|
||||||
&.ds-tab-nav-item-active {
|
&.ds-tab-nav-item-active {
|
||||||
border-bottom: 3px solid #17b53f;
|
border-bottom: 3px solid #17b53f;
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ds-heading tag="h1">
|
<ds-heading tag="h1">{{ $t('settings.name') }}</ds-heading>
|
||||||
{{ $t('settings.name') }}
|
|
||||||
</ds-heading>
|
|
||||||
<ds-flex gutter="small">
|
<ds-flex gutter="small">
|
||||||
<ds-flex-item :width="{ base: '100%', md: '200px' }">
|
<ds-flex-item :width="{ base: '100%', md: '200px' }">
|
||||||
<ds-menu :routes="routes" :is-exact="() => true" />
|
<ds-menu :routes="routes" :is-exact="() => true" />
|
||||||
@ -33,6 +31,10 @@ export default {
|
|||||||
name: this.$t('settings.social-media.name'),
|
name: this.$t('settings.social-media.name'),
|
||||||
path: `/settings/my-social-media`,
|
path: `/settings/my-social-media`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: this.$t('settings.deleteUserAccount.name'),
|
||||||
|
path: `/settings/delete-account`,
|
||||||
|
},
|
||||||
// TODO implement
|
// TODO implement
|
||||||
/* {
|
/* {
|
||||||
name: this.$t('settings.invites.name'),
|
name: this.$t('settings.invites.name'),
|
||||||
@ -44,10 +46,6 @@ export default {
|
|||||||
path: `/settings/data-download`
|
path: `/settings/data-download`
|
||||||
}, */
|
}, */
|
||||||
// TODO implement
|
// TODO implement
|
||||||
/* {
|
|
||||||
name: this.$t('settings.delete.name'),
|
|
||||||
path: `/settings/delete-account`
|
|
||||||
}, */
|
|
||||||
// TODO implement
|
// TODO implement
|
||||||
/* {
|
/* {
|
||||||
name: this.$t('settings.organizations.name'),
|
name: this.$t('settings.organizations.name'),
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-card :header="$t('settings.delete.name')">
|
<delete-data />
|
||||||
<hc-empty icon="tasks" message="Coming Soon…" />
|
|
||||||
</ds-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HcEmpty from '~/components/Empty.vue'
|
import DeleteData from '~/components/DeleteData/DeleteData.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
HcEmpty,
|
DeleteData,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -79,6 +79,8 @@ export const actions = {
|
|||||||
role
|
role
|
||||||
about
|
about
|
||||||
locationName
|
locationName
|
||||||
|
contributionsCount
|
||||||
|
commentsCount
|
||||||
socialMedia {
|
socialMedia {
|
||||||
id
|
id
|
||||||
url
|
url
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user