From 113684b9864a7cbf0066d040b0eda2e1b841d01c Mon Sep 17 00:00:00 2001 From: "Enjeck C." Date: Mon, 20 Apr 2026 08:33:51 +0100 Subject: [PATCH] feat: add export access validation and error handling for CSV downloads Signed-off-by: Enjeck C. --- src/modules/main/sections/MainWrapper.vue | 16 +++++++++++++++- src/pages/Context.vue | 17 +++++++++++++++-- src/store/store.js | 20 ++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/modules/main/sections/MainWrapper.vue b/src/modules/main/sections/MainWrapper.vue index 98d08ba4ba..b2a7387740 100644 --- a/src/modules/main/sections/MainWrapper.vue +++ b/src/modules/main/sections/MainWrapper.vue @@ -42,6 +42,7 @@ import exportTableMixin from '../../../shared/components/ncTable/mixins/exportTa import { useTablesStore } from '../../../store/store.js' import { useDataStore } from '../../../store/data.js' import { computed } from 'vue' +import { showError } from '@nextcloud/dialogs' export default { name: 'MainWrapper', @@ -100,10 +101,23 @@ export default { methods: { ...mapActions(useDataStore, ['removeRows', 'clearState', 'loadColumnsFromBE', 'loadRowsFromBE']), + ...mapActions(useTablesStore, ['validateExportAccess']), createColumn() { emit('tables:column:create', { isView: this.isView, element: this.element }) }, - downloadCSV() { + async downloadCSV() { + const access = await this.validateExportAccess({ + id: this.element.id, + isView: this.isView, + }) + + if (!access?.ok) { + if (access?.reason === 'NO_ACCESS') { + showError(t('tables', 'Your access was revoked. Reload the page to update your permissions.')) + } + return + } + this.downloadCsv(this.rows, this.columns, this.element.title) }, toggleShare() { diff --git a/src/pages/Context.vue b/src/pages/Context.vue index 52a888dafb..ac79bb75c4 100644 --- a/src/pages/Context.vue +++ b/src/pages/Context.vue @@ -55,6 +55,7 @@ import { useTablesStore } from '../store/store.js' import { useDataStore } from '../store/data.js' import ErrorMessage from '../modules/main/partials/ErrorMessage.vue' import displayError, { getNotFoundError, getGenericLoadError } from '../shared/utils/displayError.js' +import { showError } from '@nextcloud/dialogs' export default { components: { @@ -144,7 +145,7 @@ export default { }, methods: { - ...mapActions(useTablesStore, ['loadContext']), + ...mapActions(useTablesStore, ['loadContext', 'validateExportAccess']), ...mapActions(useDataStore, ['loadColumnsFromBE', 'loadRowsFromBE']), async reload() { if (!this.activeContextId) { @@ -236,7 +237,19 @@ export default { createColumn(isView, element) { emit('tables:column:create', { isView, element }) }, - downloadCSV(element, isView) { + async downloadCSV(element, isView) { + const access = await this.validateExportAccess({ + id: element.id, + isView, + }) + + if (!access?.ok) { + if (access?.reason === 'NO_ACCESS') { + showError(t('tables', 'Your access was revoked. Reload the page to update your permissions.')) + } + return + } + const rowId = this.getKey(isView, element.key) const colId = this.getKey(isView, element.key) this.downloadCsv(this.rows[rowId], this.columns[colId], element.title) diff --git a/src/store/store.js b/src/store/store.js index 5a2e1b17eb..3f8dafc2dc 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -556,6 +556,26 @@ export const useTablesStore = defineStore('store', { return res?.data }, + async validateExportAccess({ id, isView }) { + id = parseInt(id) + + try { + if (isView) { + await axios.get(generateUrl('/apps/tables/view/' + id)) + } else { + await axios.get(generateOcsUrl('/apps/tables/api/2/tables/' + id)) + } + } catch (e) { + if ([401, 403, 404].includes(e?.response?.status)) { + return { ok: false, reason: 'NO_ACCESS' } + } + displayError(e, t('tables', 'Could not verify export permissions.')) + return { ok: false, reason: 'ERROR' } + } + + return { ok: true } + }, + async transferContext({ id, data }) { try { await axios.put(generateOcsUrl('/apps/tables/api/2/contexts/' + id + '/transfer'), data)