- Add `.junie/guidelines.md` with comprehensive development instructions. - Include `.aiassistant/rules/Creating and maintaining tests.md` and `.aiassistant/rules/Creating and securing routes.md`. - Introduce `CACHE_SELFSERVE_LANE_KEY_ALLOWED_SERVICES` for lane service validation. - Update `selfserve_lane_relay_controller_t` to enforce service-specific permissions for machine relay.
9.4 KiB
9.4 KiB
apply
| apply |
|---|
| always |
Creating and securing routes — Rules (Project‑specific)
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.
-
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
routesand each file must define one class that usestraits\route_tand implementsrun(): void.<?php namespace routes; use traits\route_t; class ExampleRoute { use route_t; public function run(): void { // endpoints go here } }
- Place route classes under
-
Registering endpoints (HTTP verbs)
- Define endpoints inside
run()using the helpers provided byroute_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 (read‑only endpoint):
$this->get('/example', function () { global $response; $response->success(['message' => 'Hello World!']); });
- Define endpoints inside
-
Route paths and local routing
- Caddy serves the app directly; Traefik adds an external
/apiprefix 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 strip‑prefix/api→ still routes to/foo/barin the app)
- Direct path in the app:
- Keep paths meaningful and resource‑oriented. Prefer plural nouns for collections and subpaths for actions when unavoidable (e.g.,
/modules/self-serve/lane/status).
- Caddy serves the app directly; Traefik adds an external
-
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, non‑sensitive error messages. Internal details go to logs (see
objects\logs_o).
- Use
-
Input handling and validation (route_t helpers)
- Read parameters via:
getParameter($name)orgetParametersAsArray()(preferred; integrates withclasses\responseparsing)fromRequest($name)(checks JSON body, then$_POST, then query)fromQuery($name)andfromRoute($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)
- Presence:
- Read parameters via:
-
Authentication and authorization (core rules)
- Default posture: endpoints are protected and require authentication unless explicitly public.
- To check authentication without a specific permission:
isAuthenticated().
- To check authentication without a specific permission:
- 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
$permissionsargument when declaring the route. Keys are permission names; values are human‑readable descriptions. You should still callrequirePermission()inside the handler to actually enforce.$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', ]);
- Default posture: endpoints are protected and require authentication unless explicitly public.
-
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-Numberheader or acustomer_numberparameter;route_texposes 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)andresolveEffectiveCustomerNumber()when building conditional logic.
- When a permission corresponds to a subuser permission node, define it with
-
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 expectg_recaptcha_responsein the request.
- Department access:
-
Public endpoints
- Keep public endpoints extremely limited. If an endpoint must be public, you must:
- Validate all inputs thoroughly using the helpers above.
- Add rate‑limit or abuse‑mitigation where relevant (Redis is available as
classes\redisfor counters/locks; coordinate design with maintainers before introducing new limits). - Prefer
requireRecaptcha()for anonymous form submissions.
- Keep public endpoints extremely limited. If an endpoint must be public, you must:
-
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 route’s docblock and in
openapi.yaml. - Wrap multi‑step operations with proper validation and permission checks before any external calls (Stripe, e‑conomic, etc.). Do not leak third‑party error payloads directly; map to concise API errors and log details.
- Logging and observability
- On auth/permission denials, the helpers already log to
logs_owith structured messages. Avoid duplicating logs for the same event. - For unexpected states you handle gracefully, add targeted logs via
objects\logs_owith a clear category/key.
- OpenAPI contract synchronization
- Every public surface change must be reflected in
openapi.yamlat 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 Traefik’s
/apiis stripped before hitting the app). - If you add or change permissions that users must hold, document them in the endpoint description.
- Example: secure command endpoint
<?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 self‑serve lane commands',
]);
}
}
- Quick reference (local)
- Bring up minimal stack:
docker compose up -d traefik redis php1 caddy - Access an app route locally:
http://localhost/<path>orhttp://localhost/api/<path>
- Tests are CLI scripts under
services/nginx/app/tests. Prefer unit‑style tests for core logic; see “Creating and maintaining tests — Rules”.
- Code style and hygiene
- Follow
.editorconfig(UTF‑8, CRLF, 4‑space indents). - Keep route files cohesive; avoid adding unrelated endpoints to the same class. If a class exceeds ~200–300 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.