mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add error alert on delete / soft delete from browse
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import { BaseException } from './base';
|
||||
import { Permission } from '../types';
|
||||
|
||||
type Extensions = {
|
||||
field?: string;
|
||||
collection?: string;
|
||||
item?: string | number | (string | number)[];
|
||||
action?: Permission['action'];
|
||||
}
|
||||
|
||||
export class ForbiddenException extends BaseException {
|
||||
constructor(message = `You don't have permission to access this.`) {
|
||||
super(message, 403, 'NO_PERMISSION');
|
||||
constructor(message = `You don't have permission to access this.`, extensions?: Extensions) {
|
||||
super(message, 403, 'NO_PERMISSION', extensions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,9 +239,12 @@ export default class AuthorizationService {
|
||||
const result = await itemsService.readByKey(pk as any, query, action);
|
||||
|
||||
if (!result) throw '';
|
||||
if (Array.isArray(pk) && result.length !== pk.length) throw '';
|
||||
} catch {
|
||||
throw new ForbiddenException(
|
||||
`You're not allowed to ${action} item "${pk}" in collection "${collection}".`
|
||||
`You're not allowed to ${action} item "${pk}" in collection "${collection}".`, {
|
||||
collection, item: pk, action
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,10 +56,12 @@ export const onError = async (error: RequestError) => {
|
||||
if (status === 401 && code === 'INVALID_CREDENTIALS' && error.request.responseURL.includes('refresh') === false) {
|
||||
try {
|
||||
await refresh();
|
||||
return api.request(error.config);
|
||||
} catch {
|
||||
logout({ reason: LogoutReason.ERROR_SESSION_EXPIRED });
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return api.request(error.config);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
|
||||
@@ -122,6 +122,7 @@ export default defineComponent({
|
||||
const sizeClass = useSizeClass(props);
|
||||
|
||||
const component = computed<'a' | 'router-link' | 'button'>(() => {
|
||||
if (props.disabled) return 'button';
|
||||
if (notEmpty(props.href)) return 'a';
|
||||
if (notEmpty(props.to)) return 'router-link';
|
||||
return 'button';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="v-error">
|
||||
<output>{{ code }}</output>
|
||||
<output>Code: {{ code }}</output>
|
||||
<v-icon
|
||||
v-tooltip="$t('copy_details')"
|
||||
v-if="showCopy"
|
||||
@@ -34,7 +34,8 @@ export default defineComponent({
|
||||
return { code, copyError, showCopy, copied };
|
||||
|
||||
async function copyError() {
|
||||
await navigator.clipboard.writeText(JSON.stringify(props.error, null, 2));
|
||||
const error = props.error?.response?.data || props.error;
|
||||
await navigator.clipboard.writeText(JSON.stringify(error, null, 2));
|
||||
copied.value = true;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
"move_to_trash": "Move to Trash",
|
||||
"move_to_trash_confirm": "Are you sure you want to move this item to the trash?",
|
||||
"move_to_trash_confirm_count": "No Items Selected | Are you sure you want to move this item to the trash? | Are you sure you want to move these {count} items to the trash?",
|
||||
|
||||
"nested_files_folders_will_be_moved": "Nested files and folders will be moved one level up.",
|
||||
|
||||
|
||||
@@ -48,8 +48,15 @@
|
||||
|
||||
<v-dialog v-model="confirmDelete" v-if="selection.length > 0">
|
||||
<template #activator="{ on }">
|
||||
<v-button rounded icon class="action-delete" @click="on" v-tooltip.bottom="$t('delete')">
|
||||
<v-icon name="delete" outline />
|
||||
<v-button
|
||||
:disabled="batchDeleteAllowed !== true"
|
||||
rounded
|
||||
icon
|
||||
class="action-delete"
|
||||
@click="on"
|
||||
v-tooltip.bottom="batchDeleteAllowed ? $t('delete') : $t('not_allowed')"
|
||||
>
|
||||
<v-icon name="delete_forever" outline />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
@@ -67,13 +74,45 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-model="confirmSoftDelete"
|
||||
v-if="selection.length > 0 && currentCollection.meta && currentCollection.meta.soft_delete_field"
|
||||
>
|
||||
<template #activator="{ on }">
|
||||
<v-button
|
||||
:disabled="batchSoftDeleteAllowed !== true"
|
||||
rounded
|
||||
icon
|
||||
class="action-soft-delete"
|
||||
@click="on"
|
||||
v-tooltip.bottom="batchEditAllowed ? $t('move_to_trash') : $t('not_allowed')"
|
||||
>
|
||||
<v-icon name="delete" outline />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title>{{ $tc('move_to_trash_confirm_count', selection.length) }}</v-card-title>
|
||||
|
||||
<v-card-actions>
|
||||
<v-button @click="confirmSoftDelete = false" secondary>
|
||||
{{ $t('cancel') }}
|
||||
</v-button>
|
||||
<v-button @click="batchDelete" class="action-soft-delete" :loading="softDeleting">
|
||||
{{ $t('move_to_trash') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-button
|
||||
rounded
|
||||
icon
|
||||
class="action-batch"
|
||||
v-if="selection.length > 1"
|
||||
:to="batchLink"
|
||||
v-tooltip.bottom="$t('edit')"
|
||||
v-tooltip.bottom="batchEditAllowed ? $t('edit') : $t('not_allowed')"
|
||||
:disabled="batchEditAllowed === false"
|
||||
>
|
||||
<v-icon name="edit" outline />
|
||||
</v-button>
|
||||
@@ -152,6 +191,18 @@
|
||||
<layout-drawer-detail @input="viewType = $event" :value="viewType" />
|
||||
<portal-target name="drawer" />
|
||||
</template>
|
||||
|
||||
<v-dialog v-if="deleteError" active>
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('something_went_wrong') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-error :error="deleteError" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-button @click="deleteError = null">{{ $t('done') }}</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</private-view>
|
||||
</template>
|
||||
|
||||
@@ -169,6 +220,7 @@ import BookmarkAdd from '@/views/private/components/bookmark-add';
|
||||
import BookmarkEdit from '@/views/private/components/bookmark-edit';
|
||||
import router from '@/router';
|
||||
import marked from 'marked';
|
||||
import { usePermissionsStore } from '@/stores';
|
||||
|
||||
type Item = {
|
||||
[field: string]: any;
|
||||
@@ -195,6 +247,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const permissionsStore = usePermissionsStore();
|
||||
const layout = ref<LayoutComponent | null>(null);
|
||||
|
||||
const { collection } = toRefs(props);
|
||||
@@ -215,7 +268,15 @@ export default defineComponent({
|
||||
saveCurrentAsBookmark,
|
||||
title: bookmarkName,
|
||||
} = usePreset(collection, bookmarkID);
|
||||
const { confirmDelete, deleting, batchDelete } = useBatchDelete();
|
||||
const {
|
||||
confirmDelete,
|
||||
deleting,
|
||||
batchDelete,
|
||||
confirmSoftDelete,
|
||||
softDelete,
|
||||
softDeleting,
|
||||
error: deleteError,
|
||||
} = useBatchDelete();
|
||||
|
||||
const {
|
||||
bookmarkDialogActive,
|
||||
@@ -235,6 +296,8 @@ export default defineComponent({
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const { batchEditAllowed, batchSoftDeleteAllowed, batchDeleteAllowed } = usePermissions();
|
||||
|
||||
return {
|
||||
addNewLink,
|
||||
batchDelete,
|
||||
@@ -262,6 +325,13 @@ export default defineComponent({
|
||||
breadcrumb,
|
||||
marked,
|
||||
clearFilters,
|
||||
confirmSoftDelete,
|
||||
softDelete,
|
||||
softDeleting,
|
||||
batchEditAllowed,
|
||||
batchSoftDeleteAllowed,
|
||||
batchDeleteAllowed,
|
||||
deleteError,
|
||||
};
|
||||
|
||||
function useBreadcrumb() {
|
||||
@@ -291,28 +361,50 @@ export default defineComponent({
|
||||
const confirmDelete = ref(false);
|
||||
const deleting = ref(false);
|
||||
|
||||
return { confirmDelete, deleting, batchDelete };
|
||||
const confirmSoftDelete = ref(false);
|
||||
const softDeleting = ref(false);
|
||||
|
||||
const error = ref<any>();
|
||||
|
||||
return { confirmDelete, deleting, batchDelete, confirmSoftDelete, softDeleting, softDelete, error };
|
||||
|
||||
async function batchDelete() {
|
||||
deleting.value = true;
|
||||
|
||||
confirmDelete.value = false;
|
||||
|
||||
const batchPrimaryKeys = selection.value;
|
||||
|
||||
try {
|
||||
await api.delete(`/items/${props.collection}/${batchPrimaryKeys}`);
|
||||
|
||||
await layout.value?.refresh?.();
|
||||
|
||||
selection.value = [];
|
||||
confirmDelete.value = false;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
error.value = err;
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function softDelete() {
|
||||
if (!currentCollection.value?.meta?.soft_delete_field) return;
|
||||
|
||||
softDeleting.value = true;
|
||||
|
||||
const batchPrimaryKeys = selection.value;
|
||||
|
||||
try {
|
||||
await api.patch(`/items/${props.collection}/${batchPrimaryKeys}`);
|
||||
await layout.value?.refresh?.();
|
||||
|
||||
selection.value = [];
|
||||
confirmSoftDelete.value = false;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
softDeleting.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useLinks() {
|
||||
@@ -374,6 +466,36 @@ export default defineComponent({
|
||||
filters.value = [];
|
||||
searchQuery.value = null;
|
||||
}
|
||||
|
||||
function usePermissions() {
|
||||
const batchEditAllowed = computed(() => {
|
||||
const updatePermissions = permissionsStore.state.permissions.find(
|
||||
(permission) => permission.action === 'update' && permission.collection === collection.value
|
||||
);
|
||||
return !!updatePermissions;
|
||||
});
|
||||
|
||||
const batchSoftDeleteAllowed = computed(() => {
|
||||
if (!currentCollection.value?.meta?.soft_delete_field) return false;
|
||||
|
||||
const updatePermissions = permissionsStore.state.permissions.find(
|
||||
(permission) => permission.action === 'update' && permission.collection === collection.value
|
||||
);
|
||||
if (!updatePermissions) return false;
|
||||
if (!updatePermissions.fields) return false;
|
||||
if (updatePermissions.fields === '*') return true;
|
||||
return updatePermissions.fields.split(',').includes(currentCollection.value.meta.soft_delete_field);
|
||||
});
|
||||
|
||||
const batchDeleteAllowed = computed(() => {
|
||||
const deletePermissions = permissionsStore.state.permissions.find(
|
||||
(permission) => permission.action === 'delete' && permission.collection === collection.value
|
||||
);
|
||||
return !!deletePermissions;
|
||||
});
|
||||
|
||||
return { batchEditAllowed, batchSoftDeleteAllowed, batchDeleteAllowed };
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -386,6 +508,13 @@ export default defineComponent({
|
||||
--v-button-color-hover: var(--danger);
|
||||
}
|
||||
|
||||
.action-soft-delete {
|
||||
--v-button-background-color: var(--warning-25);
|
||||
--v-button-color: var(--warning);
|
||||
--v-button-background-color-hover: var(--warning-50);
|
||||
--v-button-color-hover: var(--warning);
|
||||
}
|
||||
|
||||
.action-batch {
|
||||
--v-button-background-color: var(--warning-25);
|
||||
--v-button-color: var(--warning);
|
||||
|
||||
@@ -189,8 +189,9 @@ import i18n from '@/lang';
|
||||
import marked from 'marked';
|
||||
import useShortcut from '@/composables/use-shortcut';
|
||||
import { NavigationGuard } from 'vue-router';
|
||||
import { usePermissionsStore, useUserStore } from '@/stores';
|
||||
import { useUserStore } from '@/stores';
|
||||
import generateJoi from '@/utils/generate-joi';
|
||||
import { isAllowed } from '@/utils/is-allowed';
|
||||
|
||||
type Values = {
|
||||
[field: string]: any;
|
||||
@@ -216,7 +217,6 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const permissionsStore = usePermissionsStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const { collection, primaryKey } = toRefs(props);
|
||||
@@ -384,42 +384,17 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function usePermissions() {
|
||||
const permissions = computed(() => {
|
||||
return permissionsStore.state.permissions.filter(
|
||||
(permission) => permission.collection === props.collection
|
||||
);
|
||||
});
|
||||
|
||||
const deleteAllowed = computed(() => isAllowed('delete'));
|
||||
const saveAllowed = computed(() => isAllowed('update'));
|
||||
const deleteAllowed = computed(() => isAllowed(collection.value, 'delete', item.value));
|
||||
const saveAllowed = computed(() => isAllowed(collection.value, 'update', item.value));
|
||||
const softDeleteAllowed = computed(() => {
|
||||
if (!collectionInfo.value?.meta?.soft_delete_field) return false;
|
||||
|
||||
return isAllowed('update', {
|
||||
return isAllowed(collection.value, 'update', {
|
||||
[collectionInfo.value.meta.soft_delete_field]: collectionInfo.value.meta.soft_delete_value,
|
||||
});
|
||||
});
|
||||
|
||||
return { deleteAllowed, saveAllowed, softDeleteAllowed };
|
||||
|
||||
function isAllowed(action: string, value?: any) {
|
||||
value = value || item.value;
|
||||
|
||||
if (userStore.isAdmin.value === true) return true;
|
||||
|
||||
const permissionInfo = permissions.value.find((permission) => permission.action === action);
|
||||
|
||||
if (!permissionInfo) return false;
|
||||
|
||||
const schema = generateJoi(permissionInfo.permissions, { allowUnknown: permissionInfo.fields === '*' });
|
||||
const { error } = schema.validate(value);
|
||||
|
||||
if (!error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
25
app/src/utils/is-allowed.ts
Normal file
25
app/src/utils/is-allowed.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { usePermissionsStore, useUserStore } from '@/stores';
|
||||
import { Permission } from '@/types';
|
||||
import generateJoi from '@/utils/generate-joi';
|
||||
|
||||
export function isAllowed(collection: string, action: Permission['action'], value: Record<string, any>) {
|
||||
const permissionsStore = usePermissionsStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
if (userStore.isAdmin.value === true) return true;
|
||||
|
||||
const permissions = permissionsStore.state.permissions;
|
||||
|
||||
const permissionInfo = permissions.find((permission) => permission.action === action);
|
||||
|
||||
if (!permissionInfo) return false;
|
||||
|
||||
const schema = generateJoi(permissionInfo.permissions, { allowUnknown: permissionInfo.fields === '*' });
|
||||
const { error } = schema.validate(value);
|
||||
|
||||
if (!error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user