Start every session through session.php; idle timeout, app access and auth status codes
This commit is contained in:
@@ -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.');
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* app_access.php — which app (WMS / Accounting) a script belongs to, and whether
|
||||
* the signed-in user's app_access allows it.
|
||||
*
|
||||
* app_access used to only choose which menus the topbar drew; a WMS-only user
|
||||
* could still open the accounting pages and call their APIs directly. db_auth.php
|
||||
* (API engines) and include_topbar.php (pages) now both enforce it through here.
|
||||
*/
|
||||
|
||||
// Accounting endpoints the WMS screens also call (master-data lookups, the
|
||||
// batch operation lock, and the GL panel on purchase invoices).
|
||||
const APP_ACCESS_SHARED_ACCOUNTING = [
|
||||
'accounting/api/engine/account.php',
|
||||
'accounting/api/engine/account_formula.php',
|
||||
'accounting/api/engine/department.php',
|
||||
'accounting/api/engine/acquire_op_lock.php',
|
||||
'accounting/api/engine/release_op_lock.php',
|
||||
'accounting/api/engine/get_gl_by_source.php',
|
||||
];
|
||||
|
||||
/** The app a script under app/ belongs to: 'accounting', or null for WMS/shared. */
|
||||
function app_access_app_for(string $script_name): ?string {
|
||||
$path = str_replace('\\', '/', $script_name);
|
||||
$pos = strpos($path, '/app/');
|
||||
if ($pos === false) return null;
|
||||
$rel = substr($path, $pos + 5);
|
||||
|
||||
if (in_array($rel, APP_ACCESS_SHARED_ACCOUNTING, true)) return null;
|
||||
if (preg_match('#^(accounting|ac_dashboard|revenue|expense|finance|journal)/#', $rel)) return 'accounting';
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether an app_access value ('wms', 'accounting', 'all') includes $app. */
|
||||
function app_access_allows(string $access, string $app): bool {
|
||||
return $access === 'all' || $access === $app;
|
||||
}
|
||||
@@ -38,6 +38,7 @@ set_exception_handler(function (Throwable $e) {
|
||||
require_once __DIR__."/../../config.php";
|
||||
require_once __DIR__."/../../dbconn.php";
|
||||
require_once __DIR__."/db_helpers.php";
|
||||
require_once __DIR__."/app_access.php";
|
||||
|
||||
if (!function_exists('require_role')) {
|
||||
function require_role(string $user_role, array $allowed): void {
|
||||
@@ -53,14 +54,14 @@ if(!empty($_SESSION["login_company_id"])){
|
||||
// CSRF Validation — add right at the top of the logged-in block
|
||||
if($_SERVER['REQUEST_METHOD'] === 'POST'){
|
||||
$csrf_token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if(empty($csrf_token) || $csrf_token !== $_SESSION['csrf_token']){
|
||||
if(empty($csrf_token) || !hash_equals((string)($_SESSION['csrf_token'] ?? ''), $csrf_token)){
|
||||
http_response_code(403);
|
||||
exit(json_encode(["message" => "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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user