Permissions for modules (and bar) and other APP UI components #20975
Replies: 1 comment · 2 replies
-
I'm facing the same limitation: in my application, some roles need to maintain READ access to a collection for the sake of some extensions, but not see it in UI.module-nav. Meanwhile this useful feature is implemented, how can I hide some collections based on the user's role using CSS? |
Beta Was this translation helpful? Give feedback.
All reactions
-
I created a module extension that I use to hide what I don't want using CSS based on the user's role, or even username if I wanted to. The following are code snippets: // module's index.ts
import { defineModule } from '@directus/extensions-sdk';
import { watch } from 'vue';
import { removeElementsByUser } from './removeElementsByUser';
export default defineModule(
{
id : '@emahuni/app-control',
name : 'App-Control',
icon : 'tune',
hidden : true,
routes : [
{
name : 'app-control-root',
path : '',
redirect: '/dashboard', // put a path you want it to redirect to here, this is so we don't put a component
},
],
// I wish this hook was put in other non-api extensions as well; very useful.
preRegisterCheck: (user, _permissions) => {
/**
* we are doing this to prevent wrong state of app control on logout then login of different user roles
* somehow the module bar doesn't show properly without refresh because of this hack
* */
const storedRole = localStorage.getItem('app-control-role');
if (user.role?.id !== storedRole && typeof user.role?.id === 'string') {
localStorage.setItem('app-control-role', user.role?.id);
if (!!storedRole) window.location.reload(); // only reload if we had a different role to begin with
}
/** watch when user changes module or page **/
watch(() => user.last_page, (_nv, _ov) => {
removeElementsByUser(user as Schema.DirectusUser);
}, { immediate: true });
}) removeElementsByUser.ts: // removeElementsByUser.ts
import awaitUntil from 'await-until';
import { useStyleTag } from '@vueuse/core';
import { DIRECTOR, PRODUCER } from '@emahuni/hub.constants/dist/roles';
let wasInitialised = false;
export async function removeElementsByUser (user: Schema.DirectusUser) {
/** hide and remove things for all */
applyToSelectors(
[
/** Restrictions */
`body.admin-content-attendances .header-bar .actions .v-button:has(i[data-icon="add"])`, // remove adding of attendances from attendances layout
/** Fixes of unwanted behaviour */
],
'all-style',
[ hideElsBySelectors, removeSelectorsEls ],
);
/** hide (only) things for all, these are global and needed elsewhere, removing them will prevent them from showing even if they should show */
applyToSelectors(
[
/** Restrictions */
/** Fixes of unwanted behaviour */
`body.admin-users .header-bar .actions .v-button:has(i[data-icon="person_add"])`, // hide inviting of users from all users layout
`body.admin-users .header-bar .actions .v-button:has(i[data-icon="add"])`, // hide adding of users from all users layout
],
'all-hide-style',
[ hideElsBySelectors ],
);
if (!!user && !(user.role as Schema.DirectusRole)?.admin_access) {
/** should not view any of the following since they are not admins */
applyToSelectors(
[
/** todo temporary blockage THINGS I JUST DON'T WANT TO INTRODUCE TO USERS JUST YET, THAT'S ALL */
'#sidebar', // remove sidebar
// '.manual.append.cell', // add table layout column
`.calendar-layout`, // remove calendar layout
`button:has(i[data-icon="notifications"])`, // remove notifications btn
`.header-bar .actions div.v-button:has(i[data-icon="info"])`, // remove notifications btn on mobiles
/** Restrictions THIS IS WHAT YOU ARE ASKING, */
`.module-nav ul > a[href="/admin/content/articles"]`, // remove articles content collection entry, leave others
/** Fixes of unwanted behaviour */
`.bookmark-controls`, // remove bookmark controls
],
'non-admin-style',
[ hideElsBySelectors, removeSelectorsEls ],
);
/** since hiding is persistent until refresh, we need to remove elements on each page change based on user */
/********** should not view content THIS IS WHAT YOU ARE ASKING ***********/
if (![
PRODUCER, // not producer
DIRECTOR, // not director
].includes((user.role as Schema.DirectusRole)?.id)) {
applyToSelectors(
[
`div.v-button:has(a[href="/admin/content"])`, // remove content module button completely
],
'non-cms-style',
[ hideElsBySelectors, removeSelectorsEls ],
);
}
/********** should not view pages collection ***********/
if (![
PRODUCER, // not producer
DIRECTOR, // not director
].includes((user.role as Schema.DirectusRole)?.id)) {
applyToSelectors(
[
`.module-nav ul > a[href="/admin/content/pages"]`, // remove the module navigation button for pages, leave others
],
'non-producer-style',
[ hideElsBySelectors, removeSelectorsEls ],
);
}
/********** should not view all users or roles THIS IS WHAT YOU ARE ASKING ***********/
if (![
PRODUCER, // not producer
DIRECTOR, // not director
].includes((user.role as Schema.DirectusRole)?.id)) {
applyToSelectors(
[
`.module-nav ul > a[href="/admin/users"]`, // modules-bar nav link
`div.v-button:has(a[href="/admin/users"])`,
`[class*="admin-users"] .layout-cards > .cards-header`, // users/roles directory view controls
`[class*="admin-users"] .header-bar .actions`, // users/roles directory actions
`[class*="admin-users"] .header-bar .item-count`, // users/roles directory actions
`.v-breadcrumb a[href="/admin/users"]`, // user directory breadcrumb link
],
'non-moderator-style',
[ hideElsBySelectors, removeSelectorsEls ],
);
}
}
wasInitialised = true; // flag that initialisation was done
}
/**
* Applies a list of callbacks to a set of selectors and an ID.
*
* @param {string[]} selectors - The list of selectors to apply the callbacks to.
* @param {string} id - The ID to pass to the callbacks.
* @param {Function[]} cbs - The list of callbacks to apply.
*/
function applyToSelectors (selectors: string[], id: string, cbs: ((selectors: string[], id: string) => void)[]) {
cbs.forEach((cb) => cb(selectors, id));
}
/**
* Removes elements from the DOM based on the given selectors.
*
* @param {string[]} selectors - The selectors of the elements to be removed.
*
* @return {void} - This method does not return a value.
*/
function removeSelectorsEls (selectors: string[]) {
/** now completely remove the elements from the DOM when they become available. Is done on every navigation. */
selectors.forEach(removeElementBySelector);
}
/**
* Hide elements by selectors.
*
* @param {string[]} selectors - An array of CSS selectors to hide elements.
* @param {string} id - The ID of the style tag to be added.
*/
function hideElsBySelectors (selectors: string[], id: string) {
if (!wasInitialised) {
/** Just quickly hide all selectables via css. This prevents the elements from briefly showing up in DOM on navigation. */
const style = selectors.join(`, `) + ` {
display: none !important;
}`;
const { css: _css } = useStyleTag(style, { id, immediate: true });
}
}
/**
* Removes all elements from the DOM that match the given selector.
*
* @param {string} selector - The CSS selector to match the elements.
* @returns {Promise<void>} - A promise that resolves once all matching elements are removed.
*/
async function removeElementBySelector (selector: string) {
try {
const elements = await awaitUntil<NodeListOf<Element>>({
worker : () => document.querySelectorAll(selector),
predicate: ({ data }) => !!data.length,
interval : 10, timeout: 1000,
});
elements.forEach((el) => {
el.remove();
});
} catch (e) {
console.warn(`[removeElementsByUser/removeElementBySelector()-162] couldn't find elements for selector %o`, selector);
}
} PRODUCER and DIRECTOR are role constants of their IDs. you can put whatever you want. I use this |
Beta Was this translation helpful? Give feedback.
All reactions
-
I used to do this in |
Beta Was this translation helpful? Give feedback.
-
Summary
With an application that has several modules that we may not want certain user roles or users to access, I think it could be beneficiary for Directus to have some sort of way to also restrict access to modules based on roles just like the collection permissions.
In addition to collections permissions CRUD..., custom permissions could also go a long way to ensure that we can add some custom permissions configurations easily.
Basic Example
Modules permissions
I want role A to be able to access module "Test", but role B should not. Therefore, the Test module icon should not appear on the modules bar for Role B, but should appear for role A.
Custom Permissions
Given that we have a messages module, that has other actions that a user can take, I may want role A to be able to send chat messages, but not role B. A "send messages" permission should be configurable in ACL view. This is defined by the admin.
Motivation
Different roles should be able to access different modules, content/collections, components and do different things from each other within the same app.
Detailed Design
NA
Requirements List
All major APP components need to be permissible. eg:
Drawbacks
I have noticed a huge rewrite coming for roles and permissions; therefore, this may need to be considered as well.
Alternatives
I have workarounds, but they are hacky, hence the request:
Adoption Strategy
NA
Unresolved Questions
No response
Beta Was this translation helpful? Give feedback.
All reactions