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 (