touchUser($user, $token); } catch (\Throwable) { // Session tracking must never block authentication resolution. } } private function touchResolvedSubuserSession(subusers_o $subuser, string $token, int|null $customerNumberContext = null): void { if (trim($token) === '') { return; } try { (new system_session_activity_tracker())->touchSubuser($subuser, $token, $customerNumberContext); } catch (\Throwable) { // Session tracking must never block authentication resolution. } } /** * @throws Exception */ public function authenticate(int $customer_number, string $password): bool { // Get the customer from the database $customer = (new users_o())->getUserByCustomerNumber($customer_number); // Check if the customer exists if (!$customer->exists()) { return false; } // Check if the customer has a password if (!$customer->hasPassword()) { // If the customer doesn't have a password, we can't authenticate them, so we return false return false; } // Check if the password is correct if (!$this->match_passwords($password, $customer->getPassword())) { return false; } return true; } public function is_2fa_enabled(users_o|subusers_o $user): bool { return $user->isTwoFactorEnabled(); } public function create_2fa_token(int $id, string $type): string { $principalType = $type === '2FA_VERIFICATION_SUBUSER' ? 'subuser' : 'customer'; if (account_deletion_service::principalIsBlocked($principalType, $id)) { throw new Exception('Account unavailable'); } // Create a temporary 2FA token $token = bin2hex(random_bytes(32)); (new tokens_o())->create($id, $token, $type); return $token; } public function verify_2fa_code(users_o|subusers_o $user, string $code): bool { $secret = $user->getTwoFactorSecret(); if (!$secret) { return false; } return (new totp())->verifyCode($secret, $code); } public function match_passwords($password, $hash): bool { // Compare the password with the hash return password_verify($password, $hash); } public function create_token(int $customer_number): string { // Create a token $token = bin2hex(random_bytes(32)); // Resolve the user by customer number and ensure it exists to avoid accessing an uninitialized typed property $user = (new users_o())->getUserByCustomerNumber($customer_number); if (!$user->exists()) { throw new \Exception('User not found for customer number: ' . $customer_number); } $user_id = $user->id; if (account_deletion_service::principalIsBlocked('customer', (int)$user_id)) { throw new Exception('Account unavailable'); } // Save the token in the database (new tokens_o())->create($user_id, $token, 'AUTH_TOKEN'); return $token; } public function create_token_by_user_id(int $user_id): string { if (account_deletion_service::principalIsBlocked('customer', $user_id)) { throw new Exception('Account unavailable'); } // Create a token $token = bin2hex(random_bytes(32)); // Save the token in the database (new tokens_o())->create($user_id, $token, 'AUTH_TOKEN'); return $token; } public function create_employee_token(int $employee_id): string { if (account_deletion_service::principalIsBlocked('customer', $employee_id)) { throw new Exception('Account unavailable'); } // Create a token $token = bin2hex(random_bytes(32)); // Save the token in the database (new tokens_o())->create($employee_id, $token, 'AUTH_TOKEN'); return $token; } public function create_impersonation_token(int $target_user_id, int $actor_user_id): string { if ($actor_user_id <= 0 || account_deletion_service::principalIsBlocked('customer', $target_user_id)) { throw new Exception('Account unavailable'); } $token = bin2hex(random_bytes(32)); (new tokens_o())->create($target_user_id, $token, 'AUTH_TOKEN_IMPERSONATION:' . $actor_user_id); return $token; } public function validate_token(string $token): bool { // First: try validating as a classic user auth token try { $dbToken = (new tokens_o())->getToken($token); if ($dbToken && $dbToken->id && $this->isClassicAuthTokenType((string)$dbToken->type->value())) { return !account_deletion_service::principalIsBlocked( 'customer', (int)$dbToken->user_id->value() ); } } catch (Exception) { // Ignore and continue to subuser session validation } // Fallback: try validating as a subuser session token $subuser = (new subusers_o())->getSubuserBySessionToken($token); if ($subuser !== null) { return !account_deletion_service::principalIsBlocked('subuser', (int)$subuser->id); } return false; } /** * @throws Exception */ public function get_user(): users_o|false { /** * Get the user from the token */ // Get the token from the headers $headers = getallheaders(); if (!isset($headers['Authorization'])) { return false; } $rawToken = $headers['Authorization']; // Strip the Bearer prefix $rawToken = str_replace('Bearer ', '', $rawToken); // Get the token from the database try { $token = (new tokens_o())->getToken($rawToken); } catch (Exception) { return false; } // Check if the token exists if (!$token->id) { return false; } if (!$this->isClassicAuthTokenType((string)$token->type->value())) { return false; } if (account_deletion_service::principalIsBlocked('customer', (int)$token->user_id->value())) { return false; } // Get the user from the database $user = (new users_o())->getUserById($token->user_id->value()); $this->touchResolvedUserSession($user, $rawToken); return $user; } private function isClassicAuthTokenType(string $type): bool { return $type === 'AUTH_TOKEN' || str_starts_with($type, 'AUTH_TOKEN_IMPERSONATION:'); } public function get_plate_scanner(): plate_scanners_o|false { // Get the token from the headers $headers = getallheaders(); $tmp = json_decode(file_get_contents('php://input'), true); if (!is_array($tmp)) { $tmp = []; } if (!isset($headers['Authorization']) && !isset($_GET['token']) && !isset($_POST['token']) && !isset($tmp['token'])) { return false; } $token = $_GET['token'] ?? $headers['Authorization'] ?? $tmp['token'] ?? $_POST['token']; // Strip the Bearer prefix (If the token is from the headers) if (isset($headers['Authorization'])) { $token = str_replace('Bearer ', '', $token); } // Get the token from the database $token = (new plate_scanners_o())->getPlateScannerByApiKey($token); // Check if the token exists if (!isset($token->id)) { return false; } // Get the plate scanner from the database return $token; } /** * @throws Exception */ public function get_subuser(): subusers_o|false { // Try to resolve a subuser from an incoming bearer token or explicit token parameter $headers = getallheaders(); $tmp = json_decode(file_get_contents('php://input'), true); if (!is_array($tmp)) { $tmp = []; } if (!isset($headers['Authorization']) && !isset($_GET['token']) && !isset($_POST['token']) && !isset($tmp['token'])) { return false; } $token = $_GET['token'] ?? $headers['Authorization'] ?? $tmp['token'] ?? $_POST['token']; // Strip the Bearer prefix (If the token is from the headers) if (isset($headers['Authorization'])) { $token = str_replace('Bearer ', '', $token); } // Resolve subuser session from cache $subuser = (new subusers_o())->getSubuserBySessionToken($token); if ($subuser === null) { return false; } if (account_deletion_service::principalIsBlocked('subuser', (int)$subuser->id)) { return false; } $customerNumberContext = null; if (isset($headers['X-Customer-Number'])) { $customerNumberContext = (int)$headers['X-Customer-Number']; } $this->touchResolvedSubuserSession($subuser, $token, $customerNumberContext); return $subuser; } public function hash_password($password): string { // Hash the password return password_hash($password, PASSWORD_DEFAULT); } public function authenticateEmployee(int $user_id, string $password): bool { // Get the employee from the database $employee = (new users_o())->getUserById($user_id); // Check if the employee exists if (!$employee->exists()) { return false; } // Check if the password is correct if (!$this->match_passwords($password, $employee->getPassword())) { return false; } return true; } public function get_subuser_customer_number_target(): int|false { /** * Decode the headers */ $headers = getallheaders(); if (!isset($headers['X-Customer-Number'])) { return false; } return (int)$headers['X-Customer-Number']; } }