diff --git a/app/ac_dashboard/api/engine/by_source.php b/app/ac_dashboard/api/engine/by_source.php index b721ba3..edcbd83 100644 --- a/app/ac_dashboard/api/engine/by_source.php +++ b/app/ac_dashboard/api/engine/by_source.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/account_formulas.php b/app/accounting/account_formulas.php index fb270db..1e7ab14 100644 --- a/app/accounting/account_formulas.php +++ b/app/accounting/account_formulas.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/api/engine/account.php b/app/accounting/api/engine/account.php index dc93c73..3c3eec0 100644 --- a/app/accounting/api/engine/account.php +++ b/app/accounting/api/engine/account.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/chart_of_accounts.php b/app/accounting/chart_of_accounts.php index 7f170b8..345924a 100644 --- a/app/accounting/chart_of_accounts.php +++ b/app/accounting/chart_of_accounts.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/departments.php b/app/accounting/departments.php index 033fd47..0d23598 100644 --- a/app/accounting/departments.php +++ b/app/accounting/departments.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/gl_entries.php b/app/accounting/gl_entries.php index c970b77..82756f2 100644 --- a/app/accounting/gl_entries.php +++ b/app/accounting/gl_entries.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/gl_movement.php b/app/accounting/gl_movement.php index ae8a269..bd56062 100644 --- a/app/accounting/gl_movement.php +++ b/app/accounting/gl_movement.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/journal_listing.php b/app/accounting/journal_listing.php index efed99a..0fb24a3 100644 --- a/app/accounting/journal_listing.php +++ b/app/accounting/journal_listing.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/manage_account.php b/app/accounting/manage_account.php index f5da6c1..eefa1a0 100644 --- a/app/accounting/manage_account.php +++ b/app/accounting/manage_account.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/manage_department.php b/app/accounting/manage_department.php index f17f99f..ee938d4 100644 --- a/app/accounting/manage_department.php +++ b/app/accounting/manage_department.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/pl_statement.php b/app/accounting/pl_statement.php index 3e5907f..f7f3d99 100644 --- a/app/accounting/pl_statement.php +++ b/app/accounting/pl_statement.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/posting_window.php b/app/accounting/posting_window.php index ed4b4c9..a86cee2 100644 --- a/app/accounting/posting_window.php +++ b/app/accounting/posting_window.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/trial_balance.php b/app/accounting/trial_balance.php index 9ca2e24..315c3a2 100644 --- a/app/accounting/trial_balance.php +++ b/app/accounting/trial_balance.php @@ -1,5 +1,5 @@ diff --git a/app/accounting/vat_report.php b/app/accounting/vat_report.php index 1aadad4..66b44fe 100644 --- a/app/accounting/vat_report.php +++ b/app/accounting/vat_report.php @@ -1,5 +1,5 @@ diff --git a/app/assets/js/custom.js b/app/assets/js/custom.js index fe9c47e..63c1fa7 100644 --- a/app/assets/js/custom.js +++ b/app/assets/js/custom.js @@ -905,6 +905,17 @@ function ajax_request(options) { return; } + // Session ended on the server (idle timeout, not signed in, password changed): + // drop per-tab data and go back to the sign-in form. + const endedCodes = ['session_expired', 'auth_required', 'password_changed']; + if (xhr?.status === 401 && endedCodes.includes(xhr?.responseJSON?.code)) { + try { sessionStorage.clear(); } catch (e) {} + bootbox.alert(escape_html(xhr.responseJSON.message || 'Please sign in again.'), function() { + window.location.href = server_url + 'login/index.php'; + }); + return; + } + // File / payload too large (nginx 413) if (xhr?.status === 413) { bootbox.alert('The file you selected is too large. Please use an image under 2 MB and try again.'); diff --git a/app/assets/utils/app_access.php b/app/assets/utils/app_access.php new file mode 100644 index 0000000..f4036fb --- /dev/null +++ b/app/assets/utils/app_access.php @@ -0,0 +1,37 @@ + "Invalid request"])); } } // validate otp - $sql = "SELECT `password` + $sql = "SELECT `password`, license, app_access FROM user WHERE user_id = :company_id"; $sth = $pdo1->prepare($sql); @@ -68,7 +69,8 @@ if(!empty($_SESSION["login_company_id"])){ ":company_id" => $_SESSION["login_user_id"] ]); db_check($sth, $answer); - $password = $sth->fetchColumn(); + $user_row = $sth->fetch(PDO::FETCH_ASSOC) ?: []; + $password = $user_row['password'] ?? ''; /** Generate OTP */ function generateOTP($sercet_key, $time_step = 180, $length = 6){ $counter = floor($_SESSION["otpTime"] / $time_step); @@ -83,7 +85,9 @@ if(!empty($_SESSION["login_company_id"])){ $otp = generateOTP($password); if( $_SESSION["otp"]!=$otp ){ + http_response_code(401); $answer["message"] = "Your password has been reset, Please logout and login again."; + $answer["code"] = "password_changed"; exit(json_encode($answer)); } @@ -98,13 +102,29 @@ if(!empty($_SESSION["login_company_id"])){ $map = $sth->fetchAll(PDO::FETCH_ASSOC); if( count($map)==0 ){ + http_response_code(403); $answer["message"] = "Your accessibility to this company has been removed."; + $answer["code"] = "access_removed"; exit(json_encode($answer)); } $user_role = $map[0]['role'] ?? 'viewer'; $_SESSION['login_role'] = $user_role; + // App access (WMS / Accounting), re-read on every request so a change made in + // Setting → Users applies at once. Owners hold it on their own user row; + // invited users per company (same rule as login_confirm.php). + $app_access = (($user_row['license'] ?? 'owner') === 'owner') + ? ($user_row['app_access'] ?? 'wms') + : ($map[0]['app_access'] ?? 'wms'); + $_SESSION['login_app_access'] = $app_access; + + $required_app = app_access_app_for($_SERVER['SCRIPT_NAME'] ?? ''); + if ($required_app !== null && !app_access_allows($app_access, $required_app)) { + http_response_code(403); + exit(json_encode(['success' => 0, 'message' => 'Your account does not have access to this module.'])); + } + // Single-session enforcement: if a session_token was issued at login, verify // it still matches the DB. A mismatch means a newer login has taken over. if (!empty($_SESSION['session_token'])) { @@ -132,7 +152,12 @@ if(!empty($_SESSION["login_company_id"])){ // unless the engine explicitly declared itself a pre-auth route. if (empty($_SESSION['login_company_id']) && !defined('UNAUTHENTICATED_ROUTE')) { http_response_code(401); - exit(json_encode(['success' => 0, 'message' => 'Authentication required.'])); + $expired = !empty($_SESSION['_idle_expired']); + exit(json_encode([ + 'success' => 0, + 'message' => $expired ? 'Your session has expired. Please sign in again.' : 'Authentication required.', + 'code' => $expired ? 'session_expired' : 'auth_required', + ])); } // set up ANSWER @@ -153,6 +178,7 @@ if (!is_array($data)) { $data = $_POST; } else { // Truly no data received + http_response_code(400); $answer["message"] = "Request denied: No valid JSON payload or Form Data detected."; exit(json_encode($answer)); } diff --git a/app/contact/api/engine/contact.php b/app/contact/api/engine/contact.php index 7caa1a2..b359be5 100644 --- a/app/contact/api/engine/contact.php +++ b/app/contact/api/engine/contact.php @@ -1,5 +1,5 @@ diff --git a/app/contact/manage_contact.php b/app/contact/manage_contact.php index 1a50311..c33e43c 100644 --- a/app/contact/manage_contact.php +++ b/app/contact/manage_contact.php @@ -1,5 +1,5 @@ diff --git a/app/dashboard/low_stock_products.php b/app/dashboard/low_stock_products.php index b7b580b..1f52e68 100644 --- a/app/dashboard/low_stock_products.php +++ b/app/dashboard/low_stock_products.php @@ -1,5 +1,5 @@ diff --git a/app/expense/api/engine/convert_purchase_request.php b/app/expense/api/engine/convert_purchase_request.php index b225d69..010e0bd 100644 --- a/app/expense/api/engine/convert_purchase_request.php +++ b/app/expense/api/engine/convert_purchase_request.php @@ -1,5 +1,5 @@ diff --git a/app/expense/purchase_order.php b/app/expense/purchase_order.php index 6f1a71c..7904ecd 100644 --- a/app/expense/purchase_order.php +++ b/app/expense/purchase_order.php @@ -1,5 +1,5 @@ diff --git a/app/expense/purchase_request.php b/app/expense/purchase_request.php index f5a3c39..b9d16b0 100644 --- a/app/expense/purchase_request.php +++ b/app/expense/purchase_request.php @@ -1,5 +1,5 @@ diff --git a/app/finance/api/engine/delete_payment.php b/app/finance/api/engine/delete_payment.php index db37bd6..8785ed1 100644 --- a/app/finance/api/engine/delete_payment.php +++ b/app/finance/api/engine/delete_payment.php @@ -1,5 +1,5 @@ diff --git a/app/finance/receipt.php b/app/finance/receipt.php index 49c6abd..34b2d4b 100644 --- a/app/finance/receipt.php +++ b/app/finance/receipt.php @@ -1,5 +1,5 @@ diff --git a/app/ics/api/engine/approve_stock.php b/app/ics/api/engine/approve_stock.php index eb209bf..2190c1f 100644 --- a/app/ics/api/engine/approve_stock.php +++ b/app/ics/api/engine/approve_stock.php @@ -11,7 +11,7 @@ * warehouse int warehouse_id (to resolve the dynamic table) * type string 'in' | 'out' | 'transfer' */ -session_start(); +require_once __DIR__ . '/../../../session.php'; require_once '../../../assets/utils/db_auth.php'; require_once '../../../assets/utils/classes/StockManager.php'; require_once '../../../assets/utils/classes/WarehouseManager.php'; diff --git a/app/ics/api/engine/barcode_lookup.php b/app/ics/api/engine/barcode_lookup.php index 6db0793..d6edb5a 100644 --- a/app/ics/api/engine/barcode_lookup.php +++ b/app/ics/api/engine/barcode_lookup.php @@ -1,5 +1,5 @@ diff --git a/app/ics/stock_transfer.php b/app/ics/stock_transfer.php index 6c66970..625ce21 100644 --- a/app/ics/stock_transfer.php +++ b/app/ics/stock_transfer.php @@ -1,5 +1,5 @@ 0) { + ob_end_clean(); + } + header('Location: '.$server_url.'dashboard/index.php'); + exit; +} // ── User display data from session ──────────────────────────────────────────── $topbar_name = htmlspecialchars(trim(($_SESSION['login_name'] ?? '') . ' ' . ($_SESSION['login_surname'] ?? '')), ENT_QUOTES, 'UTF-8'); @@ -367,6 +379,7 @@ function log_out() { checkRequired: 0, action: 'read', onSuccess: function(res) { + try { sessionStorage.clear(); } catch (e) {} window.location.href = server_url + 'index.php'; } }); diff --git a/app/inventory/api/engine/manage_category.php b/app/inventory/api/engine/manage_category.php index 21380d0..76c21da 100644 --- a/app/inventory/api/engine/manage_category.php +++ b/app/inventory/api/engine/manage_category.php @@ -1,5 +1,5 @@ diff --git a/app/inventory/warehouse.php b/app/inventory/warehouse.php index 90307f8..c0f3e43 100644 --- a/app/inventory/warehouse.php +++ b/app/inventory/warehouse.php @@ -1,5 +1,5 @@ diff --git a/app/journal/new.php b/app/journal/new.php index 9a67953..e498b43 100644 --- a/app/journal/new.php +++ b/app/journal/new.php @@ -1,5 +1,5 @@ diff --git a/app/login/api/engine/back.php b/app/login/api/engine/back.php index 9b5daac..cf48fbd 100644 --- a/app/login/api/engine/back.php +++ b/app/login/api/engine/back.php @@ -34,7 +34,8 @@ if (!empty($_SESSION['login_user_id'])) { // Intentional 1-second delay — prevents timing attacks on session enumeration sleep(1); -session_destroy(); +// Clears the session data, deletes the session file and expires the cookie. +session_end_completely(); $answer["success"] = 1; exit(json_encode($answer)); \ No newline at end of file diff --git a/app/order/api/engine/cancel_order.php b/app/order/api/engine/cancel_order.php index 3a63187..be6f23f 100644 --- a/app/order/api/engine/cancel_order.php +++ b/app/order/api/engine/cancel_order.php @@ -1,5 +1,5 @@ diff --git a/app/order/manage_invoice.php b/app/order/manage_invoice.php index 54f4f69..5edae95 100644 --- a/app/order/manage_invoice.php +++ b/app/order/manage_invoice.php @@ -1,5 +1,5 @@ diff --git a/app/order/print_invoice.php b/app/order/print_invoice.php index 5746221..f91acb1 100644 --- a/app/order/print_invoice.php +++ b/app/order/print_invoice.php @@ -1,5 +1,5 @@ diff --git a/app/po/api/engine/cancel_po.php b/app/po/api/engine/cancel_po.php index 6d308e4..85bdc3e 100644 --- a/app/po/api/engine/cancel_po.php +++ b/app/po/api/engine/cancel_po.php @@ -1,5 +1,5 @@ diff --git a/app/po/manage_po.php b/app/po/manage_po.php index aa75577..283626c 100644 --- a/app/po/manage_po.php +++ b/app/po/manage_po.php @@ -1,5 +1,5 @@ diff --git a/app/po/supplier_returns.php b/app/po/supplier_returns.php index 911a6f5..3eaed25 100644 --- a/app/po/supplier_returns.php +++ b/app/po/supplier_returns.php @@ -1,5 +1,5 @@ diff --git a/app/reports/api/engine_report/bin_log.php b/app/reports/api/engine_report/bin_log.php index 297bb13..f54a251 100644 --- a/app/reports/api/engine_report/bin_log.php +++ b/app/reports/api/engine_report/bin_log.php @@ -1,5 +1,5 @@ diff --git a/app/reports/occupy_rack.php b/app/reports/occupy_rack.php index 908745b..b699413 100644 --- a/app/reports/occupy_rack.php +++ b/app/reports/occupy_rack.php @@ -1,5 +1,5 @@ diff --git a/app/reports/product_lot.php b/app/reports/product_lot.php index bccb40c..7503783 100644 --- a/app/reports/product_lot.php +++ b/app/reports/product_lot.php @@ -1,5 +1,5 @@ diff --git a/app/reports/stock_movement.php b/app/reports/stock_movement.php index a2f4874..a5ee3b2 100644 --- a/app/reports/stock_movement.php +++ b/app/reports/stock_movement.php @@ -1,5 +1,5 @@ diff --git a/app/revenue/api/engine/convert_quotation.php b/app/revenue/api/engine/convert_quotation.php index 5ad14ad..1451111 100644 --- a/app/revenue/api/engine/convert_quotation.php +++ b/app/revenue/api/engine/convert_quotation.php @@ -1,5 +1,5 @@ diff --git a/app/revenue/manage_credit_note.php b/app/revenue/manage_credit_note.php index a290f8e..4ba61d1 100644 --- a/app/revenue/manage_credit_note.php +++ b/app/revenue/manage_credit_note.php @@ -1,5 +1,5 @@ diff --git a/app/revenue/quotation.php b/app/revenue/quotation.php index 27b1fcf..776005a 100644 --- a/app/revenue/quotation.php +++ b/app/revenue/quotation.php @@ -1,5 +1,5 @@ diff --git a/app/session.php b/app/session.php index 3651670..b9766e2 100644 --- a/app/session.php +++ b/app/session.php @@ -1,5 +1,7 @@ 0, 'path' => $repo_name, 'domain' => '', - 'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on', + 'secure' => $is_https, 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); -} \ No newline at end of file + + // Idle timeout: a signed-in session untouched for SESSION_IDLE_SECONDS is + // cleared here, so pages redirect to the login form and API engines answer + // 401 (db_auth.php) exactly as for a visitor who never signed in. + if (!empty($_SESSION['login_company_id'])) { + $last = (int)($_SESSION['_last_activity'] ?? 0); + if ($last > 0 && (time() - $last) > SESSION_IDLE_SECONDS) { + $_SESSION = []; + session_regenerate_id(true); + $_SESSION['_idle_expired'] = true; + } else { + $_SESSION['_last_activity'] = time(); + } + } +} + +/** + * End the current session completely: server data, the session file and the + * browser cookie. Used by logout and by any flow that must force a new sign-in. + */ +if (!function_exists('session_end_completely')) { + function session_end_completely(): void { + if (session_status() !== PHP_SESSION_ACTIVE) return; + $_SESSION = []; + $p = session_get_cookie_params(); + setcookie(session_name(), '', [ + 'expires' => time() - 42000, + 'path' => $p['path'], + 'domain' => $p['domain'], + 'secure' => $p['secure'], + 'httponly' => $p['httponly'], + 'samesite' => $p['samesite'] ?? 'Lax', + ]); + session_destroy(); + } +} diff --git a/app/setting/api/engine/change_password.php b/app/setting/api/engine/change_password.php index 363dea0..4b6dcc2 100644 --- a/app/setting/api/engine/change_password.php +++ b/app/setting/api/engine/change_password.php @@ -1,5 +1,5 @@ diff --git a/app/setting/document_types.php b/app/setting/document_types.php index 0a6b4df..ff5a0f9 100644 --- a/app/setting/document_types.php +++ b/app/setting/document_types.php @@ -1,5 +1,5 @@ diff --git a/app/setting/gl_maintenance.php b/app/setting/gl_maintenance.php index 011cffb..79c470a 100644 --- a/app/setting/gl_maintenance.php +++ b/app/setting/gl_maintenance.php @@ -1,5 +1,5 @@ diff --git a/app/setting/profile.php b/app/setting/profile.php index 269fe6c..a5eaa1e 100644 --- a/app/setting/profile.php +++ b/app/setting/profile.php @@ -1,5 +1,5 @@ diff --git a/app/setting/smtp.php b/app/setting/smtp.php index 5ce9851..4d99154 100644 --- a/app/setting/smtp.php +++ b/app/setting/smtp.php @@ -1,5 +1,5 @@ diff --git a/app/setting/stock_maintenance.php b/app/setting/stock_maintenance.php index d0ea5e1..809b844 100644 --- a/app/setting/stock_maintenance.php +++ b/app/setting/stock_maintenance.php @@ -1,5 +1,5 @@ diff --git a/app/setting/system_config.php b/app/setting/system_config.php index 22a4f73..f5e7bcd 100644 --- a/app/setting/system_config.php +++ b/app/setting/system_config.php @@ -1,5 +1,5 @@ diff --git a/app/setting/users.php b/app/setting/users.php index 9b394bd..f130a69 100644 --- a/app/setting/users.php +++ b/app/setting/users.php @@ -1,5 +1,5 @@