Remove outdated edge gateway object classes, add new agent implementation

Transitioned from obsolete gateway object classes (`edge_gateway_shell_action_jobs_o`, `edge_gateway_shell_events_o`, `edge_gateway_shell_sessions_o`, `edge_gateway_update_jobs_o`) to the new agent implementation (`edge-gateway-agent/agent.php`).
This commit is contained in:
Jeppe Bundgaard
2026-04-21 14:13:17 +02:00
parent d30a006457
commit 7d450e285e
90 changed files with 11811 additions and 2760 deletions
@@ -2,172 +2,18 @@
apply: always
---
### Creating and securing routes — Rules (Projectspecific)
<!-- AUTOGENERATED: Run `node scripts/sync-ai-workflow.mjs --write`. -->
These rules define how to add HTTP routes to the Copenhagen Truck Wash API and how to secure them consistently. They reflect current patterns in `services/nginx/app/routes` and the helper APIs in `traits\route_t`.
# Backend Route Rules
1. Location, naming, and structure
- Place route classes under `services/nginx/app/routes`.
- File names should describe the domain and end with `Route.php`, e.g., `BookingsRoute.php`, `ModuleSelfServeRoute.php`.
- Namespace must be `routes` and each file must define one class that uses `traits\route_t` and implements `run(): void`.
```php
<?php
namespace routes;
use traits\route_t;
These rules apply to files under `services/nginx/app/routes` and the classes they call.
class ExampleRoute
{
use route_t;
public function run(): void
{
// endpoints go here
}
}
```
2. Registering endpoints (HTTP verbs)
- Define endpoints inside `run()` using the helpers provided by `route_t`:
- `get($path, $handler, $permissions = [])`
- `post($path, $handler, $permissions = [])`
- `put($path, $handler, $permissions = [])`
- `delete($path, $handler, $permissions = [])`
- `patch($path, $handler, $permissions = [])`
- `options($path, $handler, $permissions = [])` (useful for CORS preflight when needed)
- Handlers are closures that perform validation, authorization, side effects, and write responses via the global `$response` (`classes\response`).
- Example (readonly endpoint):
```php
$this->get('/example', function () {
global $response;
$response->success(['message' => 'Hello World!']);
});
```
3. Route paths and local routing
- Caddy serves the app directly; Traefik adds an external `/api` prefix for local access.
- Direct path in the app: `/foo/bar`.
- Local access paths (both work):
- `http://localhost/foo/bar` (direct)
- `http://localhost/api/foo/bar` (Traefik stripprefix `/api` → still routes to `/foo/bar` in the app)
- Keep paths meaningful and resourceoriented. Prefer plural nouns for collections and subpaths for actions when unavoidable (e.g., `/modules/self-serve/lane/status`).
4. Responses and status codes
- Use `$response->success($payload, $status = 200)` for successful results and `$response->error($messageOrPayload, $status)` for failures.
- Typical statuses:
- 200 OK for successful reads/updates; 201 Created when new resources are created.
- 400 Bad Request for validation errors.
- 401 Unauthorized when authentication is missing/invalid.
- 403 Forbidden when authenticated but lacking permissions.
- Return concise, nonsensitive error messages. Internal details go to logs (see `objects\logs_o`).
5. Input handling and validation (route_t helpers)
- Read parameters via:
- `getParameter($name)` or `getParametersAsArray()` (preferred; integrates with `classes\response` parsing)
- `fromRequest($name)` (checks JSON body, then `$_POST`, then query)
- `fromQuery($name)` and `fromRoute($segmentName)` when relevant
- Validate early and fail fast using helpers:
- Presence: `requireParameters(['a','b'])`, `isParametersSet(['a','b'])`
- Types: `requireType($val, type_string()|type_int()|TYPE_BOOL()|TYPE_ARRAY())`, `requireTypeIn($val, [...])`
- Ranges/lengths: `requireMinValue($n, $min)`, `requireMaxValue($n, $max)`, `requireMaxLength($name, $len)`, `requireParameterIntPositive($n, $name)`
- Formats/enums: `requireDateFormat($date, FORMAT_DATE())`, `requireInArray($val, $allowed)`
6. Authentication and authorization (core rules)
- Default posture: endpoints are protected and require authentication unless explicitly public.
- To check authentication without a specific permission: `isAuthenticated()`.
- Enforce permissions inside handlers using:
- `requirePermission('some_permission')` — throws 401/403 as appropriate.
- `hasPermission('some_permission', $customerNumber = null)` — boolean check (do not throw).
- Register permissions for discoverability/administration by supplying the third `$permissions` argument when declaring the route. Keys are permission names; values are humanreadable descriptions. You should still call `requirePermission()` inside the handler to actually enforce.
```php
$this->get('/modules/self-serve/lane/status', function () {
global $response;
self::requirePermission('modules_selfserve_lane_status_view');
// ...
$response->success(['status' => 'OK']);
}, [
'modules_selfserve_lane_status_view' => 'View self-serve lane status',
]);
```
7. Subusers, permission nodes, and customer context
- When a permission corresponds to a subuser permission node, define it with `definePermission($perm, subusers_permission_node_key::CASE)` to keep mapping explicit.
- Subuser contexts may target a specific customer via the `X-Customer-Number` header or a `customer_number` parameter; `route_t` exposes this as response meta (`target_customer_number`) when applicable.
- Use `allowOwnOrDepartmentAccess($ownPerm, $deptPerm, $targetCustomerNumber, $departmentId, $ownGuard = null, $denyMessage = null)` for the common pattern: allow the principal to act on their own customer scope or fall back to a department/admin permission.
- Use `isOwnCustomerContext($targetCustomerNumber)` and `resolveEffectiveCustomerNumber()` when building conditional logic.
8. Department, order, and special access helpers
- Department access: `requireDepartmentAccess($departmentId, $permissionSuffix = null)`, `hasDepartmentAccess(...)`.
- Order access: `requireOrderAccess($orderId, $permissionSuffix = null)` (note: indirect access rules may evolve; keep logic minimal in routes).
- Plate scanners (API key based): protect endpoints with `requirePlateScannerAuth()`.
- Bot protection (public forms): use `requireRecaptcha()` and expect `g_recaptcha_response` in the request.
9. Public endpoints
- Keep public endpoints extremely limited. If an endpoint must be public, you must:
- Validate all inputs thoroughly using the helpers above.
- Add ratelimit or abusemitigation where relevant (Redis is available as `classes\redis` for counters/locks; coordinate design with maintainers before introducing new limits).
- Prefer `requireRecaptcha()` for anonymous form submissions.
10. Side effects and idempotency
- For POST/PUT/PATCH with side effects, make operations idempotent when feasible (e.g., by honoring a client idempotency key header). If you add such behavior, document it in the routes docblock and in `openapi.yaml`.
- Wrap multistep operations with proper validation and permission checks before any external calls (Stripe, economic, etc.). Do not leak thirdparty error payloads directly; map to concise API errors and log details.
11. Logging and observability
- On auth/permission denials, the helpers already log to `logs_o` with structured messages. Avoid duplicating logs for the same event.
- For unexpected states you handle gracefully, add targeted logs via `objects\logs_o` with a clear category/key.
12. OpenAPI contract synchronization
- Every public surface change must be reflected in `openapi.yaml` at repo root. Keep paths, methods, parameters, request/response schemas, and error statuses up to date.
- Use the same path strings as in the route declaration (remember Traefiks `/api` is stripped before hitting the app).
- If you add or change permissions that users must hold, document them in the endpoint description.
13. Example: secure command endpoint
```php
<?php
namespace routes;
use traits\route_t;
use classes\authentication;
class LaneCommandRoute
{
use route_t;
public function run(): void
{
$this->post('/modules/self-serve/lane/command', function () {
global $response;
// AuthZ
self::requirePermission('modules_selfserve_lane_command_execute');
// Validate input
self::requireParameters(['lane_id', 'command']);
$laneId = (int) self::getParameter('lane_id');
self::requireType($laneId, self::type_int());
self::requireMinValue($laneId, 1);
$cmd = (string) self::getParameter('command');
self::requireType($cmd, self::type_string());
self::requireInArray($cmd, ['START','STOP','RESERVE','RELEASE','RESET']);
// Business logic (call into modules/classes)
// ...
$response->success(['ok' => true]);
}, [
'modules_selfserve_lane_command_execute' => 'Execute selfserve lane commands',
]);
}
}
```
14. Quick reference (local)
- Bring up minimal stack: `docker compose up -d traefik redis php1 caddy`
- Access an app route locally:
- `http://localhost/<path>` or `http://localhost/api/<path>`
- Tests are CLI scripts under `services/nginx/app/tests`. Prefer unitstyle tests for core logic; see “Creating and maintaining tests — Rules”.
15. Code style and hygiene
- Follow `.editorconfig` (UTF8, CRLF, 4space indents).
- Keep route files cohesive; avoid adding unrelated endpoints to the same class. If a class exceeds ~200300 lines or mixes domains, split it.
- Do not perform heavy bootstrap in route files. Delegate to classes/modules and keep handlers thin: validate → authorize → invoke → respond.
1. Keep route handlers thin: validate input, enforce permissions, call domain code, and write the response.
2. Default to protected endpoints. Use the existing authentication and permission helpers instead of ad hoc access checks.
3. Add or update backend tests in the `php1` container whenever route behavior changes.
4. Update `openapi.yaml` whenever paths, parameters, request bodies, response envelopes, or permissions change.
5. Prefer deterministic route tests and avoid live external integrations in route coverage.
6. Use concise API errors and keep sensitive implementation details out of the response body.
7. When a change touches department, order, or subuser authorization, cover both the allow path and the deny path.
Canonical workflow reference: `.ai-workflow/workflow.md`.