diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index bb2caf3..82ba410 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -90,86 +90,6 @@ await queryInterface.bulkInsert("rolesPermissionsPermissions", [ - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_USERS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_USERS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_USERS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_USERS') }, - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("SupportLead"), permissionId: getId('READ_USERS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("SupportLead"), permissionId: getId('UPDATE_USERS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AgentCurator"), permissionId: getId('READ_USERS') }, - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("Analyst"), permissionId: getId('READ_USERS') }, - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("Member"), permissionId: getId('READ_USERS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("Member"), permissionId: getId('UPDATE_USERS') }, - - - - - @@ -749,4 +669,3 @@ await queryInterface.bulkInsert("rolesPermissionsPermissions", [ } }; - diff --git a/backend/src/middlewares/check-permissions.js b/backend/src/middlewares/check-permissions.js index 77740c7..75879ab 100644 --- a/backend/src/middlewares/check-permissions.js +++ b/backend/src/middlewares/check-permissions.js @@ -2,6 +2,13 @@ const ValidationError = require('../services/notifications/errors/validation'); const RolesDBApi = require('../db/api/roles'); +const USER_PERMISSIONS = new Set([ + 'CREATE_USERS', + 'READ_USERS', + 'UPDATE_USERS', + 'DELETE_USERS', +]); + // Cache for the 'Public' role object let publicRoleCache = null; @@ -47,7 +54,16 @@ function checkPermissions(permission) { return next(); // User has access to their own resource } - // 2. Check Custom Permissions (only if the user is authenticated) + // 2. Only administrators can use global users permissions. + if (USER_PERMISSIONS.has(permission)) { + const roleName = currentUser?.app_role?.name; + + if (roleName !== 'Administrator') { + return next(new ValidationError('auth.forbidden', `Role '${roleName || 'anonymous'}' denied access to '${permission}'.`)); + } + } + + // 3. Check Custom Permissions (only if the user is authenticated) if (currentUser) { // Ensure custom_permissions is an array before using find const customPermissions = Array.isArray(currentUser.custom_permissions) @@ -61,7 +77,7 @@ function checkPermissions(permission) { } } - // 3. Determine the "effective" role for permission check + // 4. Determine the "effective" role for permission check let effectiveRole = null; try { if (currentUser && currentUser.app_role) { @@ -90,7 +106,7 @@ function checkPermissions(permission) { return next(new Error("Internal Server Error: Could not determine effective role.")); } - // 4. Check Permissions on the "effective" role + // 5. Check Permissions on the "effective" role // Assume the effectiveRole object (from app_role or RolesDBApi) has a getPermissions() method // or a 'permissions' property (if permissions are eagerly loaded). let rolePermissions = []; @@ -146,4 +162,3 @@ module.exports = { checkPermissions, checkCrudPermissions, }; - diff --git a/frontend/src/components/Agents/TableAgents.tsx b/frontend/src/components/Agents/TableAgents.tsx index 40711df..fdd69cc 100644 --- a/frontend/src/components/Agents/TableAgents.tsx +++ b/frontend/src/components/Agents/TableAgents.tsx @@ -16,6 +16,7 @@ import {loadColumns} from "./configureAgentsCols"; import _ from 'lodash'; import dataFormatter from '../../helpers/dataFormatter' import {dataGridStyles} from "../../styles"; +import DataGridLoadingOverlay from '../DataGridLoadingOverlay'; @@ -45,6 +46,7 @@ const TableSampleAgents = ({ filterItems, setFilterItems, filters, showGrid }) = const focusRing = useAppSelector((state) => state.style.focusRingColor); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); const corners = useAppSelector((state) => state.style.corners); + const isDataGridLoading = !currentUser || loading || columns.length === 0; const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); for (let i = 0; i < numPages; i++) { pagesList.push(i); @@ -211,7 +213,8 @@ const TableSampleAgents = ({ filterItems, setFilterItems, filters, showGrid }) =
`datagrid--row`} @@ -251,7 +254,10 @@ const TableSampleAgents = ({ filterItems, setFilterItems, filters, showGrid }) = rowCount={count} pageSizeOptions={[10]} paginationMode={'server'} - loading={loading} + loading={isDataGridLoading} + slots={{ + loadingOverlay: DataGridLoadingOverlay, + }} onPaginationModelChange={(params) => { onPageChange(params.page); }} diff --git a/frontend/src/components/Agents/configureAgentsCols.tsx b/frontend/src/components/Agents/configureAgentsCols.tsx index 8136752..d1b2d47 100644 --- a/frontend/src/components/Agents/configureAgentsCols.tsx +++ b/frontend/src/components/Agents/configureAgentsCols.tsx @@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver"; import dataFormatter from '../../helpers/dataFormatter' import DataGridMultiSelect from "../DataGridMultiSelect"; import ListActionsPopover from '../ListActionsPopover'; +import { makeReadableColumns } from '../../helpers/dataGridColumns'; import {hasPermission} from "../../helpers/userPermissions"; @@ -39,7 +40,7 @@ export const loadColumns = async ( const hasUpdatePermission = hasPermission(user, 'UPDATE_AGENTS') - return [ + const columns = [ { field: 'name', @@ -204,4 +205,6 @@ export const loadColumns = async ( }, }, ]; + + return makeReadableColumns(columns); }; diff --git a/frontend/src/components/AsideMenu.tsx b/frontend/src/components/AsideMenu.tsx index 0e878ea..e53d84c 100644 --- a/frontend/src/components/AsideMenu.tsx +++ b/frontend/src/components/AsideMenu.tsx @@ -1,30 +1,18 @@ import React from 'react' import { MenuAsideItem } from '../interfaces' import AsideMenuLayer from './AsideMenuLayer' -import OverlayLayer from './OverlayLayer' type Props = { menu: MenuAsideItem[] - isAsideMobileExpanded: boolean - isAsideLgActive: boolean - onAsideLgClose: () => void } export default function AsideMenu({ - isAsideMobileExpanded = false, - isAsideLgActive = false, ...props }: Props) { return ( - <> - - {isAsideLgActive && } - + ) } diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx index 5b39d36..eb55e27 100644 --- a/frontend/src/components/AsideMenuLayer.tsx +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -1,6 +1,4 @@ import React from 'react' -import { mdiClose } from '@mdi/js' -import BaseIcon from './BaseIcon' import AsideMenuList from './AsideMenuList' import { MenuAsideItem } from '../interfaces' import { useAppSelector } from '../stores/hooks' @@ -9,49 +7,36 @@ import { useAppSelector } from '../stores/hooks' type Props = { menu: MenuAsideItem[] className?: string - onAsideLgCloseClick: () => void } -export default function AsideMenuLayer({ menu, className = '', ...props }: Props) { +export default function AsideMenuLayer({ menu, className = '' }: Props) { const asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle) const darkMode = useAppSelector((state) => state.style.darkMode) - const handleAsideLgCloseClick = (e: React.MouseEvent) => { - e.preventDefault() - props.onAsideLgCloseClick() - } - return ( diff --git a/frontend/src/components/Attachments/TableAttachments.tsx b/frontend/src/components/Attachments/TableAttachments.tsx index 9cc7539..231d5e3 100644 --- a/frontend/src/components/Attachments/TableAttachments.tsx +++ b/frontend/src/components/Attachments/TableAttachments.tsx @@ -16,6 +16,7 @@ import {loadColumns} from "./configureAttachmentsCols"; import _ from 'lodash'; import dataFormatter from '../../helpers/dataFormatter' import {dataGridStyles} from "../../styles"; +import DataGridLoadingOverlay from '../DataGridLoadingOverlay'; @@ -45,6 +46,7 @@ const TableSampleAttachments = ({ filterItems, setFilterItems, filters, showGrid const focusRing = useAppSelector((state) => state.style.focusRingColor); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); const corners = useAppSelector((state) => state.style.corners); + const isDataGridLoading = !currentUser || loading || columns.length === 0; const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); for (let i = 0; i < numPages; i++) { pagesList.push(i); @@ -211,7 +213,8 @@ const TableSampleAttachments = ({ filterItems, setFilterItems, filters, showGrid
`datagrid--row`} @@ -251,7 +254,10 @@ const TableSampleAttachments = ({ filterItems, setFilterItems, filters, showGrid rowCount={count} pageSizeOptions={[10]} paginationMode={'server'} - loading={loading} + loading={isDataGridLoading} + slots={{ + loadingOverlay: DataGridLoadingOverlay, + }} onPaginationModelChange={(params) => { onPageChange(params.page); }} diff --git a/frontend/src/components/Attachments/configureAttachmentsCols.tsx b/frontend/src/components/Attachments/configureAttachmentsCols.tsx index 2ee0fce..cf40ee3 100644 --- a/frontend/src/components/Attachments/configureAttachmentsCols.tsx +++ b/frontend/src/components/Attachments/configureAttachmentsCols.tsx @@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver"; import dataFormatter from '../../helpers/dataFormatter' import DataGridMultiSelect from "../DataGridMultiSelect"; import ListActionsPopover from '../ListActionsPopover'; +import { makeReadableColumns } from '../../helpers/dataGridColumns'; import {hasPermission} from "../../helpers/userPermissions"; @@ -39,7 +40,7 @@ export const loadColumns = async ( const hasUpdatePermission = hasPermission(user, 'UPDATE_ATTACHMENTS') - return [ + const columns = [ { field: 'message', @@ -225,4 +226,6 @@ export const loadColumns = async ( }, }, ]; + + return makeReadableColumns(columns); }; diff --git a/frontend/src/components/Conversations/TableConversations.tsx b/frontend/src/components/Conversations/TableConversations.tsx index b09c422..8ede122 100644 --- a/frontend/src/components/Conversations/TableConversations.tsx +++ b/frontend/src/components/Conversations/TableConversations.tsx @@ -16,6 +16,7 @@ import {loadColumns} from "./configureConversationsCols"; import _ from 'lodash'; import dataFormatter from '../../helpers/dataFormatter' import {dataGridStyles} from "../../styles"; +import DataGridLoadingOverlay from '../DataGridLoadingOverlay'; @@ -45,6 +46,7 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr const focusRing = useAppSelector((state) => state.style.focusRingColor); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); const corners = useAppSelector((state) => state.style.corners); + const isDataGridLoading = !currentUser || loading || columns.length === 0; const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); for (let i = 0; i < numPages; i++) { pagesList.push(i); @@ -211,7 +213,8 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
`datagrid--row`} @@ -251,7 +254,10 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr rowCount={count} pageSizeOptions={[10]} paginationMode={'server'} - loading={loading} + loading={isDataGridLoading} + slots={{ + loadingOverlay: DataGridLoadingOverlay, + }} onPaginationModelChange={(params) => { onPageChange(params.page); }} diff --git a/frontend/src/components/Conversations/configureConversationsCols.tsx b/frontend/src/components/Conversations/configureConversationsCols.tsx index 5b69c6a..3b77e84 100644 --- a/frontend/src/components/Conversations/configureConversationsCols.tsx +++ b/frontend/src/components/Conversations/configureConversationsCols.tsx @@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver"; import dataFormatter from '../../helpers/dataFormatter' import DataGridMultiSelect from "../DataGridMultiSelect"; import ListActionsPopover from '../ListActionsPopover'; +import { makeReadableColumns } from '../../helpers/dataGridColumns'; import {hasPermission} from "../../helpers/userPermissions"; @@ -39,7 +40,7 @@ export const loadColumns = async ( const hasUpdatePermission = hasPermission(user, 'UPDATE_CONVERSATIONS') - return [ + const columns = [ { field: 'user', @@ -203,4 +204,6 @@ export const loadColumns = async ( }, }, ]; + + return makeReadableColumns(columns); }; diff --git a/frontend/src/components/DataGridLoadingOverlay.tsx b/frontend/src/components/DataGridLoadingOverlay.tsx new file mode 100644 index 0000000..c0408f5 --- /dev/null +++ b/frontend/src/components/DataGridLoadingOverlay.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +const SKELETON_ROWS = 4; + +export default function DataGridLoadingOverlay() { + return ( +
+
+
+
+
+

Loading data

+

Fetching the latest rows for this table.

+
+
+
+ {Array.from({ length: SKELETON_ROWS }).map((_, index) => ( +
+
+
+
+
+
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/components/FormImagePicker.tsx b/frontend/src/components/FormImagePicker.tsx index 7bc34ae..e87c587 100644 --- a/frontend/src/components/FormImagePicker.tsx +++ b/frontend/src/components/FormImagePicker.tsx @@ -12,13 +12,25 @@ type Props = { accept?: string; color: ColorButtonKey; isRoundIcon?: boolean; + showFilename?: boolean; path: string; schema: object; field: any, form: any, }; -const FormImagePicker = ({ label, icon, accept, color, isRoundIcon, path, schema, form, field }: Props) => { +const FormImagePicker = ({ + label, + icon, + accept, + color, + isRoundIcon, + showFilename = false, + path, + schema, + form, + field, +}: Props) => { const [file, setFile] = useState(null); const [loading, setLoading] = useState(false); const corners = useAppSelector((state) => state.style.corners); @@ -51,14 +63,14 @@ const FormImagePicker = ({ label, icon, accept, color, isRoundIcon, path, schema setLoading(false); }; - const showFilename = !isRoundIcon && file; + const shouldShowFilename = Boolean(showFilename && !isRoundIcon && file); return (
- {showFilename && !loading && ( + {shouldShowFilename && !loading && (
{file.name} diff --git a/frontend/src/components/Messages/TableMessages.tsx b/frontend/src/components/Messages/TableMessages.tsx index 998ad9c..ce31e68 100644 --- a/frontend/src/components/Messages/TableMessages.tsx +++ b/frontend/src/components/Messages/TableMessages.tsx @@ -16,6 +16,7 @@ import {loadColumns} from "./configureMessagesCols"; import _ from 'lodash'; import dataFormatter from '../../helpers/dataFormatter' import {dataGridStyles} from "../../styles"; +import DataGridLoadingOverlay from '../DataGridLoadingOverlay'; @@ -45,6 +46,7 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid }) const focusRing = useAppSelector((state) => state.style.focusRingColor); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); const corners = useAppSelector((state) => state.style.corners); + const isDataGridLoading = !currentUser || loading || columns.length === 0; const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); for (let i = 0; i < numPages; i++) { pagesList.push(i); @@ -211,7 +213,8 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
`datagrid--row`} @@ -251,7 +254,10 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid }) rowCount={count} pageSizeOptions={[10]} paginationMode={'server'} - loading={loading} + loading={isDataGridLoading} + slots={{ + loadingOverlay: DataGridLoadingOverlay, + }} onPaginationModelChange={(params) => { onPageChange(params.page); }} diff --git a/frontend/src/components/Messages/configureMessagesCols.tsx b/frontend/src/components/Messages/configureMessagesCols.tsx index fd9cb87..339c8c0 100644 --- a/frontend/src/components/Messages/configureMessagesCols.tsx +++ b/frontend/src/components/Messages/configureMessagesCols.tsx @@ -12,6 +12,7 @@ import {saveFile} from "../../helpers/fileSaver"; import dataFormatter from '../../helpers/dataFormatter' import DataGridMultiSelect from "../DataGridMultiSelect"; import ListActionsPopover from '../ListActionsPopover'; +import { makeReadableColumns } from '../../helpers/dataGridColumns'; import {hasPermission} from "../../helpers/userPermissions"; @@ -39,7 +40,7 @@ export const loadColumns = async ( const hasUpdatePermission = hasPermission(user, 'UPDATE_MESSAGES') - return [ + const columns = [ { field: 'conversation', @@ -266,4 +267,6 @@ export const loadColumns = async ( }, }, ]; + + return makeReadableColumns(columns); }; diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index c8b5242..79a7631 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState, useEffect } from 'react' +import React, { ReactNode, useState, useEffect, CSSProperties } from 'react' import { mdiClose, mdiDotsVertical } from '@mdi/js' import { containerMaxW } from '../config' import BaseIcon from './BaseIcon' @@ -12,9 +12,10 @@ type Props = { className: string contentClassName?: string children: ReactNode + style?: CSSProperties } -export default function NavBar({ menu, className = '', contentClassName = containerMaxW, children }: Props) { +export default function NavBar({ menu, className = '', contentClassName = containerMaxW, children, style }: Props) { const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false) const [isScrolled, setIsScrolled] = useState(false); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); @@ -37,7 +38,8 @@ export default function NavBar({ menu, className = '', contentClassName = contai return (