From bb85e8b3cb33afccfdb9dfd5479800ff160c1a87 Mon Sep 17 00:00:00 2001 From: alexm Date: Fri, 20 Sep 2024 11:43:15 +0200 Subject: [PATCH] feat(useAcl): create checkUrl --- src/boot/quasar.js | 10 ++++- src/composables/useAcl.js | 72 +++++++++++++++++++++++++++++---- src/composables/useSession.js | 2 +- src/stores/useArrayDataStore.js | 1 + src/utils/errors.js | 9 +++++ 5 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 src/utils/errors.js diff --git a/src/boot/quasar.js b/src/boot/quasar.js index 5db6edd24..3cd917958 100644 --- a/src/boot/quasar.js +++ b/src/boot/quasar.js @@ -3,6 +3,8 @@ import qFormMixin from './qformMixin'; import mainShortcutMixin from './mainShortcutMixin'; import keyShortcut from './keyShortcut'; import useNotify from 'src/composables/useNotify.js'; +import { AccessError } from 'src/utils/errors'; + const { notify } = useNotify(); export default boot(({ app }) => { @@ -11,6 +13,12 @@ export default boot(({ app }) => { app.directive('shortcut', keyShortcut); app.config.errorHandler = function (err) { console.error(err); - notify('globals.error', 'negative', 'error'); + switch (err.constructor) { + case AccessError: + notify('errors.statusUnauthorized', 'negative', 'account_circle_off'); + break; + default: + notify('globals.error', 'negative', 'error'); + } }; }); diff --git a/src/composables/useAcl.js b/src/composables/useAcl.js index ede359186..5825ff5e8 100644 --- a/src/composables/useAcl.js +++ b/src/composables/useAcl.js @@ -1,5 +1,6 @@ import axios from 'axios'; import { useState } from './useState'; +import { AccessError } from 'src/utils/errors'; export function useAcl() { const state = useState(); @@ -8,31 +9,86 @@ export function useAcl() { const { data } = await axios.get('VnUsers/acls'); const acls = {}; data.forEach((acl) => { - acls[acl.model] = acls[acl.model] || {}; - acls[acl.model][acl.property] = acls[acl.model][acl.property] || {}; - acls[acl.model][acl.property][acl.accessType] = true; + const model = acl.model.toLowerCase(); + const property = acl.property.toLowerCase(); + acls[model] = acls[model] || {}; + acls[model][property] = acls[model][property] || {}; + acls[model][property][acl.accessType] = true; }); state.setAcls(acls); } function hasAny(acls) { + let result = false; for (const acl of acls) { let { model, props, accessType } = acl; - const modelAcls = state.getAcls().value[model]; + const modelAcls = state.getAcls().value[model.toLowerCase()]; Array.isArray(props) || (props = [props]); - if (modelAcls) - return ['*', ...props].some((key) => { - const acl = modelAcls[key]; + if (modelAcls) { + result = ['*', ...props].some((key) => { + const acl = modelAcls[key.toLowerCase()]; return acl && (acl['*'] || acl[accessType]); }); + } + if (result) return result; } - return false; + return result; + } + + function checkRead(model, urlSplit) { + const acls = []; + let props = urlSplit[1] ?? 'find'; + if (typeof +props == 'number') { + props = urlSplit[2] ?? 'findById'; + acls.push({ model, props: `__get__${props}`, accessType: 'READ' }); + } + + acls.push({ model, props, accessType: 'READ' }); + console.log('acls: ', acls); + console.log('hasAny(acls): ', hasAny(acls)); + return hasAny(acls); + } + + function checkWrite(model, urlSplit, type) { + const acls = []; + let props = urlSplit[1] ?? (type != 'post' ? 'upsert' : 'create'); + + if (typeof +props == 'number') { + if (!urlSplit[2]) props = 'updateAttributes'; + else if (['post', 'delete'].includes(type)) { + let prefix = 'create'; + if (type == 'delete') prefix = type; + acls.push({ model, props: `__${prefix}__${props}`, accessType: 'WRITE' }); + } + } + + acls.push({ model, props, accessType: 'WRITE' }); + console.log('acls: ', acls); + console.log('hasAny(acls): ', hasAny(acls)); + return hasAny(acls); + } + + function checkUrl(url, type = 'get', throwError, model) { + if (!url) return true; + + const urlSplit = url.split('/'); + model ??= urlSplit[0]?.slice(1, -1); + type = type.toLowerCase(); + let hasPermission; + if (type == 'get') hasPermission = checkRead(model, urlSplit); + else hasPermission = checkWrite(model, urlSplit, type); + + if (throwError && !hasPermission) throw new AccessError(url); + return hasPermission; } return { fetch, hasAny, state, + checkRead, + checkWrite, + checkUrl, }; } diff --git a/src/composables/useSession.js b/src/composables/useSession.js index 633a30bb0..6a4ea1842 100644 --- a/src/composables/useSession.js +++ b/src/composables/useSession.js @@ -54,7 +54,7 @@ export function useSession() { headers: { Authorization: storage.getItem(key) }, }); } catch (error) { - notify('errors.statusUnauthorized', 'negative'); + notify('errors.statusUnauthorized', 'negative', 'account_circle_off'); } finally { storage.removeItem(key); } diff --git a/src/stores/useArrayDataStore.js b/src/stores/useArrayDataStore.js index ea029bc12..063e11bf4 100644 --- a/src/stores/useArrayDataStore.js +++ b/src/stores/useArrayDataStore.js @@ -17,6 +17,7 @@ export const useArrayDataStore = defineStore('arrayDataStore', () => { searchUrl: 'params', navigate: null, page: 1, + checkAcl: true, }; function get(key) { diff --git a/src/utils/errors.js b/src/utils/errors.js new file mode 100644 index 000000000..f8c86fd76 --- /dev/null +++ b/src/utils/errors.js @@ -0,0 +1,9 @@ +class AccessError extends Error { + constructor(details, message) { + super(message ?? 'Access denied'); + this.name = AccessError.name; + this.details = details; + } +} + +export { AccessError };