Start every session through session.php; idle timeout, app access and auth status codes

This commit is contained in:
Thanakorn
2026-09-24 14:30:35 +07:00
parent 5cc43cff92
commit e579dd596c
273 changed files with 409 additions and 274 deletions
+11
View File
@@ -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.');
+37
View File
@@ -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;
}
+30 -4
View File
@@ -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));
}