consumers->list($page['limit'], $page['offset']); $total = $this->consumers->count(); return self::json($response, 200, [ 'data' => array_map(static fn ($c) => $c->toArray(), $rows), 'page' => $page['page'], 'limit' => $page['limit'], 'total' => $total, ]); } /** * @param array{id: string} $args */ public function show(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = self::parseId($args['id']); if ($id === null) { return self::error($response, 404, 'not_found'); } $consumer = $this->consumers->findById($id); if ($consumer === null) { return self::error($response, 404, 'not_found'); } return self::json($response, 200, $consumer->toArray()); } public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $body = self::jsonBody($request); $errors = []; $name = isset($body['name']) && is_string($body['name']) ? trim($body['name']) : ''; if ($name === '' || strlen($name) > 128) { $errors['name'] = 'required, 1–128 chars'; } $description = null; if (array_key_exists('description', $body)) { if ($body['description'] !== null && !is_string($body['description'])) { $errors['description'] = 'must be string or null'; } else { $description = $body['description']; } } $policyId = null; if (!array_key_exists('policy_id', $body) || !is_int($body['policy_id'])) { // Accept numeric string too — JSON usually has int but be lenient. if (isset($body['policy_id']) && is_string($body['policy_id']) && ctype_digit($body['policy_id'])) { $policyId = (int) $body['policy_id']; } else { $errors['policy_id'] = 'required, integer'; } } else { $policyId = $body['policy_id']; } if ($policyId !== null && $policyId > 0 && !$this->consumers->policyExists($policyId)) { $errors['policy_id'] = 'unknown policy'; } if ($errors === [] && $this->consumers->findByName($name) !== null) { $errors['name'] = 'already exists'; } if ($errors !== []) { return self::validationFailed($response, $errors); } /** @var int $policyId */ $id = $this->consumers->create($name, $description, $policyId, self::actingUserId($request)); $created = $this->consumers->findById($id); if ($created === null) { return self::error($response, 500, 'create_failed'); } $this->audit->emit( AuditAction::CONSUMER_CREATED, 'consumer', $id, ['name' => $name, 'policy_id' => $policyId, 'description' => $description], self::auditContext($request), $name, ); return self::json($response, 201, $created->toArray()); } /** * @param array{id: string} $args */ public function update(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = self::parseId($args['id']); if ($id === null) { return self::error($response, 404, 'not_found'); } $existing = $this->consumers->findById($id); if ($existing === null) { return self::error($response, 404, 'not_found'); } $body = self::jsonBody($request); $errors = []; $fields = []; if (array_key_exists('name', $body)) { if (!is_string($body['name']) || trim($body['name']) === '' || strlen(trim($body['name'])) > 128) { $errors['name'] = 'required, 1–128 chars'; } else { $name = trim($body['name']); $other = $this->consumers->findByName($name); if ($other !== null && $other->id !== $id) { $errors['name'] = 'already exists'; } else { $fields['name'] = $name; } } } if (array_key_exists('description', $body)) { if ($body['description'] !== null && !is_string($body['description'])) { $errors['description'] = 'must be string or null'; } else { $fields['description'] = $body['description']; } } if (array_key_exists('policy_id', $body)) { $newPolicy = null; if (is_int($body['policy_id'])) { $newPolicy = $body['policy_id']; } elseif (is_string($body['policy_id']) && ctype_digit($body['policy_id'])) { $newPolicy = (int) $body['policy_id']; } if ($newPolicy === null || $newPolicy <= 0) { $errors['policy_id'] = 'must be positive integer'; } elseif (!$this->consumers->policyExists($newPolicy)) { $errors['policy_id'] = 'unknown policy'; } else { $fields['policy_id'] = $newPolicy; } } if (array_key_exists('is_active', $body)) { if (!is_bool($body['is_active'])) { $errors['is_active'] = 'must be boolean'; } else { $fields['is_active'] = $body['is_active'] ? 1 : 0; } } if (array_key_exists('audit_enabled', $body)) { if (!is_bool($body['audit_enabled'])) { $errors['audit_enabled'] = 'must be boolean'; } else { $fields['audit_enabled'] = $body['audit_enabled'] ? 1 : 0; } } if ($errors !== []) { return self::validationFailed($response, $errors); } $beforeSnapshot = [ 'name' => $existing->name, 'description' => $existing->description, 'policy_id' => $existing->policyId, 'is_active' => $existing->isActive ? 1 : 0, 'audit_enabled' => $existing->auditEnabled ? 1 : 0, ]; $this->consumers->update($id, $fields); $updated = $this->consumers->findById($id); if ($updated === null) { return self::error($response, 500, 'update_failed'); } $this->audit->emit( AuditAction::CONSUMER_UPDATED, 'consumer', $id, ['name' => $existing->name, 'changes' => self::diffFields($beforeSnapshot, $fields)], self::auditContext($request), $updated->name, ); return self::json($response, 200, $updated->toArray()); } /** * @param array{id: string} $args */ public function delete(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = self::parseId($args['id']); if ($id === null) { return self::error($response, 404, 'not_found'); } $existing = $this->consumers->findById($id); if ($existing === null) { return self::error($response, 404, 'not_found'); } $this->consumers->softDelete($id); $this->audit->emit( AuditAction::CONSUMER_DELETED, 'consumer', $id, ['name' => $existing->name, 'soft' => true], self::auditContext($request), $existing->name, ); return $response->withStatus(204); } private static function parseId(string $raw): ?int { return preg_match('/^[1-9][0-9]*$/', $raw) === 1 ? (int) $raw : null; } }