Integrate cors_policy class to standardize CORS handling, refactor optionsRoute to use it, and add unit tests for CORS and Self-Serve Lane Access functionalities.

This commit is contained in:
Jeppe Bundgaard
2026-05-26 16:35:02 +02:00
parent 6ff6ce9b48
commit 78462f3ae4
18 changed files with 969 additions and 68 deletions
@@ -30,6 +30,8 @@ class moduleSelfServeRoute
{
use route_t;
private const CUSTOMER_SELFSERVE_PERMISSION = 'list_own_department_selfserve_vehicle_conditions';
public function run(): void
{
global /** @var response $response */
@@ -389,7 +391,6 @@ class moduleSelfServeRoute
/** Modules > Self Serve > Lane > Command */
$this->post('/modules/self-serve/lane/command', function () {
global $response;
self::requirePermission('modules_selfserve_lane_command_execute');
$selfserve = new selfserve();
// Get the request user
$user = (new authentication())->get_user();
@@ -412,35 +413,63 @@ class moduleSelfServeRoute
if ($command === null) {
$response->error("Invalid command: " . $commandParam);
}
$customer_number = $this->resolveEffectiveCustomerNumber();
$customer_number = $customer_number === null ? 0 : (int)$customer_number;
// Require permissions for specific commands
switch ($command) {
case selfserve_lane_command::START:
self::requirePermission('modules_selfserve_lane_command_execute_start');
$this->requireSelfServeLaneCommandPermission(
$lane,
$customer_number,
'modules_selfserve_lane_command_execute_start',
true
);
break;
case selfserve_lane_command::STOP:
self::requirePermission('modules_selfserve_lane_command_execute_stop');
$this->requireSelfServeLaneCommandPermission(
$lane,
$customer_number,
'modules_selfserve_lane_command_execute_stop',
true,
true
);
break;
case selfserve_lane_command::RESERVE:
self::requirePermission('modules_selfserve_lane_command_execute_reserve');
$this->requireSelfServeLaneCommandPermission(
$lane,
$customer_number,
'modules_selfserve_lane_command_execute_reserve',
false
);
break;
case selfserve_lane_command::RELEASE:
self::requirePermission('modules_selfserve_lane_command_execute_release');
$this->requireSelfServeLaneCommandPermission(
$lane,
$customer_number,
'modules_selfserve_lane_command_execute_release',
false
);
break;
case selfserve_lane_command::RESET:
self::requirePermission('modules_selfserve_lane_command_execute_reset');
$this->requireSelfServeLaneCommandPermission(
$lane,
$customer_number,
'modules_selfserve_lane_command_execute_reset',
false
);
break;
case selfserve_lane_command::OPEN_PROPERTY_ACCESS_GATE:
$this->requirePropertyGateCommandPermission(
'modules_selfserve_lane_command_execute_open_property_access_gate',
$lane,
(int)$user->customer_number->value()
$customer_number
);
break;
case selfserve_lane_command::OPEN_PROPERTY_EXIT_GATE:
$this->requirePropertyGateCommandPermission(
'modules_selfserve_lane_command_execute_open_property_exit_gate',
$lane,
(int)$user->customer_number->value()
$customer_number
);
break;
}
@@ -450,7 +479,7 @@ class moduleSelfServeRoute
$args = new \modules\selfserve\classes\selfserve_lane_command_arguments();
$args->setParameters([
...$this->getParametersAsArray(), // Pass all parameters
'customer_number' => (int)$user->customer_number->value(), // Get customer number from request user
'customer_number' => $customer_number, // Get customer number from request user
]);
$lane->execute($command, $args);
$response->success([
@@ -493,7 +522,6 @@ class moduleSelfServeRoute
/** Modules > Self Serve > Lane > Allowed services (derived from shown tasks) */
$this->post('/modules/self-serve/lane/services/allowed', function () {
global $response;
self::requirePermission('modules_selfserve_lane_services_set_allowed');
$selfserve = new selfserve();
// Validate parameters
self::requireParameters(['lane_id']);
@@ -516,6 +544,12 @@ class moduleSelfServeRoute
$task_ids = array_values(array_unique(array_map(fn($v) => (int)$v, $task_ids_param)));
// Build allowed services from provided tasks
$lane = $selfserve->lane($lane_id);
$customer_number = $this->resolveEffectiveCustomerNumber();
$this->requireSelfServeLaneAccess(
$lane,
$customer_number === null ? 0 : (int)$customer_number,
['modules_selfserve_lane_services_set_allowed']
);
$allowed_services = [];
foreach ($task_ids as $tid) {
if ($tid <= 0) continue;
@@ -835,7 +869,6 @@ class moduleSelfServeRoute
/** Modules > Self Serve > Lane > Relay > Enable MACHINE (manual, gated by allowed services) */
$this->post('/modules/self-serve/lane/relay/machine/enable', function () {
global $response;
self::requirePermission('modules_selfserve_lane_relay_enable_machine');
$selfserve = new selfserve();
// Validate parameters
self::requireParameters(['lane_id']);
@@ -848,6 +881,13 @@ class moduleSelfServeRoute
self::requireMinValue($duration, 1);
}
$lane = $selfserve->lane($lane_id);
$customer_number = $this->resolveEffectiveCustomerNumber();
$this->requireSelfServeLaneAccess(
$lane,
$customer_number === null ? 0 : (int)$customer_number,
['modules_selfserve_lane_relay_enable_machine'],
true
);
try {
$this->applyShellyTransportOverride($lane);
$lane->turnOnRelay(selfserve_lane_relay::MACHINE, $duration);
@@ -1271,9 +1311,87 @@ class moduleSelfServeRoute
$lane->setShellyTransportOverride($transport);
}
/**
* @param array<int,string> $permissions
*/
private function hasAllPermissions(array $permissions): bool
{
foreach ($permissions as $permission) {
if (!$this->hasPermission($permission)) {
return false;
}
}
return true;
}
/**
* @param array<int,string> $elevated_permissions
*/
private function requireSelfServeLaneAccess(
selfserve_lane $lane,
int $customer_number,
array $elevated_permissions,
bool $requires_active_wash = false,
bool $requires_operational_lane = true
): void {
if ($this->hasAllPermissions($elevated_permissions)) {
return;
}
if ($requires_active_wash) {
$customer_allowed = $this->canCustomerUseActiveSelfServeLane($lane, $customer_number)
&& (!$requires_operational_lane || $this->isLaneSelfServeOperationallyEnabled($lane));
} else {
$customer_allowed = $this->canCustomerUseSelfServeLane($lane, $customer_number);
}
if ($customer_allowed) {
return;
}
$this->emitForbidden([...$elevated_permissions, self::CUSTOMER_SELFSERVE_PERMISSION]);
}
private function requireSelfServeLaneCommandPermission(
selfserve_lane $lane,
int $customer_number,
string $command_permission,
bool $allow_customer_self_serve,
bool $requires_active_wash = false
): void {
$elevated_permissions = [
'modules_selfserve_lane_command_execute',
$command_permission,
];
if ($this->hasAllPermissions($elevated_permissions)) {
return;
}
if ($allow_customer_self_serve) {
$customer_allowed = $requires_active_wash
? $this->canCustomerUseActiveSelfServeLane($lane, $customer_number)
: $this->canCustomerUseSelfServeLane($lane, $customer_number);
if ($customer_allowed) {
return;
}
}
$this->emitForbidden(
$allow_customer_self_serve
? [...$elevated_permissions, self::CUSTOMER_SELFSERVE_PERMISSION]
: $elevated_permissions
);
}
private function requirePropertyGateCommandPermission(string $permission, selfserve_lane $lane, int $customer_number): void
{
if (self::hasPermission($permission)) {
$elevated_permissions = [
'modules_selfserve_lane_command_execute',
$permission,
];
if ($this->hasAllPermissions($elevated_permissions)) {
return;
}
@@ -1281,21 +1399,70 @@ class moduleSelfServeRoute
return;
}
self::requirePermission($permission);
$this->emitForbidden([...$elevated_permissions, self::CUSTOMER_SELFSERVE_PERMISSION]);
}
protected function canCustomerUseSelfServeLane(selfserve_lane $lane, int $customer_number): bool
{
return $customer_number > 0
&& $this->hasPermission(self::CUSTOMER_SELFSERVE_PERMISSION)
&& $this->isLaneSelfServeOperationallyEnabled($lane);
}
protected function canCustomerUseActiveSelfServeLane(selfserve_lane $lane, int $customer_number): bool
{
if ($customer_number <= 0 || !$this->hasPermission(self::CUSTOMER_SELFSERVE_PERMISSION)) {
return false;
}
try {
if ((int)$lane->getCustomerNumber() === $customer_number) {
return true;
}
} catch (\Throwable) {
// Fall back to the persisted session lookup below.
}
$department_id = $this->departmentIdForLane($lane);
return $department_id > 0
&& $this->customerHasActiveSelfServeWashInDepartment($department_id, $customer_number);
}
protected function canCustomerUsePropertyGateForLane(selfserve_lane $lane, int $customer_number): bool
{
if ($customer_number <= 0) {
return $this->canCustomerUseActiveSelfServeLane($lane, $customer_number);
}
protected function isLaneSelfServeOperationallyEnabled(selfserve_lane $lane): bool
{
try {
if (empty($lane->department_lane) || !$lane->department_lane->isSelfServeEnabled()) {
return false;
}
} catch (\Throwable) {
return false;
}
$department_id = (int)$lane->department_lane?->department?->value();
$department_id = $this->departmentIdForLane($lane);
if ($department_id <= 0) {
return false;
}
return $this->customerHasActiveSelfServeWashInDepartment($department_id, $customer_number);
try {
$department = (new departments_o())->select($department_id);
return $department->exists() && $department->getSelfServeEnabled();
} catch (\Throwable) {
return false;
}
}
protected function departmentIdForLane(selfserve_lane $lane): int
{
try {
return (int)$lane->department_lane?->department?->value();
} catch (\Throwable) {
return 0;
}
}
protected function customerHasActiveSelfServeWashInDepartment(int $department_id, int $customer_number): bool