Skip to content

Commit

Permalink
Merge pull request #8741 from owncloud/gdpr-export
Browse files Browse the repository at this point in the history
Implement GDPR export for users
  • Loading branch information
JammingBen authored Apr 4, 2023
2 parents 5225a63 + c4925ba commit 13f2abe
Show file tree
Hide file tree
Showing 15 changed files with 836 additions and 202 deletions.
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-gdpr-export
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: GDPR export

Users can now request a GDPR export on their account page. Note that this is only supported when running oCIS as backend.

https://github.com/owncloud/web/issues/8738
https://github.com/owncloud/web/pull/8741
1 change: 1 addition & 0 deletions changelog/unreleased/enhancement-update-libregraph
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ libre-graph-api has been updated to v1.0
https://github.com/owncloud/web/pull/8132
https://github.com/owncloud/web/pull/8171
https://github.com/owncloud/web/pull/8250
https://github.com/owncloud/web/pull/8741
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6.3.0-SNAPSHOT
6.5.0-SNAPSHOT
701 changes: 518 additions & 183 deletions packages/web-client/src/generated/api.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/web-client/src/generated/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable */
/**
* Libre Graph API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
* Libre Graph is a free API for cloud collaboration inspired by the MS Graph API.
*
* The version of the OpenAPI document: v1.0.1
*
Expand Down
2 changes: 1 addition & 1 deletion packages/web-client/src/generated/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable */
/**
* Libre Graph API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
* Libre Graph is a free API for cloud collaboration inspired by the MS Graph API.
*
* The version of the OpenAPI document: v1.0.1
*
Expand Down
2 changes: 1 addition & 1 deletion packages/web-client/src/generated/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable */
/**
* Libre Graph API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
* Libre Graph is a free API for cloud collaboration inspired by the MS Graph API.
*
* The version of the OpenAPI document: v1.0.1
*
Expand Down
2 changes: 1 addition & 1 deletion packages/web-client/src/generated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable */
/**
* Libre Graph API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
* Libre Graph is a free API for cloud collaboration inspired by the MS Graph API.
*
* The version of the OpenAPI document: v1.0.1
*
Expand Down
18 changes: 9 additions & 9 deletions packages/web-client/src/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import {
CollectionOfApplications,
ApplicationsApiFactory,
UserAppRoleAssignmentApiFactory,
AppRoleAssignment
AppRoleAssignment,
ExportPersonalDataRequest
} from './generated'

export interface Graph {
Expand Down Expand Up @@ -55,6 +56,10 @@ export interface Graph {
userId: string,
appRoleAssignment: AppRoleAssignment
) => AxiosPromise<AppRoleAssignment>
exportPersonalData: (
userId: string,
exportPersonalDataRequest?: ExportPersonalDataRequest
) => AxiosPromise<void>
}
groups: {
listGroups: (orderBy?: string) => AxiosPromise<CollectionOfGroup>
Expand Down Expand Up @@ -133,17 +138,16 @@ export const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => {
deleteUser: (userId: string) => userApiFactory.deleteUser(userId),
listUsers: (orderBy?: any, filter?: string) =>
usersApiFactory.listUsers(
0,
0,
'',
filter,
false,
new Set<any>([orderBy]),
new Set<any>([]),
new Set<any>(['memberOf', 'appRoleAssignments'])
),
createUserAppRoleAssignment: (userId: string, appRoleAssignment: AppRoleAssignment) =>
userAppRoleAssignmentApiFactory.userCreateAppRoleAssignments(userId, appRoleAssignment)
userAppRoleAssignmentApiFactory.userCreateAppRoleAssignments(userId, appRoleAssignment),
exportPersonalData: (userId: string, exportPersonalDataRequest?: ExportPersonalDataRequest) =>
userApiFactory.exportPersonalData(userId, exportPersonalDataRequest)
},
groups: {
createGroup: (group: Group) => groupsApiFactory.createGroup(group),
Expand All @@ -153,11 +157,7 @@ export const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => {
deleteGroup: (groupId: string) => groupApiFactory.deleteGroup(groupId),
listGroups: (orderBy?: any) =>
groupsApiFactory.listGroups(
0,
0,
'',
'',
false,
new Set<any>([orderBy]),
new Set<any>([]),
new Set<any>(['members'])
Expand Down
5 changes: 4 additions & 1 deletion packages/web-pkg/src/composables/capability/useCapability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ export const useCapabilityCoreSupportUrlSigning = createCapabilityComposable(
'core.support-url-signing',
false
)

export const useCapabilityGraphPersonalDataExport = createCapabilityComposable(
'graph.personal-data-export',
false
)
export const useCapabilityFilesSharingQuickLinkDefaultRole = createCapabilityComposable(
'files_sharing.quick_link.default_role',
'viewer'
Expand Down
139 changes: 139 additions & 0 deletions packages/web-runtime/src/components/Account/GdprExport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<template>
<span v-if="loading">
<oc-spinner />
</span>
<span v-else-if="exportInProgress" class="oc-flex oc-flex-middle" data-testid="export-in-process">
<oc-icon name="time" fill-type="line" size="small" class="oc-mr-s" />
<span v-text="$gettext('Export is being processed. This can take up to 24 hours.')" />
</span>
<div v-else>
<oc-button
appearance="raw"
variation="primary"
data-testid="request-export-btn"
@click="requestExport"
>
<span v-text="$gettext('Request new export')" />
</oc-button>
<div v-if="exportFile" class="oc-flex oc-flex-middle">
<oc-button
appearance="raw"
variation="primary"
data-testid="download-export-btn"
@click="downloadExport"
>
<oc-icon name="download" fill-type="line" size="small" />
<span v-text="$gettext('Download export')" />
</oc-button>
<span class="oc-ml-s" v-text="`(${exportDate})`" />
</div>
</div>
</template>

<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, ref, unref } from 'vue'
import { useTask } from 'vue-concurrency'
import { useGettext } from 'vue3-gettext'
import { Resource } from 'web-client'
import { useClientService, useStore } from 'web-pkg/src/composables'
import { useDownloadFile } from 'web-pkg/src/composables/download'
import { formatDateFromJSDate } from 'web-pkg'
import { isPersonalSpaceResource } from 'web-client/src/helpers'
const GDPR_EXPORT_FILE_NAME = '.personal_data_export.json'
const POLLING_INTERVAL = 30000
export default defineComponent({
name: 'GdprExport',
setup() {
const store = useStore()
const { $gettext, current: currentLanguage } = useGettext()
const { webdav, graphAuthenticated } = useClientService()
const { downloadFile } = useDownloadFile()
const loading = ref(true)
const checkInterval = ref()
const exportFile = ref<Resource>()
const exportInProgress = ref(false)
const personalSpace = computed(() => {
return store.getters['runtime/spaces/spaces'].find((s) => isPersonalSpaceResource(s))
})
const loadExportTask = useTask(function* () {
try {
const resource = yield webdav.getFileInfo(unref(personalSpace), {
path: `/${GDPR_EXPORT_FILE_NAME}`
})
if (resource.processing) {
exportInProgress.value = true
if (!unref(checkInterval)) {
checkInterval.value = setInterval(() => {
loadExportTask.perform()
}, POLLING_INTERVAL)
}
return
}
exportFile.value = resource
exportInProgress.value = false
if (unref(checkInterval)) {
clearInterval(unref(checkInterval))
}
} catch (e) {
if (e.statusCode !== 404) {
// resource seems to exist, but something else went wrong
console.error(e)
}
} finally {
loading.value = false
}
}).restartable()
const requestExport = async () => {
try {
await graphAuthenticated.users.exportPersonalData(store.getters.user.uuid, {
storageLocation: `/${GDPR_EXPORT_FILE_NAME}`
})
await loadExportTask.perform()
return store.dispatch('showMessage', {
title: $gettext('GDPR export has been requested')
})
} catch (e) {
return store.dispatch('showMessage', {
title: $gettext('GDPR export could not be requested. Please contact an administrator.'),
status: 'danger'
})
}
}
const downloadExport = () => {
return downloadFile(unref(exportFile))
}
const exportDate = computed(() => {
return formatDateFromJSDate(new Date(unref(exportFile).mdate), currentLanguage)
})
onMounted(() => {
loadExportTask.perform()
})
onUnmounted(() => {
if (unref(checkInterval)) {
clearInterval(unref(checkInterval))
}
})
return {
loading,
loadExportTask,
exportFile,
exportInProgress,
requestExport,
downloadExport,
exportDate
}
}
})
</script>
24 changes: 22 additions & 2 deletions packages/web-runtime/src/pages/account.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@
</oc-button>
</dd>
</div>
<div v-if="showGdprExport" class="account-page-gdpr-export oc-mb oc-width-1-2@s">
<dt class="oc-text-normal oc-text-muted" v-text="$gettext('GDPR export')" />
<dd data-testid="gdpr-export">
<gdpr-export />
</dd>
</div>
</dl>
</main>
</template>
Expand All @@ -104,6 +110,7 @@ import EditPasswordModal from '../components/EditPasswordModal.vue'
import { computed, defineComponent, onMounted, unref } from 'vue'
import {
useAccessToken,
useCapabilityGraphPersonalDataExport,
useCapabilitySpacesEnabled,
useClientService,
useStore
Expand All @@ -113,26 +120,38 @@ import axios from 'axios'
import { v4 as uuidV4 } from 'uuid'
import { useGettext } from 'vue3-gettext'
import { setCurrentLanguage } from 'web-runtime/src/helpers/language'
import { configurationManager } from 'web-pkg/src/configuration'
import GdprExport from 'web-runtime/src/components/Account/GdprExport.vue'
import { useConfigurationManager } from 'web-pkg/src/composables/configuration'
import { isPersonalSpaceResource } from 'web-client/src/helpers'
export default defineComponent({
name: 'Personal',
components: {
EditPasswordModal
EditPasswordModal,
GdprExport
},
setup() {
const store = useStore()
const accessToken = useAccessToken({ store })
const language = useGettext()
const clientService = useClientService()
const configurationManager = useConfigurationManager()
// FIXME: Use graph capability when we have it
const isLanguageSupported = useCapabilitySpacesEnabled()
const isChangePasswordEnabled = useCapabilitySpacesEnabled()
const isPersonalDataExportEnabled = useCapabilityGraphPersonalDataExport()
const user = computed(() => {
return store.getters.user
})
const showGdprExport = computed(() => {
return (
unref(isPersonalDataExportEnabled) &&
store.getters['runtime/spaces/spaces'].some((s) => isPersonalSpaceResource(s))
)
})
const loadAccountBundleTask = useTask(function* () {
try {
const {
Expand Down Expand Up @@ -235,6 +254,7 @@ export default defineComponent({
updateSelectedLanguage,
accountEditLink,
isChangePasswordEnabled,
showGdprExport,
isLanguageSupported,
groupNames,
user,
Expand Down
Loading

0 comments on commit 13f2abe

Please sign in to comment.