From f7a91737bcfa280b5a7ab8db022101dce6467e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CVeLiTi=E2=80=9D?= <“info@veliti.nl”> Date: Tue, 27 Jan 2026 15:10:21 +0100 Subject: [PATCH] Implement RBAC migration and role management enhancements - Added AJAX functionality to fetch role permissions for copying. - Introduced system role management with permission checks for updates. - Implemented role deletion with confirmation modal and backend handling. - Enhanced user role assignment migration scripts to transition from legacy profiles to RBAC. - Created SQL migration scripts for user roles and permissions mapping. - Updated user interface to support new role management features including copy permissions and system role indicators. --- .DS_Store | Bin 12292 -> 12292 bytes api.php | 2 +- api/v2/get/marketing_files.php | 3 - api/v2/get/marketing_folders.php | 7 - api/v2/get/marketing_tags.php | 3 - api/v2/get/service.php | 43 ++- api/v2/get/user_roles.php | 18 +- api/v2/post/history.php | 5 +- api/v2/post/role_access_permissions.php | 4 +- assets/.DS_Store | Bin 6148 -> 6148 bytes .../database/migration_profiles_to_rbac.sql | 222 ++++++++++++++ assets/database/migration_users_to_rbac.sql | 228 +++++++++++++++ assets/functions.php | 94 +++--- assets/marketing.js | 146 +++++++-- assets/softwaretool.js | 52 +++- equipment.php | 12 +- factuur.php | 6 +- index.php | 43 ++- login.php | 19 +- order.php | 4 +- partners.php | 1 + settings/systemservicetool_init.php | 10 +- softwaretool.php | 4 - style/admin.css | 57 ++++ style/marketing.css | 88 ++++++ user.php | 276 ++++++++++++------ user_role.php | 163 ++++++++++- users.php | 1 + webhook_mollie.php | 5 +- webhook_paypal.php | 5 +- 30 files changed, 1285 insertions(+), 236 deletions(-) create mode 100644 assets/database/migration_profiles_to_rbac.sql create mode 100644 assets/database/migration_users_to_rbac.sql diff --git a/.DS_Store b/.DS_Store index 555e5f6e77a6d9d56ae7c3a0490ae0559409bbfa..8f764648eda816b2498ef5584f36f7e42e03a7c9 100644 GIT binary patch delta 147 zcmZokXi1ph&uFtT;4m9!T1s(pQht68<78&`7d#9M42(bw1RUI(A4-U`Y*y!a!px|% znMXi|iIHKmrqEhO8BT^=hD3%UhHQpZh7yKMhCGIJxG9@23rq4a@^9u*U}r{{y_s2g eI`ibcN(P&ch$%B}X4Cl0viX6^E=H)3A|n7uSSgnP delta 109 zcmZokXi1ph&uFzV;4s@{9p#Ta3=9m6Knw&N+?yFC#aTA1^E_c@)ZWY^Aj8DSxLH$Z zE#u~|!eTrV8{`@JHuEU3GjG;WZfBm%uc$RyMO<+67cm3I&8!+soldto) || $partner->soldto == ''){$soldto_search = '%';} el //default whereclause $whereclause = ''; -// For testing, disable account hierarchy filtering -// list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'get'); - //NEW ARRAY $criterias = []; $clause = ''; diff --git a/api/v2/get/marketing_folders.php b/api/v2/get/marketing_folders.php index 92f0853..fed5660 100644 --- a/api/v2/get/marketing_folders.php +++ b/api/v2/get/marketing_folders.php @@ -31,8 +31,6 @@ if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} el //default whereclause $whereclause = ''; -list($whereclause,$condition) = getWhereclauselvl2('',$permission,$partner,'get'); - //NEW ARRAY $criterias = []; $clause = ''; @@ -110,11 +108,6 @@ else { $stmt = $pdo->prepare($sql); -//Bind to query -if (str_contains($whereclause, ':condition')){ - $stmt->bindValue('condition', $condition, PDO::PARAM_STR); -} - if (!empty($criterias)){ foreach ($criterias as $key => $value){ $key_condition = ':'.$key; diff --git a/api/v2/get/marketing_tags.php b/api/v2/get/marketing_tags.php index 8bc7431..fe1d003 100644 --- a/api/v2/get/marketing_tags.php +++ b/api/v2/get/marketing_tags.php @@ -14,9 +14,6 @@ if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} el //default whereclause $whereclause = ''; -// Tags are global, so no account hierarchy filtering -// list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'get'); - //NEW ARRAY $criterias = []; $clause = ''; diff --git a/api/v2/get/service.php b/api/v2/get/service.php index 9fe1e98..3f71c20 100644 --- a/api/v2/get/service.php +++ b/api/v2/get/service.php @@ -1,13 +1,17 @@ prepare($sql); + $stmt->execute(); + //Get results + $messages = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode($messages); + +} +elseif ($action == 'equipments' && (isset($_GET['serialnumber']) && $_GET['serialnumber'] != '' && !isset($_GET['validate']))) { + + $sql = "SELECT e.rowID as equipmentID, e.*, p.productcode, p.productname, p.product_media, psl.starts_at,psl.expires_at,psl.status as license_status from equipment e LEFT JOIN products p ON e.productrowid = p.rowID LEFT JOIN products_software_licenses psl ON e.sw_version_license = psl.license_key WHERE e.serialnumber = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$_GET['serialnumber']]); + //Get results + $messages = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode($messages); + +} +elseif ($action == 'equipments' && (isset($_GET['serialnumber']) && $_GET['serialnumber'] != '' && isset($_GET['validate']))){ + + $sql = "SELECT count(rowID) as rowID from equipment e WHERE e.serialnumber = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$_GET['serialnumber']]); + $messages = $stmt->fetch(); + + if ($messages[0] == 1) { + echo json_encode(array('SN'=> TRUE)); + } + else { + echo json_encode(array('SN'=> FALSE)); + } + } else { http_response_code(400); diff --git a/api/v2/get/user_roles.php b/api/v2/get/user_roles.php index 7827803..cb07b3f 100644 --- a/api/v2/get/user_roles.php +++ b/api/v2/get/user_roles.php @@ -24,7 +24,7 @@ if(isset($get_content) && $get_content!=''){ $v = explode("=", $y); //INCLUDE VARIABLES IN ARRAY $criterias[$v[0]] = $v[1]; - if ($v[0] == 'page' || $v[0] =='p' || $v[0] =='totals' || $v[0] =='success_msg' || $v[0] =='sort'){ + if ($v[0] == 'page' || $v[0] =='p' || $v[0] =='totals' || $v[0] =='success_msg' || $v[0] =='sort' || $v[0] =='all'){ //do nothing } elseif ($v[0] == 'rowid') { @@ -50,6 +50,11 @@ if(isset($get_content) && $get_content!=''){ } } +//Filter system roles for users without delete permission on user_roles +if (isAllowed('user_roles', $profile, $permission, 'D') !== 1) { + $clause .= ' AND r.is_system != 1'; +} + //Build WHERE clause $whereclause = ''; if ($clause != ''){ @@ -81,6 +86,12 @@ if (isset($criterias['totals']) && $criterias['totals'] ==''){ //Request for total rows $sql = 'SELECT count(*) as count FROM user_roles r '.$whereclause; } +elseif (isset($criterias['all']) && $criterias['all'] ==''){ + //Return all records (no paging) + $sql = 'SELECT r.*, + (SELECT COUNT(*) FROM role_access_permissions WHERE role_id = r.rowID) as permission_count + FROM user_roles r '.$whereclause.' ORDER BY '.$sort; +} else { //SQL with permission count $sql = 'SELECT r.*, @@ -129,6 +140,11 @@ if(isset($criterias['totals']) && $criterias['totals']==''){ $messages = $stmt->fetch(); $messages = $messages[0]; } +elseif(isset($criterias['all']) && $criterias['all']==''){ + //Return all records (no paging) + $stmt->execute(); + $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, PDO::PARAM_INT); diff --git a/api/v2/post/history.php b/api/v2/post/history.php index 0861871..51ed350 100644 --- a/api/v2/post/history.php +++ b/api/v2/post/history.php @@ -389,8 +389,8 @@ if (isset($post_content['sn']) && (isset($post_content['payload']) || isset($pos // Create license $sql = 'INSERT INTO products_software_licenses - (version_id, license_type, license_key, status, starts_at, expires_at, transaction_id, created, createdby) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'; + (version_id, license_type, license_key, status, starts_at, expires_at, transaction_id, accounthierarchy,created, createdby) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; $stmt = $pdo->prepare($sql); $stmt->execute([ $sw_version_consent, @@ -400,6 +400,7 @@ if (isset($post_content['sn']) && (isset($post_content['payload']) || isset($pos date('Y-m-d H:i:s'), '2099-12-31 23:59:59', // effectively permanent 'Customer_consent', + $account, date('Y-m-d H:i:s'), $user ]); diff --git a/api/v2/post/role_access_permissions.php b/api/v2/post/role_access_permissions.php index 3d0951d..c064dc2 100644 --- a/api/v2/post/role_access_permissions.php +++ b/api/v2/post/role_access_permissions.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/.DS_Store b/assets/.DS_Store index 1ba4cf5a23caa3cca1ed969ea790723f77e30419..7502f1f324f78f13bfed6b98ca295d7072285086 100644 GIT binary patch delta 55 zcmZoMXfc@J&&a(oU^g=(_hcRxV<`@X6oy2G5+F_j;$nu>w3OoHr2PCG#?3Y?>zFsQ IbNuB80FWLJ3jhEB delta 31 ncmZoMXfc@J&&ahgU^g=(*JK_R role names: +-- 'standard_profile' or empty with view 0-2 -> Standard +-- 'superuser_profile' or view=2 -> Superuser +-- 'admin_profile' or view=4 -> Admin +-- 'adminplus_profile' or view=5 -> AdminPlus +-- 'build' -> Build +-- 'commerce' -> Commerce +-- 'distribution' -> Distribution +-- 'firmware' -> Firmware +-- 'garage' -> Garage +-- 'interface' -> Interface +-- 'service' -> Service +-- 'other' -> Other +-- +-- users.view field (legacy permission level): +-- 1 = SuperUser +-- 2 = Create & Update +-- 3 = Read-only +-- 4 = Admin +-- 5 = Admin+ +-- =================================================== + +-- Get role IDs +SET @role_standard = (SELECT rowID FROM user_roles WHERE name = 'Standard' LIMIT 1); +SET @role_superuser = (SELECT rowID FROM user_roles WHERE name = 'Superuser' LIMIT 1); +SET @role_admin = (SELECT rowID FROM user_roles WHERE name = 'Admin' LIMIT 1); +SET @role_adminplus = (SELECT rowID FROM user_roles WHERE name = 'AdminPlus' LIMIT 1); +SET @role_build = (SELECT rowID FROM user_roles WHERE name = 'Build' LIMIT 1); +SET @role_commerce = (SELECT rowID FROM user_roles WHERE name = 'Commerce' LIMIT 1); +SET @role_distribution = (SELECT rowID FROM user_roles WHERE name = 'Distribution' LIMIT 1); +SET @role_firmware = (SELECT rowID FROM user_roles WHERE name = 'Firmware' LIMIT 1); +SET @role_garage = (SELECT rowID FROM user_roles WHERE name = 'Garage' LIMIT 1); +SET @role_interface = (SELECT rowID FROM user_roles WHERE name = 'Interface' LIMIT 1); +SET @role_service = (SELECT rowID FROM user_roles WHERE name = 'Service' LIMIT 1); +SET @role_other = (SELECT rowID FROM user_roles WHERE name = 'Other' LIMIT 1); + +-- =================================================== +-- PHASE 1: MIGRATE USERS BY SETTINGS FIELD (profile name) +-- =================================================== + +-- Users with 'standard_profile' setting +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT id, @role_standard, 1, 'migration_script', NOW(), NOW(), 1 +FROM users +WHERE settings = 'standard_profile' +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with 'superuser_profile' setting +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT id, @role_superuser, 1, 'migration_script', NOW(), NOW(), 1 +FROM users +WHERE settings = 'superuser_profile' +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with 'admin_profile' setting +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT id, @role_admin, 1, 'migration_script', NOW(), NOW(), 1 +FROM users +WHERE settings = 'admin_profile' +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with 'adminplus_profile' setting +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT id, @role_adminplus, 1, 'migration_script', NOW(), NOW(), 1 +FROM users +WHERE settings = 'adminplus_profile' +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with 'build' setting +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT id, @role_build, 1, 'migration_script', NOW(), NOW(), 1 +FROM users +WHERE settings = 'build' +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with 'commerce' setting +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT id, @role_commerce, 1, 'migration_script', NOW(), NOW(), 1 +FROM users +WHERE settings = 'commerce' +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with 'distribution' setting +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT id, @role_distribution, 1, 'migration_script', NOW(), NOW(), 1 +FROM users +WHERE settings = 'distribution' +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with 'firmware' setting +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT id, @role_firmware, 1, 'migration_script', NOW(), NOW(), 1 +FROM users +WHERE settings = 'firmware' +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with 'garage' setting +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT id, @role_garage, 1, 'migration_script', NOW(), NOW(), 1 +FROM users +WHERE settings = 'garage' +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with 'interface' setting +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT id, @role_interface, 1, 'migration_script', NOW(), NOW(), 1 +FROM users +WHERE settings = 'interface' +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with 'service' setting +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT id, @role_service, 1, 'migration_script', NOW(), NOW(), 1 +FROM users +WHERE settings = 'service' +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with 'other' setting +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT id, @role_other, 1, 'migration_script', NOW(), NOW(), 1 +FROM users +WHERE settings = 'other' +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- =================================================== +-- PHASE 2: MIGRATE USERS WITH EMPTY/NULL SETTINGS (use view field) +-- Only for users not already assigned a role +-- =================================================== + +-- Users with view=5 (Admin+) and no settings +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT u.id, @role_adminplus, 1, 'migration_script', NOW(), NOW(), 1 +FROM users u +LEFT JOIN user_role_assignments ura ON u.id = ura.user_id AND ura.is_active = 1 +WHERE (u.settings IS NULL OR u.settings = '') + AND u.view = '5' + AND ura.rowID IS NULL +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with view=4 (Admin) and no settings +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT u.id, @role_admin, 1, 'migration_script', NOW(), NOW(), 1 +FROM users u +LEFT JOIN user_role_assignments ura ON u.id = ura.user_id AND ura.is_active = 1 +WHERE (u.settings IS NULL OR u.settings = '') + AND u.view = '4' + AND ura.rowID IS NULL +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with view=1 (SuperUser) and no settings +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT u.id, @role_superuser, 1, 'migration_script', NOW(), NOW(), 1 +FROM users u +LEFT JOIN user_role_assignments ura ON u.id = ura.user_id AND ura.is_active = 1 +WHERE (u.settings IS NULL OR u.settings = '') + AND u.view = '1' + AND ura.rowID IS NULL +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- Users with view=2 or view=3 (Create/Update or Read-only) and no settings -> Standard +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT u.id, @role_standard, 1, 'migration_script', NOW(), NOW(), 1 +FROM users u +LEFT JOIN user_role_assignments ura ON u.id = ura.user_id AND ura.is_active = 1 +WHERE (u.settings IS NULL OR u.settings = '') + AND u.view IN ('2', '3') + AND ura.rowID IS NULL +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- =================================================== +-- PHASE 3: CATCH-ALL - Any remaining users without role -> Standard +-- =================================================== + +INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`) +SELECT u.id, @role_standard, 1, 'migration_script', NOW(), NOW(), 1 +FROM users u +LEFT JOIN user_role_assignments ura ON u.id = ura.user_id AND ura.is_active = 1 +WHERE ura.rowID IS NULL +ON DUPLICATE KEY UPDATE updated = NOW(); + +-- =================================================== +-- VERIFICATION QUERIES +-- =================================================== + +-- Check migration results: users per role +SELECT + ur.name as role_name, + COUNT(ura.user_id) as user_count +FROM user_roles ur +LEFT JOIN user_role_assignments ura ON ur.rowID = ura.role_id AND ura.is_active = 1 +GROUP BY ur.rowID, ur.name +ORDER BY user_count DESC; + +-- Check for users without role assignments (should be 0) +SELECT COUNT(*) as users_without_role +FROM users u +LEFT JOIN user_role_assignments ura ON u.id = ura.user_id AND ura.is_active = 1 +WHERE ura.rowID IS NULL; + +-- Compare old vs new: show users with their old settings and new role +SELECT + u.id, + u.username, + u.settings as old_profile, + u.view as old_view_level, + ur.name as new_role +FROM users u +LEFT JOIN user_role_assignments ura ON u.id = ura.user_id AND ura.is_active = 1 +LEFT JOIN user_roles ur ON ura.role_id = ur.rowID +ORDER BY u.id +LIMIT 50; + +-- =================================================== +-- Change ROLLBACK to COMMIT when ready to apply +-- =================================================== +COMMIT; diff --git a/assets/functions.php b/assets/functions.php index e4592d3..400aa0a 100644 --- a/assets/functions.php +++ b/assets/functions.php @@ -515,10 +515,19 @@ echo << allowed_actions_string] $always_allowed = [ - 'com_log' => 'U', + 'com_log' => 'CRU', + 'application' => 'CRU', 'user_permissions' => 'R', 'software_update' => 'R', 'software_download' => 'R', 'software_available' => 'R', - 'history' => 'U', - 'payment' => 'U', - 'marketing_files' => 'CRUD', - 'marketing_folders' => 'CRUD', - 'marketing_tags' => 'CRUD', - 'marketing_upload' => 'CRUD', - 'marketing_delete' => 'CRUD' + 'history' => 'RU', + 'payment' => 'U' ]; - // Debug log - initial call - if(debug){ - $perm_count = is_array($permissions) ? count($permissions) : 'not_array'; - $test = "$date - isAllowed called: access_element=$access_element, basic_permission_level=$basic_permission_level, action=$action, permissions_count=$perm_count".PHP_EOL; - error_log($test, 3, $filelocation); - } - // 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); - } + return 1; } // 2. Check always_allowed list if (isset($always_allowed[$access_element]) && str_contains($always_allowed[$access_element], $action)) { - if(debug){ - $test = "$date - Allowed by always_allowed list".PHP_EOL; - error_log($test, 3, $filelocation); - } + return 1; } @@ -1691,20 +1683,21 @@ function getProfile($profile, $permission){ $permission_key = $action_map[$action] ?? null; if ($permission_key && isset($element_permissions[$permission_key]) && $element_permissions[$permission_key] == 1) { - if(debug){ - $test = "$date - Allowed by RBAC permissions: $access_element -> $permission_key = 1".PHP_EOL; - error_log($test, 3, $filelocation); - } + return 1; } if(debug){ + $test = "$date - isAllowed called: access_element=$access_element, basic_permission_level=$basic_permission_level, action=$action".PHP_EOL; + error_log($test, 3, $filelocation); $perm_value = $element_permissions[$permission_key] ?? 'not_set'; $test = "$date - RBAC check failed: $access_element -> $permission_key = $perm_value".PHP_EOL; error_log($test, 3, $filelocation); } } else { if(debug){ + $test = "$date - isAllowed called: access_element=$access_element, basic_permission_level=$basic_permission_level, action=$action".PHP_EOL; + error_log($test, 3, $filelocation); $test = "$date - Access element '$access_element' not found in permissions array".PHP_EOL; error_log($test, 3, $filelocation); } @@ -1712,9 +1705,12 @@ function getProfile($profile, $permission){ // Not allowed if(debug){ + $test = "$date - isAllowed called: access_element=$access_element, basic_permission_level=$basic_permission_level, action=$action".PHP_EOL; + error_log($test, 3, $filelocation); $test = "$date - Not allowed: access_element=$access_element, action=$action".PHP_EOL; error_log($test, 3, $filelocation); } + return 0; } @@ -3913,27 +3909,29 @@ function dateInRange($start_date, $end_date, $date_check) function getLatestVersion($productcode,$token){ - //CALL TO API TO GET ALL ACTIVE CONTRACTS - $api_url = '/v2/products_software/productcode='.$productcode; - $responses = ioAPIv2($api_url,'',$token); + //$pdo = dbConnect($dbname); - //Decode Payload - if (!empty($responses)){$responses = json_decode($responses,true); - } - else{ - $responses = $output = array( - "productcode" => "", - "version"=> "", - "mandatory"=> "", - "latest"=> "", - "software"=> "", - "source" => "", - "source_type" => "" - ); - ;} + //CALL TO API TO GET ALL ACTIVE CONTRACTS + $api_url = '/v2/products_software/productcode='.$productcode; + $responses = ioAPIv2($api_url,'',$token); + + //Decode Payload + if (!empty($responses)){$responses = json_decode($responses,true); + } + else{ + $responses = $output = array( + "productcode" => "", + "version"=> "", + "mandatory"=> "", + "latest"=> "", + "software"=> "", + "source" => "", + "source_type" => "" + ); + ;} - //DEFAULT OUTPUT - return $responses; + //DEFAULT OUTPUT + return $responses; } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/assets/marketing.js b/assets/marketing.js index faf20a2..240287f 100644 --- a/assets/marketing.js +++ b/assets/marketing.js @@ -249,15 +249,19 @@ class MarketingFileManager { emptyState.style.display = 'none'; try { - // Use proper folder ID (null for root, or the folder ID) - const folderId = this.currentFolder ? this.currentFolder : 'null'; // Add cache busting to prevent browser caching - let url = `index.php?page=marketing&action=marketing_files&folder_id=${folderId}&limit=50&_t=${Date.now()}`; - + let url = `index.php?page=marketing&action=marketing_files&limit=50&_t=${Date.now()}`; + + // Only filter by folder if no tag filter is active (tag search is across all folders) + if (!this.filters.tag) { + const folderId = this.currentFolder ? this.currentFolder : 'null'; + url += `&folder_id=${folderId}`; + } + if (this.filters.search) { url += `&search=${encodeURIComponent(this.filters.search)}`; } - + if (this.filters.tag) { url += `&tag=${encodeURIComponent(this.filters.tag)}`; } @@ -289,21 +293,33 @@ class MarketingFileManager { if (data && data.length > 0) { let files = data; - + // Client-side file type filtering if (this.filters.fileTypes.length > 0) { - files = files.filter(file => + files = files.filter(file => this.filters.fileTypes.includes(file.file_type.toLowerCase()) ); } - + if (files.length === 0) { - emptyState.style.display = 'block'; + // No files after filtering, check for subfolders + const subfolders = this.getSubfolders(this.currentFolder); + if (subfolders.length > 0) { + this.renderFolderTiles(subfolders); + } else { + emptyState.style.display = 'block'; + } } else { this.renderFiles(files); } } else { - emptyState.style.display = 'block'; + // No files, check for subfolders + const subfolders = this.getSubfolders(this.currentFolder); + if (subfolders.length > 0) { + this.renderFolderTiles(subfolders); + } else { + emptyState.style.display = 'block'; + } } } catch (error) { console.error('Error loading files:', error); @@ -372,12 +388,73 @@ class MarketingFileManager { renderFiles(files) { const container = document.getElementById('filesContainer'); container.innerHTML = ''; - + files.forEach(file => { const fileElement = this.createFileElement(file); container.appendChild(fileElement); }); } + + getSubfolders(folderId) { + // Find immediate children of the specified folder + if (!folderId || folderId === '') { + // Root folder - return top-level folders + return this.folders; + } + + // Recursively search for the folder and return its children + const findFolder = (folders, targetId) => { + for (const folder of folders) { + if (folder.id === targetId) { + return folder.children || []; + } + if (folder.children && folder.children.length > 0) { + const found = findFolder(folder.children, targetId); + if (found) return found; + } + } + return []; + }; + + return findFolder(this.folders, folderId); + } + + renderFolderTiles(subfolders) { + const container = document.getElementById('filesContainer'); + container.innerHTML = ''; + + subfolders.forEach(folder => { + const folderElement = this.createFolderTileElement(folder); + container.appendChild(folderElement); + }); + } + + createFolderTileElement(folder) { + const folderElement = document.createElement('div'); + folderElement.className = `folder-tile ${this.viewMode}-item`; + folderElement.setAttribute('data-folder-id', folder.id); + + folderElement.innerHTML = ` +
+ +
+
+
+ ${this.escapeHtml(folder.folder_name)} +
+
+ ${folder.file_count || 0} files +
+
+ `; + + // Click to navigate to folder + folderElement.addEventListener('click', () => { + this.selectFolder(folder.id); + }); + + return folderElement; + } createFileElement(file) { const fileElement = document.createElement('div'); @@ -385,7 +462,7 @@ class MarketingFileManager { fileElement.setAttribute('data-file-id', file.id); const thumbnail = this.getThumbnail(file); - const tags = file.tags.map(tag => `${this.escapeHtml(tag)}`).join(''); + const tags = file.tags.map(tag => `${this.escapeHtml(tag)}`).join(''); fileElement.innerHTML = `
@@ -429,11 +506,20 @@ class MarketingFileManager { fileElement.querySelector('.edit-btn').addEventListener('click', () => { this.editFile(file); }); - + + // Make tags clickable to filter by tag + fileElement.querySelectorAll('.tag.clickable').forEach(tagElement => { + tagElement.addEventListener('click', (e) => { + e.stopPropagation(); + const tagName = tagElement.getAttribute('data-tag'); + this.filterByTag(tagName); + }); + }); + fileElement.addEventListener('dblclick', () => { this.previewFile(file); }); - + return fileElement; } @@ -843,15 +929,41 @@ class MarketingFileManager { updateFileTypeFilters() { this.filters.fileTypes = []; - + document.querySelectorAll('.file-type-filters input[type="checkbox"]:checked').forEach(checkbox => { const types = checkbox.value.split(','); this.filters.fileTypes.push(...types); }); - + this.loadFiles(); } - + + filterByTag(tagName) { + // Set the tag filter + this.filters.tag = tagName; + + // Update the dropdown to show the selected tag + const tagSelect = document.getElementById('tagFilter'); + if (tagSelect) { + tagSelect.value = tagName; + } + + // Clear folder selection to search across all folders + this.currentFolder = ''; + + // Update folder tree UI to show root as active + document.querySelectorAll('.folder-item').forEach(item => { + item.classList.remove('active'); + }); + const rootFolder = document.querySelector('.folder-item.root'); + if (rootFolder) { + rootFolder.classList.add('active'); + } + + // Reload files with the tag filter + this.loadFiles(); + } + populateTagFilter(tags) { const select = document.getElementById('tagFilter'); select.innerHTML = ''; diff --git a/assets/softwaretool.js b/assets/softwaretool.js index 0bc22c8..e730632 100644 --- a/assets/softwaretool.js +++ b/assets/softwaretool.js @@ -100,6 +100,45 @@ if (document.readyState === 'loading') { checkBrowserCompatibility(); } +// Shared serial port reference for upload.js to use +window.sharedSerialPort = null; + +// Override requestPort to minimize user prompts +// This intercepts all requestPort calls (including from upload.js) to reuse authorized ports +if ('serial' in navigator) { + const originalRequestPort = navigator.serial.requestPort.bind(navigator.serial); + + navigator.serial.requestPort = async function(options) { + // If we have a shared port, return it instead of prompting + if (window.sharedSerialPort) { + console.log('Using shared serial port (no prompt needed)'); + return window.sharedSerialPort; + } + + // Try already-authorized ports matching the filters + const ports = await navigator.serial.getPorts(); + if (ports.length > 0 && options?.filters) { + const match = ports.find(p => { + const info = p.getInfo(); + return options.filters.some(f => + info.usbVendorId === f.usbVendorId && + info.usbProductId === f.usbProductId + ); + }); + if (match) { + console.log('Using previously authorized port (no prompt needed)'); + window.sharedSerialPort = match; + return match; + } + } + + // Fallback: original prompt behavior + const port = await originalRequestPort(options); + window.sharedSerialPort = port; // Store for future use + return port; + }; +} + // Function to log communication to API (reused from scripts.js) async function logCommunication(data, direction) { // Only log if debug mode is enabled @@ -400,7 +439,11 @@ async function closePortAfterRead() { await port.close(); await logCommunication('Port closed successfully', 'info'); - // Reset for next connection + // Keep port reference in sharedSerialPort for upload.js to reuse + // This prevents the need for another user prompt during firmware upload + window.sharedSerialPort = port; + + // Reset local variables for next connection reader = null; writer = null; readableStreamClosed = null; @@ -410,7 +453,12 @@ async function closePortAfterRead() { console.error('Error closing port after read:', error); await logCommunication(`Error closing port: ${error.message}`, 'error'); - // Force reset even on error + // Keep port reference even on error if port exists + if (port) { + window.sharedSerialPort = port; + } + + // Force reset local variables even on error reader = null; writer = null; readableStreamClosed = null; diff --git a/equipment.php b/equipment.php index 93a9a08..e52af6d 100644 --- a/equipment.php +++ b/equipment.php @@ -440,9 +440,15 @@ $view .= '
'.getRelativeTime($responses->created).' - '.$general_updated.' - '.getRelativeTime($responses->updated).' - + '.$general_updated.''; + + if ($update_allowed === 1){ + $view .= ''.getRelativeTime($responses->updated).''; + } else { + $view .= ''.getRelativeTime($responses->updated).''; + } + + $view .= '
diff --git a/factuur.php b/factuur.php index 015b461..dea47e3 100644 --- a/factuur.php +++ b/factuur.php @@ -104,7 +104,7 @@ if (isset($_POST['email_invoice'])) { $attachment = $dompdf->output(); $attachment_name = $file_name . '.pdf'; - $header_redirect = 'Location: index.php?page=order&id=' . $order_id . '&success=invoice_sent'; + $header_redirect = 'Location: index.php?page=order&txn_id=' . $order_id . '&success=invoice_sent'; // Send to PHPMailer send_mail($to, $subject, $message, $attachment, $attachment_name); @@ -120,7 +120,7 @@ if (isset($_POST['email_invoice_to_admin'])) { $attachment = $dompdf->output(); $attachment_name = $file_name . '.pdf'; - $header_redirect = 'Location: index.php?page=order&id=' . $order_id . '&success=invoice_sent_admin'; + $header_redirect = 'Location: index.php?page=order&txn_id=' . $order_id . '&success=invoice_sent_admin'; // Send to bookkeeping if configured if (invoice_bookkeeping && email_bookkeeping) { @@ -144,7 +144,7 @@ if (isset($_GET['show_invoice'])) { } // If no action specified, redirect back -header('Location: index.php?page=order&id=' . $order_number); +header('Location: index.php?page=order&txn_id=' . $order_number); exit; ?> \ No newline at end of file diff --git a/index.php b/index.php index 4b787c6..0a7f799 100644 --- a/index.php +++ b/index.php @@ -106,23 +106,28 @@ if (isset($_GET['page']) && $_GET['page'] == 'logout') { die(); } -//===================================== -//DEFINE WHERE TO SEND THE USER TO. GET first assigned view in the profile if not available use dashboard -/*===================================== -$allowed_views = explode(',',$_SESSION['authorization']['profile']); -$ignoreViews = ['profile','assets','sales']; - -// If dashboard is in the profile, prioritize it -if (in_array('dashboard', $allowed_views) && file_exists('dashboard.php')) { - $allowed_views = 'dashboard'; -} else { - $allowed_views = findExistingView($allowed_views, 'dashboard', $ignoreViews); -} -*/ //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // SIMPLE ROUTING SYSTEM //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -$page = $_GET['page'] ?? 'dashboard'; +if (isset($_GET['page'])) { + $page = $_GET['page']; +} else { + // Get first available page from user's permissions using the menu structure + $default_page = null; + if (!empty($_SESSION['authorization']['permissions'])) { + include_once dirname(__FILE__).'/settings/settingsmenu.php'; + $filteredMenu = filterMenuByPermissions($main_menu, $_SESSION['authorization']['permissions']); + + // Get first menu item's URL as default page + foreach ($filteredMenu as $section) { + if (isset($section['main_menu']['url'])) { + $default_page = $section['main_menu']['url']; + break; + } + } + } + $page = $default_page ?? 'dashboard'; +} // Sanitize page parameter to prevent directory traversal $page = preg_replace('/[^a-zA-Z0-9_-]/', '', $page); @@ -135,10 +140,6 @@ 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 { @@ -166,9 +167,6 @@ try {

Please check the URL or navigate using the menu.

- - Return to Dashboard - '; template_footer(); } @@ -195,9 +193,6 @@ try {

Please try again or contact the system administrator.

- - Return to Dashboard - diff --git a/login.php b/login.php index be38c0f..d3699bb 100644 --- a/login.php +++ b/login.php @@ -78,13 +78,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { //Decode Payload if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = '400';} - - if ($responses === 'NOK'){ + + if ($responses === 'NOK' || $responses === 'NULL' || $responses === 'NULL '){ $retry++; $password_err = $password_err_1 ?? 'Not authorized, please retry'; } elseif ($responses == '1'){ $password_err = $password_err_2 ?? 'Too many login attempts. User blocked, please contact your administrator'; - } else { + } elseif (!empty($responses['userkey']) && ctype_xdigit($responses['userkey'])) { // Start a new session session_start(); @@ -93,14 +93,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $_SESSION['authorization'] = $responses; $language_user = trim($_SESSION['authorization']['language']) ?? 'US'; - if($responses->profile == 'firmwaretool,products_software,application'){ - header('location: index.php?page=firmwaretool'); - exit(); - } else { - header('location: index.php?language='.$language_user.''); - exit(); - } + header('location: index.php?language='.$language_user.''); + exit(); + + } else { + $retry++; + $password_err = $password_err_1 ?? 'Not authorized, please retry'; } } else { diff --git a/order.php b/order.php index 8092ddc..fcfa9bb 100644 --- a/order.php +++ b/order.php @@ -85,7 +85,7 @@ $view = ' // //------------------------------------ if ($update_allowed_edit === 1){ - $view .= '✏️'; + $view .= '✏️'; } $view .= '
'; @@ -310,7 +310,7 @@ $view .=' Giftcards
- Relate giftcards + Relate giftcards diff --git a/partners.php b/partners.php index da9b4a4..1ff7852 100644 --- a/partners.php +++ b/partners.php @@ -17,6 +17,7 @@ if (isAllowed($page,$_SESSION['authorization']['permissions'],$_SESSION['authori exit; } //PAGE Security +$page = 'partner'; $update_allowed = isAllowed($page ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'U'); $delete_allowed = isAllowed($page ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'D'); $create_allowed = isAllowed($page ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'C'); diff --git a/settings/systemservicetool_init.php b/settings/systemservicetool_init.php index 1648ef8..bc3971f 100644 --- a/settings/systemservicetool_init.php +++ b/settings/systemservicetool_init.php @@ -2,7 +2,7 @@ //================================================================= //Software version SERVICE Tool==================================== -//================================================================= +/*================================================================= $latest_version = getLatestVersion('EPSK01',$clientsecret) ?? ''; $service_tool_current_version = ($latest_version !='') ? $latest_version['version'] : ''; @@ -11,7 +11,7 @@ $software_download_url = 'https://'.$_SERVER['SERVER_NAME'].'/firmware'.'/'; //getSoftware (legacy) $software_url = ($latest_version !='') ? $latest_version['source'] : 'https://'.$_SERVER['SERVER_NAME'].'/firmware'.'/'.$service_tool_current_filename; - +*/ //================================================================= //SERVICE Tool manual =================================== //================================================================= @@ -90,9 +90,9 @@ $init = array( "ManualURL"=> $manual_url, "termsURL"=> "https://emergency-plug.com/en/terms-and-conditions", "Application" => array( - "current_version" => $service_tool_current_version, - "current_filename" => $service_tool_current_filename, - "location" => $software_download_url + "current_version" => '', + "current_filename" => '', + "location" => '' ) ); diff --git a/softwaretool.php b/softwaretool.php index b3db692..e1251d9 100644 --- a/softwaretool.php +++ b/softwaretool.php @@ -197,14 +197,10 @@ $view .= ' '; -if (isset($_GET['equipmentID'])){$returnpage = 'equipment&equipmentID='.$_GET['equipmentID']; } else {$returnpage = 'dashboard';} - - //SHOW BACK BUTTON ONLY FOR PORTAL USERS if (isAllowed($page ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'R') != 0){ $view .= '
- diff --git a/style/admin.css b/style/admin.css index a90502f..959804a 100644 --- a/style/admin.css +++ b/style/admin.css @@ -591,6 +591,8 @@ main .content-block-wrapper .content-block { width: 100%; margin: 0 10px; border-radius: 3px; + max-height: 300px; + overflow-y: auto; } main .content-block-wrapper .content-block:first-child { @@ -3140,6 +3142,61 @@ main .content-block .button-container { border-top: 1px solid #eee; } +/* Generic Modal Styles */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + align-items: center; + justify-content: center; +} + +.modal .modal-content { + background: white; + border-radius: 12px; + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow-y: auto; + margin: 20px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); +} + +.modal .modal-header { + padding: 20px 25px; + border-bottom: 1px solid #e0e0e0; +} + +.modal .modal-header h3 { + margin: 0; + color: #333; + display: flex; + align-items: center; + gap: 10px; +} + +.modal .modal-body { + padding: 25px; +} + +.modal .modal-body p { + margin: 0 0 10px 0; +} + +.btn.danger { + background-color: #e74c3c; + color: white; +} + +.btn.danger:hover { + background-color: #c0392b; +} + /* Registration Modal Styles */ .reg-modal { diff --git a/style/marketing.css b/style/marketing.css index 8668595..88661e8 100644 --- a/style/marketing.css +++ b/style/marketing.css @@ -402,6 +402,94 @@ font-size: 0.75rem; } +.tag.clickable { + cursor: pointer; + transition: all 0.2s; +} + +.tag.clickable:hover { + background: var(--color-primary, #007cba); + color: white; +} + +/* Folder Tiles */ +.folder-tile { + background: var(--color-white, #fff); + border: 1px solid var(--color-border, #dee2e6); + border-radius: 8px; + padding: 1rem; + cursor: pointer; + transition: all 0.2s; + position: relative; +} + +.folder-tile:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + border-color: var(--color-primary, #005655); +} + +.folder-tile.grid-item { + text-align: center; +} + +.folder-tile.list-item { + display: flex; + align-items: center; + gap: 1rem; + text-align: left; +} + +.folder-tile-icon { + width: 100%; + height: 150px; + margin-bottom: 1rem; + display: flex; + align-items: center; + justify-content: center; + background: var(--color-light-grey, #f8f9fa); + border-radius: 4px; + font-size: 4rem; + color: var(--color-primary, #005655); +} + +.folder-tile.list-item .folder-tile-icon { + width: 60px; + height: 60px; + flex-shrink: 0; + font-size: 2rem; + margin-bottom: 0; +} + +.folder-tile-info { + text-align: center; +} + +.folder-tile.list-item .folder-tile-info { + flex: 1; + text-align: left; +} + +.folder-tile-name { + font-weight: 600; + margin-bottom: 0.5rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.folder-tile-meta { + display: flex; + justify-content: center; + gap: 0.5rem; + font-size: 0.8rem; + color: var(--color-text-light, #6c757d); +} + +.folder-tile.list-item .folder-tile-meta { + justify-content: flex-start; +} + /* Loading and Empty States */ .loading-indicator, .empty-state { display: flex; diff --git a/user.php b/user.php index eb68f1e..c36c70e 100644 --- a/user.php +++ b/user.php @@ -12,33 +12,25 @@ include_once './settings/settings_redirector.php'; //SET ORIGIN FOR NAVIGATION $_SESSION['prev_origin_user'] = $_SERVER['REQUEST_URI']; + $page = 'user'; //Check if allowed if (isAllowed($page,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'R') === 0){ header('location: index.php'); exit; } + //PAGE Security $page_manage = 'user_manage'; $update_allowed = isAllowed($page ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'U'); -$update_allowed_edit = isAllowed($page_manage ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'U'); $delete_allowed = isAllowed($page_manage ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'D'); $create_allowed = isAllowed($page_manage ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'C'); //GET Details from URL $user_ID = $_GET['id'] ?? ''; -if ($user_ID == ''){ - header('location: index.php?page=users'); - exit; -} - -//CALL TO API FOR User information -$api_url = '/v2/users/id='.$user_ID; -$responses = ioServer($api_url,''); -//Decode Payload -if (!empty($responses)){$responses = json_decode($responses);}else{$responses = null;} -$user = $responses[0]; +// Determine if this is a new user creation +$is_new_user = empty($user_ID); //Helper function to convert service hex string to 1/0 function isServiceActive($service) { @@ -49,13 +41,53 @@ function isServiceActive($service) { return 0; } -$service_active = isServiceActive($user->service); +if ($is_new_user) { + // Check create permission for new users + if ($create_allowed !== 1) { + header('location: index.php?page=users'); + exit; + } + // Create empty user object with default values + $user = (object)[ + 'rowID' => '', + 'username' => '', + 'email' => '', + 'userkey' => '1', + 'view' => 3, + 'settings' => '', + 'service' => 0, + 'language' => '', + 'login_count' => 0, + 'partnerhierarchy' => json_encode($_SESSION['authorization']['partnerhierarchy'] ?? new stdClass()), + 'created' => null, + 'updated' => null, + 'lastlogin' => null, + 'updatedby' => null + ]; + $service_active = 0; + $role_assignments = null; +} else { + //CALL TO API FOR User information + $api_url = '/v2/users/id='.$user_ID; + $responses = ioServer($api_url,''); + //Decode Payload + if (!empty($responses)){$responses = json_decode($responses);}else{$responses = null;} + $user = $responses[0] ?? null; -//CALL TO API FOR User Role Assignments -$api_url = '/v2/user_role_assignments/user_id='.$user_ID; -$role_assignments = ioServer($api_url,''); -//Decode Payload -if (!empty($role_assignments)){$role_assignments = json_decode($role_assignments);}else{$role_assignments = null;} + // If user not found, redirect + if ($user === null) { + header('location: index.php?page=users'); + exit; + } + + $service_active = isServiceActive($user->service); + + //CALL TO API FOR User Role Assignments + $api_url = '/v2/user_role_assignments/user_id='.$user_ID; + $role_assignments = ioServer($api_url,''); + //Decode Payload + if (!empty($role_assignments)){$role_assignments = json_decode($role_assignments);}else{$role_assignments = null;} +} //CALL TO API FOR All Available Roles $api_url = '/v2/user_roles/status=1&p=1'; @@ -70,10 +102,56 @@ if (!empty($all_roles_response)){ $all_roles = []; } +//------------------------------ +// Handle POST for creating new user +//------------------------------ +if (isset($_POST['create_user']) && $create_allowed === 1 && $is_new_user) { + // Build user data for new user + $user_data = [ + 'userkey' => $_POST['userkey'] ?? 1, + 'username' => $_POST['username'] ?? '', + 'email' => $_POST['email'] ?? '', + 'view' => $_POST['view'] ?? 3, + 'settings' => $_POST['settings'] ?? '', + 'service' => $_POST['service'] ?? 0, + 'language' => $_POST['language'] ?? '', + 'login_count' => 0, + 'salesid' => $_POST['salesid'] ?? '', + 'soldto' => $_POST['soldto'] ?? '', + 'shipto' => $_POST['shipto'] ?? '', + 'location' => $_POST['location'] ?? '' + ]; + + $data = json_encode($user_data, JSON_UNESCAPED_UNICODE); + $response = ioServer('/v2/users', $data); + + // Get the new user ID from the response + $new_user = json_decode($response); + $new_user_id = $new_user->id ?? null; + + // Save role assignments for new user if we have an ID and roles are selected + if ($new_user_id && !empty($_POST['roles'])) { + $role_data = [ + 'batch_update' => true, + 'user_id' => (int)$new_user_id, + 'roles' => array_map('intval', $_POST['roles']) + ]; + $data = json_encode($role_data, JSON_UNESCAPED_UNICODE); + ioServer('/v2/user_role_assignments', $data); + } + + if ($new_user_id) { + header('Location: index.php?page=user&id='.$new_user_id.'&success_msg=1'); + } else { + header('Location: index.php?page=users&success_msg=1'); + } + exit; +} + //------------------------------ // Handle POST for inline edit (user AND roles) //------------------------------ -if (isset($_POST['save_user']) && $update_allowed_edit === 1) { +if (isset($_POST['save_user']) && $update_allowed === 1 && !$is_new_user) { // Build user data using existing field names $user_data = [ 'id' => $user_ID, @@ -109,7 +187,7 @@ if (isset($_POST['save_user']) && $update_allowed_edit === 1) { } // Handle password reset -if (isset($_POST['reset']) && $update_allowed_edit === 1) { +if (isset($_POST['reset']) && $update_allowed === 1) { $data = json_encode(['id' => $user_ID, 'reset' => 'reset'], JSON_UNESCAPED_UNICODE); ioServer('/v2/users', $data); header('Location: index.php?page=user&id='.$user_ID.'&success_msg=4'); @@ -117,7 +195,7 @@ if (isset($_POST['reset']) && $update_allowed_edit === 1) { } // Handle unblock -if (isset($_POST['unblock']) && $update_allowed_edit === 1) { +if (isset($_POST['unblock']) && $update_allowed === 1) { $data = json_encode(['id' => $user_ID, 'login_count' => '0'], JSON_UNESCAPED_UNICODE); ioServer('/v2/users', $data); header('Location: index.php?page=user&id='.$user_ID.'&success_msg=5'); @@ -172,13 +250,24 @@ if (isset($_GET['success_msg'])) { } template_header(($user_title ?? 'User'), 'user', 'view'); + +if ($is_new_user) { + $page_title = ($user_new ?? 'New User'); +} else { + $page_title = ($user_h2 ?? 'User').' - '.$user->username; +} + $view = '
-

'.($user_h2 ?? 'User').' - '.$user->username.'

+

'.$page_title.'

'; -if ($update_allowed_edit === 1){ +if ($is_new_user) { + // New user mode - show save button directly + $view .= ''; +} elseif ($update_allowed === 1) { + // Edit mode - show edit/save toggle $view .= '✏️'; $view .= ''; } @@ -194,12 +283,17 @@ if (isset($success_msg)){ } // Start form wrapper for edit mode +$form_action = $is_new_user ? 'create_user' : 'save_user'; $view .= '
- + '; $view .= '
'; +// Display styles for view/edit mode (new users start in edit mode) +$view_style = $is_new_user ? 'display:none;' : ''; +$edit_style = $is_new_user ? '' : 'display:none;'; + // User Information Block $view .= '
@@ -208,8 +302,8 @@ $view .= '

'.($general_status ?? 'Status').'

- '.$status_text.' - @@ -218,22 +312,22 @@ $view .= '

'.($User_username ?? 'Username').'

- '.$user->username.' - + '.$user->username.' +

'.($User_email ?? 'Email').'

- '.$user->email.' - + '.$user->email.' +

'.($User_language ?? 'Language').'

- '.($user->language ?? '-').' - '; foreach ($supportedLanguages as $language){ $view .= ''; @@ -249,7 +343,7 @@ $view .='

'.($view_user_roles ?? 'Assigned Roles').'
-
'; +
'; // Get list of already assigned role IDs $assigned_role_ids = []; @@ -290,9 +384,9 @@ if (!empty($role_assignments)){ $view .= '
'; // Close view-mode-roles -// EDIT MODE - Show all roles with checkboxes (only if user has edit permission) -if ($update_allowed_edit === 1 && !empty($all_roles)){ - $view .= '
'; } @@ -423,65 +517,67 @@ $location_dropdown = listPartner('location', $_SESSION['authorization']['permiss $view .= '
'.($User_permission ?? 'Permission Level').' - '; + '; // Display permission level text switch($user->view){ @@ -339,7 +433,7 @@ switch($user->view){ } $view .= ' - '; @@ -358,10 +452,10 @@ $view .= '
'.($User_profile ?? 'Profile').' - '.($user->settings ?? '-').''; + '.($user->settings ?? '-').''; if ($_SESSION['authorization']['permission'] == 3 || $_SESSION['authorization']['permission'] == 4){ - $view .= ' '; foreach ($all_profiles as $profile) { $view .= ''; @@ -376,8 +470,8 @@ $view .= '
'.($User_service ?? 'Service Access').' - '.(($service_active == 1) ? ($enabled ?? 'Enabled') : ($disabled ?? 'Disabled')).' - @@ -404,15 +498,15 @@ if ($_SESSION['authorization']['permission'] == 3 || $_SESSION['authorization'][ $view .= '
'.($general_salesid ?? 'Sales ID').' - '.($partner_data->salesid ?? '-').' - + '.($partner_data->salesid ?? '-').' + '.$salesid_dropdown.'
'.($general_soldto ?? 'Sold To').' - '.($partner_data->soldto ?? '-').' - + '.($partner_data->soldto ?? '-').' + '.$soldto_dropdown.'
'.($general_shipto ?? 'Ship To').' - '.($partner_data->shipto ?? '-').' - + '.($partner_data->shipto ?? '-').' + '.$shipto_dropdown.'
'.($general_location ?? 'Location').' - '.($partner_data->location ?? '-').' - + '.($partner_data->location ?? '-').' + '.$location_dropdown.'
'; -// Metadata Block -$view .= '
-
- '.($tab3 ?? 'Details').' -
-
- - - - - - - - - - - - - - - - - - - - - -
'.($general_created ?? 'Created').''.getRelativeTime($user->created).'
'.($User_lastlogin ?? 'Last Login').''.($user->lastlogin ? getRelativeTime($user->lastlogin) : '-').'
'.($general_updated ?? 'Updated').''.($user->updated ? getRelativeTime($user->updated) : '-').'
'.($general_updatedby ?? 'Updated By').''.($user->updatedby ?? '-').'
'.($User_pw_login_count ?? 'Login Attempts').' - '.$user->login_count.''; +// Metadata Block (hide for new users) +if (!$is_new_user) { + $view .= '
+
+ '.($tab3 ?? 'Details').' +
+
+ + + + + + + + + + + + + + + + + + + + + +
'.($general_created ?? 'Created').''.getRelativeTime($user->created).'
'.($User_lastlogin ?? 'Last Login').''.($user->lastlogin ? getRelativeTime($user->lastlogin) : '-').'
'.($general_updated ?? 'Updated').''.($user->updated ? getRelativeTime($user->updated) : '-').'
'.($general_updatedby ?? 'Updated By').''.($user->updatedby ?? '-').'
'.($User_pw_login_count ?? 'Login Attempts').' + '.$user->login_count.''; -if ($_SESSION['authorization']['permission'] == 3 || $_SESSION['authorization']['permission'] == 4){ - $view .= ''; -} else { - $view .= ''; + if ($_SESSION['authorization']['permission'] == 3 || $_SESSION['authorization']['permission'] == 4){ + $view .= ''; + } else { + $view .= ''; + } + + $view .= '
+
+
'; } - -$view .= '
-
-
- +$view .= ' '; -// Actions Block (outside form for separate actions) -if ($update_allowed_edit === 1){ +// Actions Block (outside form for separate actions, hide for new users) +if ($update_allowed === 1 && !$is_new_user){ $view .= '
'.($general_actions ?? 'Actions').' diff --git a/user_role.php b/user_role.php index e0e7fe4..736f342 100644 --- a/user_role.php +++ b/user_role.php @@ -24,6 +24,17 @@ $update_allowed = isAllowed($page ,$_SESSION['authorization']['permissions'],$_S $update_allowed_edit = isAllowed($page_manage ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'U'); $delete_allowed = isAllowed($page_manage ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'D'); $create_allowed = isAllowed($page_manage ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'C'); +$system_role_allowed = isAllowed('user_roles' ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'D'); + +//Handle AJAX request for role permissions (copy functionality) +if (isset($_GET['action']) && $_GET['action'] === 'get_role_permissions' && isset($_GET['source_role_id'])) { + header('Content-Type: application/json'); + $source_role_id = intval($_GET['source_role_id']); + $api_url = '/v2/role_access_permissions/role_id='.$source_role_id; + $role_perms = ioServer($api_url,''); + echo $role_perms; + exit; +} //GET Details from URL $GET_VALUES = urlGETdetails($_GET) ?? ''; @@ -63,17 +74,28 @@ $assignments = ioServer($api_url,''); //Decode Payload if (!empty($assignments)){$assignments = json_decode($assignments);}else{$assignments = null;} +//CALL TO API FOR All User Roles (for copy dropdown) +$api_url = '/v2/user_roles/status=1&all='; +$all_roles = ioServer($api_url,''); +//Decode Payload +if (!empty($all_roles)){$all_roles = json_decode($all_roles);}else{$all_roles = null;} + //------------------------------ // Handle POST for inline edit //------------------------------ if (isset($_POST['save_permissions']) && $update_allowed_edit === 1) { - // Update role info (name, description, status) - $role_data = json_encode([ + // Update role info (name, description, status, system role) + $role_data_array = [ 'rowID' => $role_id, 'name' => $_POST['name'] ?? '', 'description' => $_POST['description'] ?? '', 'is_active' => $_POST['is_active'] ?? 1 - ], JSON_UNESCAPED_UNICODE); + ]; + // Only allow is_system to be changed if user has delete permission on user_roles + if ($system_role_allowed === 1) { + $role_data_array['is_system'] = isset($_POST['is_system']) ? 1 : 0; + } + $role_data = json_encode($role_data_array, JSON_UNESCAPED_UNICODE); ioServer('/v2/user_roles', $role_data); // Process permission updates @@ -132,6 +154,21 @@ if (isset($_POST['save_permissions']) && $update_allowed_edit === 1) { exit; } +//------------------------------ +// Handle POST for delete +//------------------------------ +if (isset($_POST['delete_role']) && $delete_allowed === 1) { + $role_data = json_encode([ + 'rowID' => $role_id, + 'delete' => 'delete' + ], JSON_UNESCAPED_UNICODE); + ioServer('/v2/user_roles', $role_data); + + // Redirect to roles list with success message + header('Location: index.php?page='.$_SESSION['origin'].'&success_msg=3'); + exit; +} + //------------------------------ //Variables @@ -161,7 +198,11 @@ $view = ' if ($update_allowed_edit === 1){ $view .= '✏️'; - $view .= ''; + $view .= ''; +} + +if ($delete_allowed === 1){ + $view .= '🗑️'; } $view .= '
'; @@ -174,6 +215,32 @@ if (isset($success_msg)){
'; } +// Delete form (hidden) +if ($delete_allowed === 1){ + $view .= ''; + + // Delete confirmation modal + $view .= ''; +} + // Start form wrapper for edit mode $view .= '
@@ -210,6 +277,31 @@ $view .= '

+
+

'.($role_system ?? 'System Role').'

+

+ '.($responses->is_system == 1 ? ' '.($yes ?? 'Yes') : ' '.($no ?? 'No')).' + '.($system_role_allowed === 1 ? '' : '').' +

+
+ '; @@ -350,17 +442,20 @@ function togglePermissionsEdit() { var saveBtn = document.getElementById("saveBtn"); var viewElements = document.querySelectorAll(".view-mode"); var editElements = document.querySelectorAll(".edit-mode"); + var editBlockElements = document.querySelectorAll(".edit-mode-block"); var editOnlyRows = document.querySelectorAll(".edit-only-row"); var i; if (permissionsEditMode) { for (i = 0; i < viewElements.length; i++) { viewElements[i].style.display = "none"; } for (i = 0; i < editElements.length; i++) { editElements[i].style.display = "inline"; } + for (i = 0; i < editBlockElements.length; i++) { editBlockElements[i].style.display = "block"; } for (i = 0; i < editOnlyRows.length; i++) { editOnlyRows[i].style.display = "table-row"; } editBtn.style.display = "none"; saveBtn.style.display = "inline-block"; } else { for (i = 0; i < viewElements.length; i++) { viewElements[i].style.display = "inline"; } for (i = 0; i < editElements.length; i++) { editElements[i].style.display = "none"; } + for (i = 0; i < editBlockElements.length; i++) { editBlockElements[i].style.display = "none"; } for (i = 0; i < editOnlyRows.length; i++) { editOnlyRows[i].style.display = "none"; } editBtn.style.display = "inline-block"; saveBtn.style.display = "none"; @@ -378,6 +473,66 @@ function toggleColumn(type) { for (var i = 0; i < checkboxes.length; i++) { checkboxes[i].checked = !allChecked; } +} +async function copyPermissionsFromRole(roleId) { + if (!roleId) return; + + if (!confirm("'.($confirm_copy_permissions ?? 'This will override all current permission settings. Continue?').'")) { + document.getElementById("copyFromRole").value = ""; + return; + } + + try { + // Call PHP page which will use ioServer to get permissions + const response = await fetch("index.php?page=user_role&action=get_role_permissions&source_role_id=" + roleId); + if (!response.ok) throw new Error("Failed to fetch"); + const permissions = await response.json(); + + // Create a lookup map of permissions by access_id + var permMap = {}; + if (permissions && permissions.length > 0) { + for (var i = 0; i < permissions.length; i++) { + permMap[permissions[i].access_id] = permissions[i]; + } + } + + // Get all permission checkboxes and reset them + var allCheckboxes = document.querySelectorAll("input[type=checkbox][name^=\\"permissions[\\"]"); + for (var i = 0; i < allCheckboxes.length; i++) { + allCheckboxes[i].checked = false; + } + + // Apply copied permissions + for (var accessId in permMap) { + var perm = permMap[accessId]; + var cbCreate = document.querySelector("input[name=\\"permissions[" + accessId + "][C]\\"]"); + var cbRead = document.querySelector("input[name=\\"permissions[" + accessId + "][R]\\"]"); + var cbUpdate = document.querySelector("input[name=\\"permissions[" + accessId + "][U]\\"]"); + var cbDelete = document.querySelector("input[name=\\"permissions[" + accessId + "][D]\\"]"); + + if (cbCreate && perm.can_create == 1) cbCreate.checked = true; + if (cbRead && perm.can_read == 1) cbRead.checked = true; + if (cbUpdate && perm.can_update == 1) cbUpdate.checked = true; + if (cbDelete && perm.can_delete == 1) cbDelete.checked = true; + } + + // Reset dropdown + document.getElementById("copyFromRole").value = ""; + + } catch (error) { + console.error("Error copying permissions:", error); + alert("'.($error_copy_permissions ?? 'Failed to copy permissions. Please try again.').'"); + document.getElementById("copyFromRole").value = ""; + } +} +function confirmDeleteRole() { + document.getElementById("deleteModal").style.display = "flex"; +} +function closeDeleteModal() { + document.getElementById("deleteModal").style.display = "none"; +} +function executeDeleteRole() { + document.getElementById("deleteRoleForm").submit(); }'; template_footer($js); diff --git a/users.php b/users.php index 613b59f..5cff75a 100644 --- a/users.php +++ b/users.php @@ -23,6 +23,7 @@ if (isAllowed($page,$_SESSION['authorization']['permissions'],$_SESSION['authori exit; } //PAGE Security +$page = 'user'; $update_allowed = isAllowed($page ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'U'); $delete_allowed = isAllowed($page ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'D'); $create_allowed = isAllowed($page ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'C'); diff --git a/webhook_mollie.php b/webhook_mollie.php index d668b3e..cac5356 100644 --- a/webhook_mollie.php +++ b/webhook_mollie.php @@ -157,8 +157,8 @@ try { // Create license $sql = 'INSERT INTO products_software_licenses - (version_id, license_type, license_key, status, starts_at, expires_at, transaction_id, created, createdby) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'; + (version_id, license_type, license_key, status, starts_at, expires_at, transaction_id,accounthierarchy, created, createdby) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; $stmt = $pdo->prepare($sql); $stmt->execute([ $item['item_id'], // version_id @@ -168,6 +168,7 @@ try { date('Y-m-d H:i:s'), '2099-12-31 23:59:59', // effectively permanent $orderId, + '{"salesid":"21-Total Safety Solutions B.V.","soldto":""}', date('Y-m-d H:i:s'), 'webhook' // created by webhook ]); diff --git a/webhook_paypal.php b/webhook_paypal.php index 084e955..f25f56f 100644 --- a/webhook_paypal.php +++ b/webhook_paypal.php @@ -203,8 +203,8 @@ try { // Create license $sql = 'INSERT INTO products_software_licenses - (version_id, license_type, license_key, status, starts_at, expires_at, transaction_id, created, createdby) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'; + (version_id, license_type, license_key, status, starts_at, expires_at, transaction_id, accounthierarchy,created, createdby) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; $stmt = $pdo->prepare($sql); $stmt->execute([ $item['item_id'], // version_id @@ -214,6 +214,7 @@ try { date('Y-m-d H:i:s'), '2099-12-31 23:59:59', // effectively permanent $orderId, + '{"salesid":"21-Total Safety Solutions B.V.","soldto":""}', date('Y-m-d H:i:s'), 'webhook_paypal' // created by PayPal webhook ]);