mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add Joi validation for delete button
This commit is contained in:
@@ -101,12 +101,12 @@ export default class FieldsService {
|
||||
const result = [...columnsWithSystem, ...aliasFieldsAsField];
|
||||
|
||||
// Filter the result so we only return the fields you have read access to
|
||||
if (this.accountability) {
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
const permissions = await this.knex.select('collection', 'fields').from('directus_permissions').where({ role: this.accountability.role, action: 'read' });
|
||||
const allowedFieldsInCollection: Record<string, string[]> = {};
|
||||
|
||||
permissions.forEach((permission) => {
|
||||
allowedFieldsInCollection[permission.collection] = permission.fields.split(',');
|
||||
allowedFieldsInCollection[permission.collection] = (permission.fields || '').split(',');
|
||||
});
|
||||
|
||||
if (collection && allowedFieldsInCollection.hasOwnProperty(collection) === false) {
|
||||
@@ -125,7 +125,7 @@ export default class FieldsService {
|
||||
}
|
||||
|
||||
async readOne(collection: string, field: string) {
|
||||
if (this.accountability) {
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
const permissions = await this.knex
|
||||
.select('fields')
|
||||
.from('directus_permissions')
|
||||
@@ -137,7 +137,7 @@ export default class FieldsService {
|
||||
|
||||
if (!permissions) throw new ForbiddenException();
|
||||
if (permissions.fields !== '*') {
|
||||
const allowedFields = permissions.fields.split(',');
|
||||
const allowedFields = (permissions.fields || '').split(',');
|
||||
if (allowedFields.includes(field) === false) throw new ForbiddenException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Filter } from '../types';
|
||||
import Joi, { AnySchema } from 'joi';
|
||||
|
||||
/**
|
||||
* @TODO
|
||||
* This is copy pasted between app and api. Make this a reusable module.
|
||||
*/
|
||||
|
||||
export default function generateJoi(filter: Filter | null) {
|
||||
filter = filter || {};
|
||||
|
||||
@@ -12,10 +17,6 @@ export default function generateJoi(filter: Filter | null) {
|
||||
if (isField) {
|
||||
const operator = Object.keys(value)[0];
|
||||
|
||||
/** @TODO
|
||||
* - Extend with all operators
|
||||
*/
|
||||
|
||||
if (operator === '_eq') {
|
||||
schema[key] = Joi.any().equal(Object.values(value)[0]);
|
||||
}
|
||||
@@ -24,6 +25,30 @@ export default function generateJoi(filter: Filter | null) {
|
||||
schema[key] = Joi.any().not(Object.values(value)[0]);
|
||||
}
|
||||
|
||||
if (operator === '_contains') {
|
||||
schema[key] = Joi.string().custom((value, helpers) => {
|
||||
const contains = value.includes(Object.values(value)[0]);
|
||||
|
||||
if (contains === false) {
|
||||
return helpers.error(`"${key}" must include "${Object.values(value)[0]}"`);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
if (operator === '_ncontains') {
|
||||
schema[key] = Joi.string().custom((value, helpers) => {
|
||||
const contains = value.includes(Object.values(value)[0]);
|
||||
|
||||
if (contains === true) {
|
||||
return helpers.error(`"${key}" can't include "${Object.values(value)[0]}"`);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
if (operator === '_in') {
|
||||
schema[key] = Joi.any().equal(...(Object.values(value)[0] as (string | number)[]));
|
||||
}
|
||||
@@ -36,9 +61,43 @@ export default function generateJoi(filter: Filter | null) {
|
||||
schema[key] = Joi.number().greater(Number(Object.values(value)[0]));
|
||||
}
|
||||
|
||||
if (operator === '_gte') {
|
||||
schema[key] = Joi.number().min(Number(Object.values(value)[0]));
|
||||
}
|
||||
|
||||
if (operator === '_lt') {
|
||||
schema[key] = Joi.number().less(Number(Object.values(value)[0]));
|
||||
}
|
||||
|
||||
if (operator === '_lte') {
|
||||
schema[key] = Joi.number().max(Number(Object.values(value)[0]));
|
||||
}
|
||||
|
||||
if (operator === '_null') {
|
||||
schema[key] = Joi.any().valid(null);
|
||||
}
|
||||
|
||||
if (operator === '_nnull') {
|
||||
schema[key] = Joi.any().invalid(null);
|
||||
}
|
||||
|
||||
if (operator === '_empty') {
|
||||
schema[key] = Joi.any().valid('');
|
||||
}
|
||||
|
||||
if (operator === '_nempty') {
|
||||
schema[key] = Joi.any().invalid('');
|
||||
}
|
||||
|
||||
if (operator === '_between') {
|
||||
const values = Object.values(value)[0] as number[];
|
||||
schema[key] = Joi.number().greater(values[0]).less(values[1]);
|
||||
}
|
||||
|
||||
if (operator === '_nbetween') {
|
||||
const values = Object.values(value)[0] as number[];
|
||||
schema[key] = Joi.number().less(values[0]).greater(values[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
45
app/package-lock.json
generated
45
app/package-lock.json
generated
@@ -1274,6 +1274,11 @@
|
||||
"integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==",
|
||||
"dev": true
|
||||
},
|
||||
"@hapi/formula": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-2.0.0.tgz",
|
||||
"integrity": "sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A=="
|
||||
},
|
||||
"@hapi/hoek": {
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz",
|
||||
@@ -1292,6 +1297,11 @@
|
||||
"@hapi/topo": "3.x.x"
|
||||
}
|
||||
},
|
||||
"@hapi/pinpoint": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.0.tgz",
|
||||
"integrity": "sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw=="
|
||||
},
|
||||
"@hapi/topo": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz",
|
||||
@@ -14110,6 +14120,41 @@
|
||||
"supports-color": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"joi": {
|
||||
"version": "17.2.1",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.2.1.tgz",
|
||||
"integrity": "sha512-YT3/4Ln+5YRpacdmfEfrrKh50/kkgX3LgBltjqnlMPIYiZ4hxXZuVJcxmsvxsdeHg9soZfE3qXxHC2tMpCCBOA==",
|
||||
"requires": {
|
||||
"@hapi/address": "^4.1.0",
|
||||
"@hapi/formula": "^2.0.0",
|
||||
"@hapi/hoek": "^9.0.0",
|
||||
"@hapi/pinpoint": "^2.0.0",
|
||||
"@hapi/topo": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/address": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.1.0.tgz",
|
||||
"integrity": "sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ==",
|
||||
"requires": {
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"@hapi/hoek": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.0.4.tgz",
|
||||
"integrity": "sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw=="
|
||||
},
|
||||
"@hapi/topo": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz",
|
||||
"integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==",
|
||||
"requires": {
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"js-beautify": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.11.0.tgz",
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"date-fns": "^2.14.0",
|
||||
"diff": "^4.0.2",
|
||||
"htmlhint": "^0.14.1",
|
||||
"joi": "^17.2.1",
|
||||
"js-yaml": "^3.14.0",
|
||||
"jshint": "^2.11.1",
|
||||
"jsonlint": "^1.6.3",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"public": "Public",
|
||||
"public_description": "Controls what API data is available without authenticating.",
|
||||
|
||||
"not_allowed": "Not Allowed",
|
||||
|
||||
"nested_files_folders_will_be_moved": "Nested files and folders will be moved one level up.",
|
||||
|
||||
"markdown": "Markdown",
|
||||
|
||||
@@ -366,7 +366,7 @@ export default defineComponent({
|
||||
|
||||
function useTable() {
|
||||
const tableSort = computed(() => {
|
||||
if (sort.value.startsWith('-')) {
|
||||
if (sort.value?.startsWith('-')) {
|
||||
return { by: sort.value.substring(1), desc: true };
|
||||
} else {
|
||||
return { by: sort.value, desc: false };
|
||||
|
||||
@@ -44,14 +44,14 @@
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<v-dialog v-if="!isNew" v-model="confirmDelete">
|
||||
<v-dialog v-if="!isNew" v-model="confirmDelete" :disabled="deleteAllowed === false">
|
||||
<template #activator="{ on }">
|
||||
<v-button
|
||||
rounded
|
||||
icon
|
||||
class="action-delete"
|
||||
v-tooltip.bottom="$t('delete_forever')"
|
||||
:disabled="item === null"
|
||||
v-tooltip.bottom="deleteAllowed ? $t('delete_forever') : $t('not_allowed')"
|
||||
:disabled="item === null || deleteAllowed === false"
|
||||
@click="on"
|
||||
v-if="collectionInfo.meta.singleton === false"
|
||||
>
|
||||
@@ -185,6 +185,8 @@ 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 generateJoi from '@/utils/generate-joi';
|
||||
|
||||
type Values = {
|
||||
[field: string]: any;
|
||||
@@ -210,6 +212,9 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const permissionsStore = usePermissionsStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const { collection, primaryKey } = toRefs(props);
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
|
||||
@@ -276,6 +281,8 @@ export default defineComponent({
|
||||
return next();
|
||||
};
|
||||
|
||||
const { deleteAllowed } = usePermissions();
|
||||
|
||||
return {
|
||||
item,
|
||||
loading,
|
||||
@@ -307,6 +314,7 @@ export default defineComponent({
|
||||
leaveTo,
|
||||
discardAndLeave,
|
||||
navigationGuard,
|
||||
deleteAllowed,
|
||||
};
|
||||
|
||||
function useBreadcrumb() {
|
||||
@@ -357,6 +365,31 @@ export default defineComponent({
|
||||
edits.value = {};
|
||||
router.push(leaveTo.value);
|
||||
}
|
||||
|
||||
function usePermissions() {
|
||||
const permissions = computed(() => {
|
||||
return permissionsStore.state.permissions.filter((permission) => permission.collection === props.collection);
|
||||
});
|
||||
|
||||
const deleteAllowed = computed(() => {
|
||||
if (userStore.isAdmin.value === true) return true;
|
||||
|
||||
const deletePermission = permissions.value.find((permission) => permission.action === 'delete');
|
||||
|
||||
if (!deletePermission) return false;
|
||||
|
||||
const schema = generateJoi(deletePermission.permissions);
|
||||
const { error } = schema.validate(item.value);
|
||||
|
||||
if (!error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return { deleteAllowed };
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { createStore } from 'pinia';
|
||||
import api from '@/api';
|
||||
import { Permission } from '@/types';
|
||||
|
||||
export const usePermissionsStore = createStore({
|
||||
id: 'permissionsStore',
|
||||
state: () => ({
|
||||
permissions: [],
|
||||
permissions: [] as Permission[],
|
||||
}),
|
||||
actions: {
|
||||
async hydrate() {
|
||||
|
||||
104
app/src/utils/generate-joi/index.ts
Normal file
104
app/src/utils/generate-joi/index.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import Joi, { AnySchema } from 'joi';
|
||||
|
||||
/**
|
||||
* @TODO
|
||||
* This is copy pasted between app and api. Make this a reusable module.
|
||||
*/
|
||||
|
||||
export default function generateJoi(filter: Record<string, any> | null) {
|
||||
filter = filter || {};
|
||||
|
||||
const schema: Record<string, AnySchema> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
const isField = key.startsWith('_') === false;
|
||||
|
||||
if (isField) {
|
||||
const operator = Object.keys(value)[0];
|
||||
|
||||
if (operator === '_eq') {
|
||||
schema[key] = Joi.any().equal(Object.values(value)[0]);
|
||||
}
|
||||
|
||||
if (operator === '_neq') {
|
||||
schema[key] = Joi.any().not(Object.values(value)[0]);
|
||||
}
|
||||
|
||||
if (operator === '_contains') {
|
||||
schema[key] = Joi.string().custom((value, helpers) => {
|
||||
const contains = value.includes(Object.values(value)[0]);
|
||||
|
||||
if (contains === false) {
|
||||
return helpers.error(`"${key}" must include "${Object.values(value)[0]}"`);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
if (operator === '_ncontains') {
|
||||
schema[key] = Joi.string().custom((value, helpers) => {
|
||||
const contains = value.includes(Object.values(value)[0]);
|
||||
|
||||
if (contains === true) {
|
||||
return helpers.error(`"${key}" can't include "${Object.values(value)[0]}"`);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
if (operator === '_in') {
|
||||
schema[key] = Joi.any().equal(...(Object.values(value)[0] as (string | number)[]));
|
||||
}
|
||||
|
||||
if (operator === '_nin') {
|
||||
schema[key] = Joi.any().not(...(Object.values(value)[0] as (string | number)[]));
|
||||
}
|
||||
|
||||
if (operator === '_gt') {
|
||||
schema[key] = Joi.number().greater(Number(Object.values(value)[0]));
|
||||
}
|
||||
|
||||
if (operator === '_gte') {
|
||||
schema[key] = Joi.number().min(Number(Object.values(value)[0]));
|
||||
}
|
||||
|
||||
if (operator === '_lt') {
|
||||
schema[key] = Joi.number().less(Number(Object.values(value)[0]));
|
||||
}
|
||||
|
||||
if (operator === '_lte') {
|
||||
schema[key] = Joi.number().max(Number(Object.values(value)[0]));
|
||||
}
|
||||
|
||||
if (operator === '_null') {
|
||||
schema[key] = Joi.any().valid(null);
|
||||
}
|
||||
|
||||
if (operator === '_nnull') {
|
||||
schema[key] = Joi.any().invalid(null);
|
||||
}
|
||||
|
||||
if (operator === '_empty') {
|
||||
schema[key] = Joi.any().valid('');
|
||||
}
|
||||
|
||||
if (operator === '_nempty') {
|
||||
schema[key] = Joi.any().invalid('');
|
||||
}
|
||||
|
||||
if (operator === '_between') {
|
||||
const values = Object.values(value)[0] as number[];
|
||||
schema[key] = Joi.number().greater(values[0]).less(values[1]);
|
||||
}
|
||||
|
||||
if (operator === '_nbetween') {
|
||||
const values = Object.values(value)[0] as number[];
|
||||
schema[key] = Joi.number().less(values[0]).greater(values[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Joi.object(schema).unknown();
|
||||
}
|
||||
Reference in New Issue
Block a user