Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe3719530a | ||
|
|
603f497bef | ||
|
|
ee16db8ecc | ||
|
|
c5c33d3cf7 | ||
|
|
da05c5adb7 | ||
|
|
707cf67d5c | ||
|
|
09fa186028 | ||
|
|
5e6b340f8c | ||
|
|
04e47a2e6d | ||
|
|
572f5027d6 | ||
|
|
235e0268c2 | ||
|
|
65d639853b | ||
|
|
e856bbffec | ||
|
|
3ee5b789ce | ||
|
|
7f5722ff75 | ||
|
|
50b596af39 | ||
|
|
af06c4d81e | ||
|
|
41ed692299 | ||
|
|
31214f0af0 | ||
|
|
cd0e0f0e61 | ||
|
|
0db6b5269d | ||
|
|
3fb1eb9644 | ||
|
|
76dfcd70d1 | ||
|
|
b13abe0d30 |
@@ -20,6 +20,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }} # Use PR head when available, otherwise the pushed SHA.
|
ref: ${{ github.event.pull_request.head.sha || github.sha }} # Use PR head when available, otherwise the pushed SHA.
|
||||||
fetch-depth: 0 # a full history is required for pull request analysis
|
fetch-depth: 0 # a full history is required for pull request analysis
|
||||||
|
- name: Mark repository as safe for Git
|
||||||
|
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||||
- name: Prepare Qodana cache directories
|
- name: Prepare Qodana cache directories
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "${RUNNER_TEMP}/qodana/caches"
|
mkdir -p "${RUNNER_TEMP}/qodana/caches"
|
||||||
@@ -44,8 +46,6 @@ jobs:
|
|||||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||||
QODANA_ENDPOINT: 'https://qodana.cloud'
|
QODANA_ENDPOINT: 'https://qodana.cloud'
|
||||||
|
|
||||||
- name: 'Qodana Scan (without cloud upload)'
|
- name: 'Skip Qodana Scan (missing cloud token)'
|
||||||
if: ${{ steps.qodana-token.outputs.present != 'true' }}
|
if: ${{ steps.qodana-token.outputs.present != 'true' }}
|
||||||
uses: JetBrains/qodana-action@v2026.1
|
run: echo "Skipping Qodana because QODANA_TOKEN is not configured for this repository."
|
||||||
with:
|
|
||||||
pr-mode: false
|
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cp .github/ci.env .env
|
cp .github/ci.env .env
|
||||||
cp .github/ci.env.staging .env.staging
|
cp .github/ci.env.staging .env.staging
|
||||||
|
printf '\nEDGE_PUBLIC_BROKER_URL=http://edge-broker:4300\n' >> .env
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -151,7 +152,7 @@ jobs:
|
|||||||
node-version: 22
|
node-version: 22
|
||||||
|
|
||||||
- name: Boot local stack
|
- name: Boot local stack
|
||||||
run: docker compose -f docker-compose.yml -f .github/docker-compose.ci.yml up -d traefik redis mysql-debug edge-broker php1 caddy
|
run: docker compose -f docker-compose.yml -f .github/docker-compose.ci.yml up -d traefik redis mysql-debug edge-broker php1 php2 php3 php4 php5 caddy
|
||||||
|
|
||||||
- name: Sync PHP app checkout
|
- name: Sync PHP app checkout
|
||||||
run: >
|
run: >
|
||||||
@@ -240,6 +241,7 @@ jobs:
|
|||||||
-e EDGE_GATEWAY_E2E_BASE_URL="http://caddy" \
|
-e EDGE_GATEWAY_E2E_BASE_URL="http://caddy" \
|
||||||
-e EDGE_GATEWAY_E2E_COMPOSE_PROJECT="$compose_project" \
|
-e EDGE_GATEWAY_E2E_COMPOSE_PROJECT="$compose_project" \
|
||||||
-e EDGE_GATEWAY_E2E_COPY_CONFIG="true" \
|
-e EDGE_GATEWAY_E2E_COPY_CONFIG="true" \
|
||||||
|
-e EDGE_GATEWAY_E2E_SKIP_COMPOSE_UP="true" \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-w /workspace \
|
-w /workspace \
|
||||||
node:22-alpine \
|
node:22-alpine \
|
||||||
@@ -266,6 +268,11 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
test -n "$RELEASE_MANAGER_GATE_TOKEN" || (echo "RELEASE_MANAGER_GATE_TOKEN is required" >&2; exit 1)
|
test -n "$RELEASE_MANAGER_GATE_TOKEN" || (echo "RELEASE_MANAGER_GATE_TOKEN is required" >&2; exit 1)
|
||||||
curl --fail --show-error --silent \
|
curl --fail --show-error --silent \
|
||||||
|
--connect-timeout 10 \
|
||||||
|
--retry 5 \
|
||||||
|
--retry-all-errors \
|
||||||
|
--retry-delay 15 \
|
||||||
|
--retry-max-time 300 \
|
||||||
-X POST "$RELEASE_MANAGER_GATE_URL" \
|
-X POST "$RELEASE_MANAGER_GATE_URL" \
|
||||||
-H "Authorization: Bearer $RELEASE_MANAGER_GATE_TOKEN" \
|
-H "Authorization: Bearer $RELEASE_MANAGER_GATE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { promisify } from "node:util";
|
|||||||
import { DEFAULT_CONFIG_FILE_NAME, DEFAULT_HOST_API_URL } from "./test-gateway.mjs";
|
import { DEFAULT_CONFIG_FILE_NAME, DEFAULT_HOST_API_URL } from "./test-gateway.mjs";
|
||||||
|
|
||||||
const execFile = promisify(execFileCallback);
|
const execFile = promisify(execFileCallback);
|
||||||
const COMPOSE_SERVICES = ["traefik", "redis", "mysql-debug", "edge-broker", "php1", "caddy"];
|
const COMPOSE_SERVICES = ["traefik", "redis", "mysql-debug", "edge-broker", "php1", "php2", "php3", "php4", "php5", "caddy"];
|
||||||
|
|
||||||
function composeArgs(projectName, args) {
|
function composeArgs(projectName, args) {
|
||||||
return ["compose", "-p", projectName, ...args];
|
return ["compose", "-p", projectName, ...args];
|
||||||
@@ -514,6 +514,10 @@ function shouldCopyGatewayConfig() {
|
|||||||
return /^(1|true|yes)$/i.test(String(process.env.EDGE_GATEWAY_E2E_COPY_CONFIG || "").trim());
|
return /^(1|true|yes)$/i.test(String(process.env.EDGE_GATEWAY_E2E_COPY_CONFIG || "").trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldSkipComposeUp() {
|
||||||
|
return /^(1|true|yes)$/i.test(String(process.env.EDGE_GATEWAY_E2E_SKIP_COMPOSE_UP || "").trim());
|
||||||
|
}
|
||||||
|
|
||||||
function collectMessages(rows) {
|
function collectMessages(rows) {
|
||||||
return Array.isArray(rows)
|
return Array.isArray(rows)
|
||||||
? rows
|
? rows
|
||||||
@@ -570,7 +574,9 @@ async function main() {
|
|||||||
let runnerNetworkAttached = false;
|
let runnerNetworkAttached = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureComposeServices(rootDir, composeProject);
|
if (!shouldSkipComposeUp()) {
|
||||||
|
await ensureComposeServices(rootDir, composeProject);
|
||||||
|
}
|
||||||
runnerNetworkAttached = await connectCurrentContainerToComposeNetwork(rootDir, composeProject);
|
runnerNetworkAttached = await connectCurrentContainerToComposeNetwork(rootDir, composeProject);
|
||||||
baseUrl = await waitForApiReady(baseUrl, rootDir, composeProject, runnerNetworkAttached);
|
baseUrl = await waitForApiReady(baseUrl, rootDir, composeProject, runnerNetworkAttached);
|
||||||
process.stdout.write(`Using API base URL ${baseUrl}\n`);
|
process.stdout.write(`Using API base URL ${baseUrl}\n`);
|
||||||
|
|||||||
@@ -464,6 +464,7 @@ trait selfserve_lane_command_t
|
|||||||
* @param selfserve_lane_command_arguments $arguments The arguments for the command
|
* @param selfserve_lane_command_arguments $arguments The arguments for the command
|
||||||
* @return selfserve_lane|selfserve_lane_command_t
|
* @return selfserve_lane|selfserve_lane_command_t
|
||||||
* @throws Exception If the command cannot be executed
|
* @throws Exception If the command cannot be executed
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function execute(selfserve_lane_command $command, selfserve_lane_command_arguments $arguments): self
|
public function execute(selfserve_lane_command $command, selfserve_lane_command_arguments $arguments): self
|
||||||
{
|
{
|
||||||
@@ -517,6 +518,7 @@ trait selfserve_lane_command_t
|
|||||||
// Open the entrance port before marking the lane occupied. Gateway timeouts are
|
// Open the entrance port before marking the lane occupied. Gateway timeouts are
|
||||||
// ambiguous because the relay may already have received the pulse.
|
// ambiguous because the relay may already have received the pulse.
|
||||||
$this->openEntrancePortForWashStart();
|
$this->openEntrancePortForWashStart();
|
||||||
|
$this->turnOnCleanerRelayForWashStart();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->setCustomerNumber($previous_customer_number);
|
$this->setCustomerNumber($previous_customer_number);
|
||||||
$this->setLicensePlate($previous_license_plate);
|
$this->setLicensePlate($previous_license_plate);
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ trait selfserve_lane_relay_controller_t
|
|||||||
*/
|
*/
|
||||||
public function setMachineRelayStatusHard(bool $on): bool
|
public function setMachineRelayStatusHard(bool $on): bool
|
||||||
{
|
{
|
||||||
return $this->setRelayStatusHard(selfserve_lane_relay::MACHINE, $on);
|
return $this->setRelayStatusHard(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, $on);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -414,11 +414,16 @@ class order_bookings_o extends db
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$orderItems = new order_items_o();
|
$orderItems = new order_items_o();
|
||||||
|
$itemNotes = isset($item['notes']) && trim((string)$item['notes']) !== ''
|
||||||
|
? (string)$item['notes']
|
||||||
|
: ((string)($this->note->value() ?? '') ?: null);
|
||||||
$orderItems->addItemToOrder(
|
$orderItems->addItemToOrder(
|
||||||
(int)$order->id,
|
(int)$order->id,
|
||||||
(int)$item['id'],
|
(int)$item['id'],
|
||||||
(int)$user_id,
|
(int)$user_id,
|
||||||
(int)$item['quantity'],
|
(int)$item['quantity'],
|
||||||
|
null,
|
||||||
|
$itemNotes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ FROM ${BASE_IMAGE}
|
|||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
apt-get update; \
|
apt-get update; \
|
||||||
apt-get install -y --no-install-recommends bash ca-certificates curl docker.io docker-compose; \
|
apt-get install -y --no-install-recommends bash ca-certificates curl docker.io docker-compose libcurl4-openssl-dev libsqlite3-dev; \
|
||||||
rm -rf /var/lib/apt/lists/*; \
|
docker-php-ext-install -j"$(nproc)" curl sqlite3 pdo_sqlite; \
|
||||||
php -r 'foreach (["curl", "sqlite3"] as $extension) { if (!extension_loaded($extension)) { fwrite(STDERR, "Missing PHP extension: {$extension}\n"); exit(1); } }'
|
php -r 'foreach (["curl", "sqlite3"] as $extension) { if (!extension_loaded($extension)) { fwrite(STDERR, "Missing PHP extension: {$extension}\n"); exit(1); } }'; \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY auto-updater.php /usr/local/bin/auto-updater.php
|
COPY auto-updater.php /usr/local/bin/auto-updater.php
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ ARG BASE_IMAGE=php:8.2-cli-bookworm
|
|||||||
FROM ${BASE_IMAGE}
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
php -r 'foreach (["curl", "sqlite3"] as $extension) { if (!extension_loaded($extension)) { fwrite(STDERR, "Missing PHP extension: {$extension}\n"); exit(1); } }'
|
apt-get update; \
|
||||||
|
apt-get install -y --no-install-recommends libcurl4-openssl-dev libsqlite3-dev; \
|
||||||
|
docker-php-ext-install -j"$(nproc)" curl sqlite3 pdo_sqlite; \
|
||||||
|
php -r 'foreach (["curl", "sqlite3"] as $extension) { if (!extension_loaded($extension)) { fwrite(STDERR, "Missing PHP extension: {$extension}\n"); exit(1); } }'; \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /opt/truckwash-edge-agent
|
WORKDIR /opt/truckwash-edge-agent
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ ARG BASE_IMAGE=php:8.2-cli-bookworm
|
|||||||
FROM ${BASE_IMAGE}
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
php -r 'foreach (["curl", "sqlite3"] as $extension) { if (!extension_loaded($extension)) { fwrite(STDERR, "Missing PHP extension: {$extension}\n"); exit(1); } }'
|
apt-get update; \
|
||||||
|
apt-get install -y --no-install-recommends libcurl4-openssl-dev libsqlite3-dev; \
|
||||||
|
docker-php-ext-install -j"$(nproc)" curl sqlite3 pdo_sqlite; \
|
||||||
|
php -r 'foreach (["curl", "sqlite3"] as $extension) { if (!extension_loaded($extension)) { fwrite(STDERR, "Missing PHP extension: {$extension}\n"); exit(1); } }'; \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /opt/truckwash-edge-agent
|
WORKDIR /opt/truckwash-edge-agent
|
||||||
|
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ class moduleSelfServeRoute
|
|||||||
$lane,
|
$lane,
|
||||||
$customer_number,
|
$customer_number,
|
||||||
'modules_selfserve_lane_command_execute_start',
|
'modules_selfserve_lane_command_execute_start',
|
||||||
true
|
false
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case selfserve_lane_command::STOP:
|
case selfserve_lane_command::STOP:
|
||||||
@@ -601,11 +601,7 @@ class moduleSelfServeRoute
|
|||||||
// Build allowed services from provided tasks
|
// Build allowed services from provided tasks
|
||||||
$lane = $selfserve->lane($lane_id);
|
$lane = $selfserve->lane($lane_id);
|
||||||
$customer_number = $this->resolveEffectiveCustomerNumber();
|
$customer_number = $this->resolveEffectiveCustomerNumber();
|
||||||
$this->requireSelfServeLaneAccess(
|
self::requirePermission('modules_selfserve_lane_services_set_allowed');
|
||||||
$lane,
|
|
||||||
$customer_number === null ? 0 : (int)$customer_number,
|
|
||||||
['modules_selfserve_lane_services_set_allowed']
|
|
||||||
);
|
|
||||||
$allowed_services = [];
|
$allowed_services = [];
|
||||||
foreach ($task_ids as $tid) {
|
foreach ($task_ids as $tid) {
|
||||||
if ($tid <= 0) continue;
|
if ($tid <= 0) continue;
|
||||||
@@ -938,12 +934,7 @@ class moduleSelfServeRoute
|
|||||||
}
|
}
|
||||||
$lane = $selfserve->lane($lane_id);
|
$lane = $selfserve->lane($lane_id);
|
||||||
$customer_number = $this->resolveEffectiveCustomerNumber();
|
$customer_number = $this->resolveEffectiveCustomerNumber();
|
||||||
$this->requireSelfServeLaneAccess(
|
self::requirePermission('modules_selfserve_lane_relay_enable_machine');
|
||||||
$lane,
|
|
||||||
$customer_number === null ? 0 : (int)$customer_number,
|
|
||||||
['modules_selfserve_lane_relay_enable_machine'],
|
|
||||||
true
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
$this->applyShellyTransportOverride($lane);
|
$this->applyShellyTransportOverride($lane);
|
||||||
$lane->turnOnRelay(selfserve_lane_relay::MACHINE, $duration);
|
$lane->turnOnRelay(selfserve_lane_relay::MACHINE, $duration);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class superuserReplicationRoute
|
|||||||
$this->get('/superuser/replication', function () {
|
$this->get('/superuser/replication', function () {
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
$this->requirePermission('superuser_replication_view');
|
$this->requireClassicSuperuserPermission('superuser_replication_view');
|
||||||
$refresh = $this->toBool($this->getParameter('refresh'), false);
|
$refresh = $this->toBool($this->getParameter('refresh'), false);
|
||||||
$response->success((new replication_manager())->summary($refresh));
|
$response->success((new replication_manager())->summary($refresh));
|
||||||
}, [
|
}, [
|
||||||
@@ -26,7 +26,7 @@ class superuserReplicationRoute
|
|||||||
$this->post('/superuser/replication/databases', function () {
|
$this->post('/superuser/replication/databases', function () {
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
$this->requirePermission('superuser_replication_manage');
|
$this->requireClassicSuperuserPermission('superuser_replication_manage');
|
||||||
$host = (new replication_manager())->addHost('database', $this->getParametersAsArray(), $this->actorUserId());
|
$host = (new replication_manager())->addHost('database', $this->getParametersAsArray(), $this->actorUserId());
|
||||||
$response->success($host, 201);
|
$response->success($host, 201);
|
||||||
}, [
|
}, [
|
||||||
@@ -36,7 +36,7 @@ class superuserReplicationRoute
|
|||||||
$this->post('/superuser/replication/redis', function () {
|
$this->post('/superuser/replication/redis', function () {
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
$this->requirePermission('superuser_replication_manage');
|
$this->requireClassicSuperuserPermission('superuser_replication_manage');
|
||||||
$host = (new replication_manager())->addHost('redis', $this->getParametersAsArray(), $this->actorUserId());
|
$host = (new replication_manager())->addHost('redis', $this->getParametersAsArray(), $this->actorUserId());
|
||||||
$response->success($host, 201);
|
$response->success($host, 201);
|
||||||
}, [
|
}, [
|
||||||
@@ -46,7 +46,7 @@ class superuserReplicationRoute
|
|||||||
$this->post('/superuser/replication/minio', function () {
|
$this->post('/superuser/replication/minio', function () {
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
$this->requirePermission('superuser_replication_manage');
|
$this->requireClassicSuperuserPermission('superuser_replication_manage');
|
||||||
$host = (new replication_manager())->addHost('minio', $this->getParametersAsArray(), $this->actorUserId());
|
$host = (new replication_manager())->addHost('minio', $this->getParametersAsArray(), $this->actorUserId());
|
||||||
$response->success($host, 201);
|
$response->success($host, 201);
|
||||||
}, [
|
}, [
|
||||||
@@ -56,7 +56,7 @@ class superuserReplicationRoute
|
|||||||
$this->post('/superuser/replication/compose-template', function () {
|
$this->post('/superuser/replication/compose-template', function () {
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
$this->requirePermission('superuser_replication_manage');
|
$this->requireClassicSuperuserPermission('superuser_replication_manage');
|
||||||
$response->success(replication_manager::composeTemplate($this->getParametersAsArray()));
|
$response->success(replication_manager::composeTemplate($this->getParametersAsArray()));
|
||||||
}, [
|
}, [
|
||||||
'superuser_replication_manage' => 'Generate Docker Compose templates for replication-ready database, Redis, and MinIO hosts',
|
'superuser_replication_manage' => 'Generate Docker Compose templates for replication-ready database, Redis, and MinIO hosts',
|
||||||
@@ -65,7 +65,7 @@ class superuserReplicationRoute
|
|||||||
$this->post('/superuser/replication/test-credentials', function () {
|
$this->post('/superuser/replication/test-credentials', function () {
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
$this->requirePermission('superuser_replication_manage');
|
$this->requireClassicSuperuserPermission('superuser_replication_manage');
|
||||||
$parameters = $this->getParametersAsArray();
|
$parameters = $this->getParametersAsArray();
|
||||||
$response->success((new replication_manager())->testCredentials(
|
$response->success((new replication_manager())->testCredentials(
|
||||||
(string)($parameters['kind'] ?? ''),
|
(string)($parameters['kind'] ?? ''),
|
||||||
@@ -78,7 +78,7 @@ class superuserReplicationRoute
|
|||||||
$this->post('/superuser/replication/{kind}/{id}/test', function () {
|
$this->post('/superuser/replication/{kind}/{id}/test', function () {
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
$this->requirePermission('superuser_replication_manage');
|
$this->requireClassicSuperuserPermission('superuser_replication_manage');
|
||||||
$response->success((new replication_manager())->testHost(
|
$response->success((new replication_manager())->testHost(
|
||||||
(string)$this->fromRoute('kind'),
|
(string)$this->fromRoute('kind'),
|
||||||
$this->routeId(),
|
$this->routeId(),
|
||||||
@@ -91,7 +91,7 @@ class superuserReplicationRoute
|
|||||||
$this->post('/superuser/replication/{kind}/{id}/provision', function () {
|
$this->post('/superuser/replication/{kind}/{id}/provision', function () {
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
$this->requirePermission('superuser_replication_manage');
|
$this->requireClassicSuperuserPermission('superuser_replication_manage');
|
||||||
try {
|
try {
|
||||||
$result = (new replication_manager())->provisionHost(
|
$result = (new replication_manager())->provisionHost(
|
||||||
(string)$this->fromRoute('kind'),
|
(string)$this->fromRoute('kind'),
|
||||||
@@ -113,7 +113,7 @@ class superuserReplicationRoute
|
|||||||
$this->post('/superuser/replication/{kind}/{id}/promote', function () {
|
$this->post('/superuser/replication/{kind}/{id}/promote', function () {
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
$this->requirePermission('superuser_replication_promote');
|
$this->requireClassicSuperuserPermission('superuser_replication_promote');
|
||||||
try {
|
try {
|
||||||
$response->success((new replication_manager())->promoteHost(
|
$response->success((new replication_manager())->promoteHost(
|
||||||
(string)$this->fromRoute('kind'),
|
(string)$this->fromRoute('kind'),
|
||||||
@@ -130,7 +130,7 @@ class superuserReplicationRoute
|
|||||||
$this->patch('/superuser/replication/{kind}/{id}', function () {
|
$this->patch('/superuser/replication/{kind}/{id}', function () {
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
$this->requirePermission('superuser_replication_manage');
|
$this->requireClassicSuperuserPermission('superuser_replication_manage');
|
||||||
try {
|
try {
|
||||||
$response->success((new replication_manager())->renameHost(
|
$response->success((new replication_manager())->renameHost(
|
||||||
(string)$this->fromRoute('kind'),
|
(string)$this->fromRoute('kind'),
|
||||||
@@ -148,7 +148,7 @@ class superuserReplicationRoute
|
|||||||
$this->delete('/superuser/replication/{kind}/{id}', function () {
|
$this->delete('/superuser/replication/{kind}/{id}', function () {
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
$this->requirePermission('superuser_replication_remove');
|
$this->requireClassicSuperuserPermission('superuser_replication_remove');
|
||||||
try {
|
try {
|
||||||
$response->success((new replication_manager())->removeHost(
|
$response->success((new replication_manager())->removeHost(
|
||||||
(string)$this->fromRoute('kind'),
|
(string)$this->fromRoute('kind'),
|
||||||
@@ -163,6 +163,23 @@ class superuserReplicationRoute
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replication controls alter infrastructure state and must only be used by
|
||||||
|
* a classic superuser session. Subuser bearer tokens can carry a delegated
|
||||||
|
* customer context via X-Customer-Number, so do not allow them to fall back
|
||||||
|
* to plain string user permission checks for these routes.
|
||||||
|
*/
|
||||||
|
private function requireClassicSuperuserPermission(string $permission): bool
|
||||||
|
{
|
||||||
|
global $response;
|
||||||
|
|
||||||
|
if ((new authentication())->get_subuser() !== false) {
|
||||||
|
$response->error('Subuser sessions cannot manage replication.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->requirePermission($permission);
|
||||||
|
}
|
||||||
|
|
||||||
private function routeId(): int
|
private function routeId(): int
|
||||||
{
|
{
|
||||||
$id = (int)$this->fromRoute('id');
|
$id = (int)$this->fromRoute('id');
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,10 +14,10 @@ it('registers superuser replication endpoints and permissions', function (): voi
|
|||||||
expect($content)->toContain('/superuser/replication/{kind}/{id}/provision');
|
expect($content)->toContain('/superuser/replication/{kind}/{id}/provision');
|
||||||
expect($content)->toContain('/superuser/replication/{kind}/{id}/promote');
|
expect($content)->toContain('/superuser/replication/{kind}/{id}/promote');
|
||||||
expect($content)->toContain("\$this->patch('/superuser/replication/{kind}/{id}'");
|
expect($content)->toContain("\$this->patch('/superuser/replication/{kind}/{id}'");
|
||||||
expect($content)->toContain("requirePermission('superuser_replication_view')");
|
expect($content)->toContain("requireClassicSuperuserPermission('superuser_replication_view')");
|
||||||
expect($content)->toContain("requirePermission('superuser_replication_manage')");
|
expect($content)->toContain("requireClassicSuperuserPermission('superuser_replication_manage')");
|
||||||
expect($content)->toContain("requirePermission('superuser_replication_promote')");
|
expect($content)->toContain("requireClassicSuperuserPermission('superuser_replication_promote')");
|
||||||
expect($content)->toContain("requirePermission('superuser_replication_remove')");
|
expect($content)->toContain("requireClassicSuperuserPermission('superuser_replication_remove')");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('documents replication management in openapi', function (): void {
|
it('documents replication management in openapi', function (): void {
|
||||||
@@ -36,3 +36,16 @@ it('documents replication management in openapi', function (): void {
|
|||||||
expect($content)->toContain('SuperuserReplicationHostRenameRequest');
|
expect($content)->toContain('SuperuserReplicationHostRenameRequest');
|
||||||
expect($content)->toContain('SuperuserReplicationComposeTemplateRequest');
|
expect($content)->toContain('SuperuserReplicationComposeTemplateRequest');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects subuser sessions before checking replication permissions', function (): void {
|
||||||
|
$content = file_get_contents(app_path('routes/superuserReplicationRoute.php'));
|
||||||
|
|
||||||
|
expect($content)->not->toBeFalse();
|
||||||
|
expect($content)->toContain('private function requireClassicSuperuserPermission(string $permission): bool');
|
||||||
|
expect($content)->toContain('get_subuser() !== false');
|
||||||
|
expect($content)->toContain("Subuser sessions cannot manage replication.");
|
||||||
|
expect($content)->toContain("\$response->error('Subuser sessions cannot manage replication.', 403);");
|
||||||
|
expect($content)->toContain('return $this->requirePermission($permission);');
|
||||||
|
expect(preg_match_all("/requireClassicSuperuserPermission\\('superuser_replication_/", $content))->toBe(11);
|
||||||
|
expect($content)->not->toContain("requirePermission('superuser_replication_");
|
||||||
|
});
|
||||||
|
|||||||
@@ -97,16 +97,21 @@ it('builds the installer around the compose stack artifacts and management polli
|
|||||||
expect($agentSource)->toContain("'last_transport_error'");
|
expect($agentSource)->toContain("'last_transport_error'");
|
||||||
expect($agentSource)->toContain('private function recordTransportFailure(string $context, Throwable $throwable): void');
|
expect($agentSource)->toContain('private function recordTransportFailure(string $context, Throwable $throwable): void');
|
||||||
expect($edgeDockerfileSource)->toContain('FROM ${BASE_IMAGE}');
|
expect($edgeDockerfileSource)->toContain('FROM ${BASE_IMAGE}');
|
||||||
|
expect($edgeDockerfileSource)->toContain('apt-get install -y --no-install-recommends libcurl4-openssl-dev libsqlite3-dev;');
|
||||||
|
expect($edgeDockerfileSource)->toContain('docker-php-ext-install -j"$(nproc)" curl sqlite3 pdo_sqlite;');
|
||||||
expect($edgeDockerfileSource)->toContain('extension_loaded($extension)');
|
expect($edgeDockerfileSource)->toContain('extension_loaded($extension)');
|
||||||
expect($edgeDockerfileSource)->toContain('Missing PHP extension: {$extension}');
|
expect($edgeDockerfileSource)->toContain('Missing PHP extension: {$extension}');
|
||||||
expect($edgeDockerfileSource)->toContain('COPY agent.php /opt/truckwash-edge-agent/agent.php');
|
expect($edgeDockerfileSource)->toContain('COPY agent.php /opt/truckwash-edge-agent/agent.php');
|
||||||
expect($workerDockerfileSource)->toContain('FROM ${BASE_IMAGE}');
|
expect($workerDockerfileSource)->toContain('FROM ${BASE_IMAGE}');
|
||||||
|
expect($workerDockerfileSource)->toContain('apt-get install -y --no-install-recommends libcurl4-openssl-dev libsqlite3-dev;');
|
||||||
|
expect($workerDockerfileSource)->toContain('docker-php-ext-install -j"$(nproc)" curl sqlite3 pdo_sqlite;');
|
||||||
expect($workerDockerfileSource)->toContain('extension_loaded($extension)');
|
expect($workerDockerfileSource)->toContain('extension_loaded($extension)');
|
||||||
expect($workerDockerfileSource)->toContain('Missing PHP extension: {$extension}');
|
expect($workerDockerfileSource)->toContain('Missing PHP extension: {$extension}');
|
||||||
expect($workerDockerfileSource)->toContain('COPY lan-worker.php /opt/truckwash-edge-agent/lan-worker.php');
|
expect($workerDockerfileSource)->toContain('COPY lan-worker.php /opt/truckwash-edge-agent/lan-worker.php');
|
||||||
expect($autoUpdaterSource)->toContain("'/bin/bash ' . escapeshellarg(\$launcherPath) . ' reconcile 2>&1'");
|
expect($autoUpdaterSource)->toContain("'/bin/bash ' . escapeshellarg(\$launcherPath) . ' reconcile 2>&1'");
|
||||||
expect($autoUpdaterDockerfileSource)->toContain('COPY auto-updater.php /usr/local/bin/auto-updater.php');
|
expect($autoUpdaterDockerfileSource)->toContain('COPY auto-updater.php /usr/local/bin/auto-updater.php');
|
||||||
expect($autoUpdaterDockerfileSource)->toContain('apt-get install -y --no-install-recommends bash ca-certificates curl docker.io docker-compose;');
|
expect($autoUpdaterDockerfileSource)->toContain('apt-get install -y --no-install-recommends bash ca-certificates curl docker.io docker-compose libcurl4-openssl-dev libsqlite3-dev;');
|
||||||
|
expect($autoUpdaterDockerfileSource)->toContain('docker-php-ext-install -j"$(nproc)" curl sqlite3 pdo_sqlite;');
|
||||||
expect($autoUpdaterDockerfileSource)->toContain('extension_loaded($extension)');
|
expect($autoUpdaterDockerfileSource)->toContain('extension_loaded($extension)');
|
||||||
expect($serviceSource)->not->toContain('node /opt/truckwash-edge-agent/agent.mjs');
|
expect($serviceSource)->not->toContain('node /opt/truckwash-edge-agent/agent.mjs');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user