login: block concurrent login, single-factor auth for staff/viewer
This commit is contained in:
@@ -821,6 +821,14 @@ function ajax_request(options) {
|
||||
throw xhr;
|
||||
}
|
||||
|
||||
// Session displaced — another login took over this account
|
||||
if (xhr?.status === 401 && xhr?.responseJSON?.code === 'signed_elsewhere') {
|
||||
bootbox.alert('<strong>Signed in from another device.</strong><br>Your session has been ended because this account was signed in elsewhere.', function() {
|
||||
window.location.href = server_url + '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.');
|
||||
|
||||
@@ -44,6 +44,7 @@ class ReportManager
|
||||
return 'td_stock_' . $warehouse_id;
|
||||
}
|
||||
|
||||
|
||||
private function resolveWarehouseTable(int $warehouse_id): ?string
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
|
||||
@@ -292,10 +292,10 @@ class StockManager {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `$table`
|
||||
(uuid, company_id, `date`, product_sku, `in`, price, zone, aisle, rack,
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number, status)
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number, status, updated_at)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity, :price, :zone, :aisle, :rack,
|
||||
:contact_id, :description, :log, 'in', :lot_number, :serial_number, 0)"
|
||||
:contact_id, :description, :log, 'in', :lot_number, :serial_number, 0, NOW())"
|
||||
)->execute([
|
||||
':uuid' => $uuid,
|
||||
':company_id' => $this->company_id,
|
||||
@@ -427,10 +427,10 @@ class StockManager {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `$table`
|
||||
(uuid, company_id, `date`, product_sku, `out`, zone, aisle, rack,
|
||||
contact_id, `description`, `log`, `type`, ref_id, lot_number, serial_number, status)
|
||||
contact_id, `description`, `log`, `type`, ref_id, lot_number, serial_number, status, updated_at)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack,
|
||||
:contact_id, :description, :log, 'out', :ref_id, :lot_number, :serial_number, 0)"
|
||||
:contact_id, :description, :log, 'out', :ref_id, :lot_number, :serial_number, 0, NOW())"
|
||||
)->execute([
|
||||
':uuid' => $uuid,
|
||||
':company_id' => $this->company_id,
|
||||
@@ -604,11 +604,11 @@ class StockManager {
|
||||
"INSERT INTO `$from_table`
|
||||
(uuid, company_id, `date`, product_sku, `out`,
|
||||
ref_warehouse, zone, aisle, rack,
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number, status)
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number, status, updated_at)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity,
|
||||
:ref_warehouse, :zone, :aisle, :rack,
|
||||
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number, 0)"
|
||||
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number, 0, NOW())"
|
||||
)->execute([
|
||||
':uuid' => $uuid,
|
||||
':company_id' => $this->company_id,
|
||||
@@ -632,11 +632,11 @@ class StockManager {
|
||||
"INSERT INTO `$to_table`
|
||||
(uuid, company_id, `date`, product_sku, `in`,
|
||||
ref_warehouse, ref_id, zone, aisle, rack,
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number, status)
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number, status, updated_at)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity,
|
||||
:ref_warehouse, :ref_id, :zone, :aisle, :rack,
|
||||
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number, 0)"
|
||||
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number, 0, NOW())"
|
||||
)->execute([
|
||||
':uuid' => $uuid,
|
||||
':company_id' => $this->company_id,
|
||||
@@ -715,7 +715,7 @@ class StockManager {
|
||||
|
||||
// ── Approve this row ──────────────────────────────────────────────
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `{$table}` SET status = 1 WHERE id = :id AND company_id = :company_id"
|
||||
"UPDATE `{$table}` SET status = 1, updated_at = NOW() WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([':id' => $id, ':company_id' => $this->company_id]);
|
||||
|
||||
// ── Rack state + balance ──────────────────────────────────────────
|
||||
@@ -823,7 +823,7 @@ class StockManager {
|
||||
// Approve outbound row
|
||||
if ($from_row && (int)$from_row['status'] === 0) {
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `{$from_table}` SET status = 1
|
||||
"UPDATE `{$from_table}` SET status = 1, updated_at = NOW()
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([':id' => $from_row['id'], ':company_id' => $this->company_id]);
|
||||
}
|
||||
@@ -831,7 +831,7 @@ class StockManager {
|
||||
// Approve inbound row
|
||||
if ($inbound_row && (int)$inbound_row['status'] === 0) {
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `{$paired_table}` SET status = 1
|
||||
"UPDATE `{$paired_table}` SET status = 1, updated_at = NOW()
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([':id' => $inbound_row['id'], ':company_id' => $this->company_id]);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,19 @@ if(!empty($_SESSION["login_company_id"])){
|
||||
$user_role = $map[0]['role'] ?? 'viewer';
|
||||
$_SESSION['login_role'] = $user_role;
|
||||
|
||||
// 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'])) {
|
||||
$sth = $pdo1->prepare("SELECT session_token FROM user WHERE user_id = :uid LIMIT 1");
|
||||
$sth->execute([':uid' => $_SESSION['login_user_id']]);
|
||||
$db_token = $sth->fetchColumn();
|
||||
if ($db_token !== $_SESSION['session_token']) {
|
||||
session_destroy();
|
||||
http_response_code(401);
|
||||
exit(json_encode(['success' => 0, 'message' => 'You have been signed in from another device.', 'code' => 'signed_elsewhere']));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Fail closed — reject any request that arrives without an authenticated session
|
||||
|
||||
Reference in New Issue
Block a user