From aeda4e4cb9f548116a8c1af9b2ad7b839e0606ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CVeLiTi=E2=80=9D?= <“info@veliti.nl”> Date: Wed, 21 Jan 2026 12:48:46 +0100 Subject: [PATCH] Refactor user permissions handling and enhance menu functionality with collapsible headers --- .DS_Store | Bin 12292 -> 12292 bytes api/v0/get/user_credentials.php | 2 +- api/v1/get/user_credentials.php | 2 +- api/v2/get/history.php | 11 ++ api/v2/get/software_update.php | 1 - api/v2/post/access_elements.php | 4 +- assets/admin.js | 60 ++++++++++ assets/functions.php | 190 +++++++++++++++++++++++--------- assets/softwaretool.js | 6 +- equipment.php | 2 +- index.php | 93 ++++++++++++++-- settings/settingsmenu.php | 8 +- softwaretool.php | 3 +- style/admin.css | 47 ++++++-- 14 files changed, 349 insertions(+), 80 deletions(-) diff --git a/.DS_Store b/.DS_Store index 9d4c452e8d05565dc11a62679d1101247abf067b..555e5f6e77a6d9d56ae7c3a0490ae0559409bbfa 100644 GIT binary patch delta 79 zcmZokXi1ph&uFzVU^hRb_GTUdW+o{PhBSsuh9ZVshH{2PAf1|)Qkfetch(); //Define User data $partnerhierarchy = $user_data['partnerhierarchy']; $permission = userRights($user_data['view']); -$profile= getProfile($user_data['settings'],$permission); +$profile= getUserPermissions($pdo, $user_data['id']); $username = $user_data['username']; $useremail = $user_data['email']; $servicekey = $user_data['service']; diff --git a/api/v1/get/user_credentials.php b/api/v1/get/user_credentials.php index 3f98774..f7fa705 100644 --- a/api/v1/get/user_credentials.php +++ b/api/v1/get/user_credentials.php @@ -17,7 +17,7 @@ if ($stmt->rowCount() == 1) { //Define User data $partnerhierarchy = $user_data['partnerhierarchy']; $permission = userRights($user_data['view']); - $profile= getProfile($user_data['settings'],$permission); + $profile= getUserPermissions($pdo, $user_data['id']); $username = $user_data['username']; $useremail = $user_data['email']; $servicekey = $user_data['service']; diff --git a/api/v2/get/history.php b/api/v2/get/history.php index e433d1d..9994a48 100644 --- a/api/v2/get/history.php +++ b/api/v2/get/history.php @@ -34,6 +34,7 @@ switch ($permission) { //NEW ARRAY $criterias = []; $clause = ''; +$type_check = false; //Check for $_GET variables and build up clause if(isset($get_content) && $get_content!=''){ @@ -117,6 +118,7 @@ if(isset($get_content) && $get_content!=''){ //add new_querystring to clause $clause .= ' AND h.type IN ('.$new_querystring.')'; //remove original key/value from array + $type_check = true; unset($criterias[$v[0]]); } else { @@ -142,6 +144,9 @@ if(isset($criterias['totals']) && $criterias['totals'] ==''){ //Request for total rows $sql ='SELECT count(h.rowID) as historyID FROM equipment_history h LEFT JOIN equipment e ON h.equipmentid = e.rowID '.$whereclause.''; } +elseif($type_check){ + $sql ='SELECT h.rowID as historyID, e.rowID as equipmentID, e.serialnumber, h.type, h.description, h.created, h.createdby FROM equipment_history h LEFT JOIN equipment e ON h.equipmentid = e.rowID '.$whereclause.' ORDER BY h.created DESC'; +} else { //request history $sql ='SELECT h.rowID as historyID, e.rowID as equipmentID, e.serialnumber, h.type, h.description, h.created, h.createdby FROM equipment_history h LEFT JOIN equipment e ON h.equipmentid = e.rowID '.$whereclause.' ORDER BY h.created DESC LIMIT :page,:num_products'; @@ -178,6 +183,12 @@ if(isset($criterias['totals']) && $criterias['totals']==''){ $messages = $stmt->fetch(); $messages = $messages[0]; } +elseif($type_check){ + //Excute Query + $stmt->execute(); + //Get results + $messages = $stmt->fetchAll(PDO::FETCH_ASSOC); +} else { $current_page = isset($criterias['p']) && is_numeric($criterias['p']) ? (int)$criterias['p'] : 1; $stmt->bindValue('page', ($current_page - 1) * $page_rows_history, PDO::PARAM_INT); diff --git a/api/v2/get/software_update.php b/api/v2/get/software_update.php index 883e6a9..98f7cb9 100644 --- a/api/v2/get/software_update.php +++ b/api/v2/get/software_update.php @@ -239,7 +239,6 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ JOIN products_software_versions psv ON psa.software_version_id = psv.rowID WHERE psa.product_id = ? AND psa.status = 1 - AND psv.latest = 1 AND (psv.hw_version = ? OR psv.hw_version IS NULL OR psv.hw_version = "")'; $stmt = $pdo->prepare($sql); diff --git a/api/v2/post/access_elements.php b/api/v2/post/access_elements.php index 9b06726..e2d768d 100644 --- a/api/v2/post/access_elements.php +++ b/api/v2/post/access_elements.php @@ -25,12 +25,12 @@ $criterias = []; //ADD STANDARD PARAMETERS TO ARRAY BASED ON INSERT OR UPDATE if ($command == 'update'){ - $post_content['updatedby'] = $username;; + $post_content['updatedby'] = $username; $post_content['updated'] = $date; } elseif ($command == 'insert'){ $post_content['created'] = $date; - $post_content['createdby'] = $username;; + $post_content['createdby'] = $username; } //CREAT NEW ARRAY AND MAP TO CLAUSE diff --git a/assets/admin.js b/assets/admin.js index 27f2927..31e7cde 100644 --- a/assets/admin.js +++ b/assets/admin.js @@ -20,6 +20,66 @@ document.querySelector('.responsive-toggle').onclick = event => { localStorage.setItem('admin_menu', 'closed'); } }; + +// Menu header collapse/expand functionality +document.querySelectorAll('aside .menu-header').forEach(header => { + header.addEventListener('click', function(event) { + event.preventDefault(); + + // Toggle expanded state + this.classList.toggle('expanded'); + + // Find the next sibling .sub element and toggle display + const submenu = this.nextElementSibling; + if (submenu && submenu.classList.contains('sub')) { + submenu.classList.toggle('expanded'); + // Update inline style for display + submenu.style.display = submenu.classList.contains('expanded') ? 'flex' : 'none'; + } + + // Rotate chevron + const chevron = this.querySelector('.menu-chevron'); + if (chevron) { + chevron.style.transform = this.classList.contains('expanded') ? 'rotate(180deg)' : 'rotate(0deg)'; + } + + // Store expanded state in localStorage for persistence + const section = this.dataset.section; + if (section) { + const expandedSections = JSON.parse(localStorage.getItem('menu_expanded') || '{}'); + expandedSections[section] = this.classList.contains('expanded'); + localStorage.setItem('menu_expanded', JSON.stringify(expandedSections)); + } + }); +}); + +// Restore menu expanded states from localStorage on page load +(function restoreMenuState() { + const expandedSections = JSON.parse(localStorage.getItem('menu_expanded') || '{}'); + + document.querySelectorAll('aside .menu-header').forEach(header => { + const section = header.dataset.section; + const submenu = header.nextElementSibling; + const chevron = header.querySelector('.menu-chevron'); + + // If explicitly saved as expanded, apply it + if (section && expandedSections[section] === true) { + header.classList.add('expanded'); + if (submenu && submenu.classList.contains('sub')) { + submenu.classList.add('expanded'); + submenu.style.display = 'flex'; + } + if (chevron) chevron.style.transform = 'rotate(180deg)'; + } + // If has selected child, always expand (override localStorage) + if (submenu && submenu.querySelector('a.selected')) { + header.classList.add('expanded'); + submenu.classList.add('expanded'); + submenu.style.display = 'flex'; + if (chevron) chevron.style.transform = 'rotate(180deg)'; + } + }); +})(); document.querySelectorAll('.tabs a').forEach((element, index) => { element.onclick = event => { event.preventDefault(); diff --git a/assets/functions.php b/assets/functions.php index 25a3eb0..e4592d3 100644 --- a/assets/functions.php +++ b/assets/functions.php @@ -233,20 +233,19 @@ function routes($urls) { //------------------------------------------ // Menu Builder //------------------------------------------ +/** + * @deprecated Use filterMenuByPermissions() instead + * Filter menu items based on profile string (legacy) + */ function filterMenuByProfile($menu, $profileString) { - // Convert profile string to array $profileArray = explode(',', $profileString); - - // Initialize result array $filteredMenu = []; - - // Loop through main menu sections + foreach ($menu as $sectionKey => $section) { $sectionIncluded = in_array($sectionKey, $profileArray); $submenuFound = false; $firstSubmenuItem = null; - - // First check if any submenu items are in profile + foreach ($section as $itemKey => $item) { if ($itemKey !== 'main_menu' && in_array($itemKey, $profileArray)) { $submenuFound = true; @@ -255,24 +254,19 @@ function filterMenuByProfile($menu, $profileString) { } } } - - // Include this section if either section key or any submenu is in profile + if ($sectionIncluded || $submenuFound) { $filteredMenu[$sectionKey] = []; - - // Add main_menu - if section not in profile but submenu found, use first submenu as main_menu + if (!$sectionIncluded && $submenuFound && $firstSubmenuItem !== null) { - // Create hybrid main_menu - keep name and icon from original, but use URL and selected from submenu $hybridMainMenu = $section['main_menu']; $hybridMainMenu['url'] = $firstSubmenuItem['url']; $hybridMainMenu['selected'] = $firstSubmenuItem['selected']; - $filteredMenu[$sectionKey]['main_menu'] = $hybridMainMenu; } else { $filteredMenu[$sectionKey]['main_menu'] = $section['main_menu']; } - - // Add allowed submenu items + foreach ($section as $itemKey => $item) { if ($itemKey !== 'main_menu' && in_array($itemKey, $profileArray)) { $filteredMenu[$sectionKey][$itemKey] = $item; @@ -280,17 +274,83 @@ function filterMenuByProfile($menu, $profileString) { } } } - + return $filteredMenu; } -function menu($selected,$selected_child){ + +/** + * Filter menu items based on user permissions array + * + * @param array $menu The full menu structure from settingsmenu.php + * @param array $permissions The permissions array from $_SESSION['authorization']['permissions'] + * @return array Filtered menu with only items user has can_read permission for + */ +function filterMenuByPermissions($menu, $permissions) { + $filteredMenu = []; + + foreach ($menu as $sectionKey => $section) { + // Get the main_menu's 'selected' path to check permission + $mainMenuPath = $section['main_menu']['selected'] ?? $sectionKey; + + // Check if user has read permission for main menu + $mainMenuAllowed = isset($permissions[$mainMenuPath]) && + $permissions[$mainMenuPath]['can_read'] == 1; + + $allowedSubmenus = []; + $firstAllowedSubmenu = null; + + // Check each submenu item for permission + foreach ($section as $itemKey => $item) { + if ($itemKey === 'main_menu') { + continue; + } + + // Get the submenu item's 'selected' path + $submenuPath = $item['selected'] ?? $itemKey; + + // Check if user has read permission for this submenu item + if (isset($permissions[$submenuPath]) && + $permissions[$submenuPath]['can_read'] == 1) { + $allowedSubmenus[$itemKey] = $item; + if ($firstAllowedSubmenu === null) { + $firstAllowedSubmenu = $item; + } + } + } + + // Include section if main menu is allowed OR any submenu is allowed + if ($mainMenuAllowed || count($allowedSubmenus) > 0) { + $filteredMenu[$sectionKey] = []; + + // Handle main_menu entry + if (!$mainMenuAllowed && $firstAllowedSubmenu !== null) { + // User doesn't have main access but has submenu access + // Create hybrid: keep name/icon from main, use URL/selected from first submenu + $hybridMainMenu = $section['main_menu']; + $hybridMainMenu['url'] = $firstAllowedSubmenu['url']; + $hybridMainMenu['selected'] = $firstAllowedSubmenu['selected']; + $filteredMenu[$sectionKey]['main_menu'] = $hybridMainMenu; + } else { + $filteredMenu[$sectionKey]['main_menu'] = $section['main_menu']; + } + + // Add allowed submenu items + foreach ($allowedSubmenus as $itemKey => $item) { + $filteredMenu[$sectionKey][$itemKey] = $item; + } + } + } + + return $filteredMenu; +} +function menu($selected, $selected_child){ include dirname(__FILE__,2).'/settings/settings_redirector.php'; if(isset($_SESSION['country_code'])){ $api_file_language = dirname(__FILE__,2).'/settings/translations/translations_'.strtoupper($_SESSION['country_code']).'.php'; - if (file_exists($api_file_language)){ - include $api_file_language; //Include the code + if (file_exists($api_file_language)){ + include $api_file_language; } else { include dirname(__FILE__,2).'/settings/translations/translations_US.php'; @@ -298,31 +358,70 @@ function menu($selected,$selected_child){ } else { include dirname(__FILE__,2).'/settings/translations/translations_US.php'; - } - - //Define Menu + } + $menu = ''; - //filter the main_menu array based on profile - $filteredMenu = filterMenuByProfile($main_menu, $_SESSION['authorization']['permissions']); + // Use permissions array if available, fallback to legacy profile string + if (isset($_SESSION['authorization']['permissions']) && !empty($_SESSION['authorization']['permissions'])) { + $filteredMenu = filterMenuByPermissions($main_menu, $_SESSION['authorization']['permissions']); + } else { + $filteredMenu = filterMenuByProfile($main_menu, $_SESSION['authorization']['profile']); + } - foreach ($filteredMenu as $menu_item){ - //Main Item - $menu .= ''.ucfirst((${$menu_item['main_menu']['name']} ?? 'not specified')).''; - - if (count($menu_item) > 1){ - //SUBMENU - $menu .= '
'; + foreach ($filteredMenu as $menu_item) { + $submenuCount = count($menu_item) - 1; // Exclude main_menu + $mainMenu = $menu_item['main_menu']; + $menuName = ucfirst((${$mainMenu['name']} ?? ucfirst(str_replace('menu_', '', $mainMenu['name'])))); + $isMainSelected = ($selected == $mainMenu['selected']); - foreach ($menu_item as $key => $item){ - //filter out main_menu - if($key !='main_menu'){ - $menu .= ''.ucfirst((${$item['name']}?? 'not specified')).''; + // Check if any child is selected (for expanded state) + $hasSelectedChild = false; + foreach ($menu_item as $key => $item) { + if ($key !== 'main_menu' && $selected == $item['selected']) { + $hasSelectedChild = true; + break; } - } - $menu .= '
'; + } + + if ($submenuCount > 0) { + // HAS SUBMENUS: Render as collapsible header (not a link) + $expandedClass = ($isMainSelected || $hasSelectedChild) ? ' expanded' : ''; + $selectedClass = $isMainSelected ? ' selected' : ''; + + $menu .= ''; + + // SUBMENU container + $subExpandedClass = ($isMainSelected || $hasSelectedChild) ? ' expanded' : ''; + $subDisplayStyle = ($isMainSelected || $hasSelectedChild) ? 'display:flex;' : 'display:none;'; + $menu .= '
'; + + foreach ($menu_item as $key => $item) { + if ($key !== 'main_menu') { + $itemName = ucfirst((${$item['name']} ?? ucfirst(str_replace('menu_', '', $item['name'])))); + $itemSelectedClass = ($selected == $item['selected']) ? ' class="selected"' : ''; + $menu .= ''; + $menu .= ''; + $menu .= '' . $itemName . ''; + $menu .= ''; + } + } + $menu .= '
'; + } else { + // NO SUBMENUS: Render as direct link + $selectedClass = $isMainSelected ? ' class="selected"' : ''; + $menu .= ''; + $menu .= ''; + $menu .= '' . $menuName . ''; + $menu .= ''; + $menu .= ''; } } + return $menu; } @@ -419,19 +518,6 @@ echo << { - form.addEventListener('submit', function(e) { - // Show loading screen before form submission - showLoading(); - }); - }); - } // Intercept all network requests (fetch and XMLHttpRequest) function interceptNetworkRequests() { @@ -1572,8 +1658,8 @@ function getProfile($profile, $permission){ error_log($test, 3, $filelocation); } - // 1. Check if basic_permission_level is 5 (System) - always allow - if ($basic_permission_level !== null && $basic_permission_level == 5) { + // 1. Check if basic_permission_level is 4 (System-admin+) - always allow + if ($basic_permission_level !== null && $basic_permission_level == 4) { if(debug){ $test = "$date - Allowed by system permission (level 5)".PHP_EOL; error_log($test, 3, $filelocation); diff --git a/assets/softwaretool.js b/assets/softwaretool.js index 512c57d..0bc22c8 100644 --- a/assets/softwaretool.js +++ b/assets/softwaretool.js @@ -179,7 +179,7 @@ async function connectDeviceForSoftware() { progressBar("1", "", ""); // Check if DEBUG mode is enabled - use mock device data - if (typeof DEBUG !== 'undefined' && DEBUG) { + if (typeof DEBUG !== 'undefined' && DEBUG && typeof DEBUG_ID !== 'undefined' && DEBUG_ID) { // TEST MODE: Use mock device data deviceSerialNumber = "22110095"; deviceVersion = "03e615af"; @@ -549,7 +549,7 @@ async function fetchSoftwareOptions() { progressBar("100", "Software options loaded", "#04AA6D"); // Show user info modal immediately (skip in debug mode) - if (typeof DEBUG === 'undefined' || !DEBUG) { + if (typeof DEBUG === 'undefined' || !DEBUG || typeof DEBUG_ID === 'undefined' || !DEBUG_ID) { showUserInfoModal(); } else { // In debug mode, reveal software options immediately @@ -1619,7 +1619,7 @@ async function downloadAndInstallSoftware(option, customerData = null) { window.upgraded_version = option.version || ""; // DEBUG MODE: Don't auto-trigger upload, let user manually test - if (typeof DEBUG !== 'undefined' && DEBUG) { + if (typeof DEBUG !== 'undefined' && DEBUG && typeof DEBUG_ID !== 'undefined' && DEBUG_ID) { // Show upload section and button for manual testing document.getElementById("uploadSection").style.display = "block"; const uploadBtn = document.getElementById("uploadSoftware"); diff --git a/equipment.php b/equipment.php index 67e0d1e..93a9a08 100644 --- a/equipment.php +++ b/equipment.php @@ -476,7 +476,7 @@ $view .= '
if ($update_allowed === 1){ - $view .=''.$button_firmware.''; + $view .=''.$button_firmware.''; } $view .='
'; diff --git a/index.php b/index.php index d14d81c..4b787c6 100644 --- a/index.php +++ b/index.php @@ -108,8 +108,8 @@ if (isset($_GET['page']) && $_GET['page'] == 'logout') { //===================================== //DEFINE WHERE TO SEND THE USER TO. GET first assigned view in the profile if not available use dashboard -//===================================== -$allowed_views = explode(',',$_SESSION['authorization']['permissions']); +/*===================================== +$allowed_views = explode(',',$_SESSION['authorization']['profile']); $ignoreViews = ['profile','assets','sales']; // If dashboard is in the profile, prioritize it @@ -118,16 +118,93 @@ if (in_array('dashboard', $allowed_views) && file_exists('dashboard.php')) { } else { $allowed_views = findExistingView($allowed_views, 'dashboard', $ignoreViews); } +*/ +//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// SIMPLE ROUTING SYSTEM +//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +$page = $_GET['page'] ?? 'dashboard'; -//===================================== -//FORWARD THE USER TO THE CORRECT PAGE -//===================================== -$page = isset($_GET['page']) && file_exists($_GET['page'] . '.php') ? $_GET['page'] : $allowed_views; +// Sanitize page parameter to prevent directory traversal +$page = preg_replace('/[^a-zA-Z0-9_-]/', '', $page); +$page_file = $page . '.php'; // Output error variable $error = ''; -// Include the requested page -include $page . '.php'; + +try { + $file_exists = file_exists($page_file); + $is_allowed = $file_exists ? isAllowed($page, $_SESSION['authorization']['permissions'], $_SESSION['authorization']['permission'], 'R') : 0; + + if (debug) { + debuglog("Routing: page={$page}, file_exists={$file_exists}, is_allowed={$is_allowed}"); + } + + if ($file_exists && $is_allowed !== 0) { + include $page_file; + } else { + // Show error page for missing files or unauthorized access + $page_exists = file_exists($page_file); + $error_title = $page_exists ? 'Access Denied' : 'Page Not Found'; + $error_message = $page_exists + ? 'You do not have permission to access this page.' + : 'The requested page "' . htmlspecialchars($page) . '" could not be found.'; + $error_icon = $page_exists ? 'fa-solid fa-lock' : 'fa-solid fa-file-circle-xmark'; + + template_header($error_title, ''); + echo ' +
+
+ +
+

' . $error_title . '

+

' . $error_message . '

+
+
+
+
+
+ +
+

Please check the URL or navigate using the menu.

+ + Return to Dashboard + +
'; + template_footer(); + } +} catch (Exception $e) { + // Handle any errors during page inclusion + if (debug) { + debuglog("Error loading page {$page}: " . $e->getMessage()); + } + + template_header('System Error', ''); + echo ' +
+
+ +
+

System Error

+

An error occurred while loading the page.

+
+
+
+
+
+ +
+

Please try again or contact the system administrator.

+
+ + Return to Dashboard + + +
+
'; + template_footer(); +} //===================================== //debuglog diff --git a/settings/settingsmenu.php b/settings/settingsmenu.php index 36557b9..4f0ee83 100644 --- a/settings/settingsmenu.php +++ b/settings/settingsmenu.php @@ -101,7 +101,7 @@ $main_menu = [ ], "equipments" =>[ "url" => "equipments", - "selected" => "assets", + "selected" => "equipments", "icon" => "fa-solid fa-database", "name" => "menu_assets" ], @@ -181,6 +181,12 @@ $main_menu = [ "icon" => "fa-solid fa-magnifying-glass-chart", "name" => "menu_report_main" ], + "report_builder" => [ + "url" => "report_builder", + "selected" => "report_builder", + "icon" => "fa-solid fa-magnifying-glass-chart", + "name" => "menu_report_main" + ], "report_build" => [ "url" => "report_build", "selected" => "report_build", diff --git a/softwaretool.php b/softwaretool.php index b04df46..b3db692 100644 --- a/softwaretool.php +++ b/softwaretool.php @@ -201,7 +201,7 @@ if (isset($_GET['equipmentID'])){$returnpage = 'equipment&equipmentID='.$_GET['e //SHOW BACK BUTTON ONLY FOR PORTAL USERS -if (isAllowed('dashboard',$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'R') != 0){ +if (isAllowed($page ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'R') != 0){ $view .= '
@@ -313,6 +313,7 @@ echo '