forked from fa/breadcrumb-the-shire
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks - Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists - Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services - Add Microsoft OIDC SSO, API token management, and user lifecycle features - Add swagger-ui vendor integration and OpenAPI spec - Add production Docker setup and bin/ scripts - Update composer dependencies, config, templates, and frontend assets throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
159 lines
4.1 KiB
PHP
159 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Http;
|
|
|
|
use MintyPHP\Service\Audit\ApiAuditService;
|
|
|
|
class ApiResponse
|
|
{
|
|
public static function success(array $data = [], int $status = 200): never
|
|
{
|
|
self::send($data, $status);
|
|
}
|
|
|
|
public static function created(array $data = []): never
|
|
{
|
|
self::success($data, 201);
|
|
}
|
|
|
|
public static function noContent(): never
|
|
{
|
|
self::send(null, 204);
|
|
}
|
|
|
|
public static function error(string $error, int $status = 400, array $extra = []): never
|
|
{
|
|
$body = array_merge(['error' => $error], $extra);
|
|
self::send($body, $status, $error);
|
|
}
|
|
|
|
public static function unauthorized(): never
|
|
{
|
|
self::error('unauthorized', 401);
|
|
}
|
|
|
|
public static function forbidden(string $detail = 'forbidden'): never
|
|
{
|
|
self::error($detail, 403);
|
|
}
|
|
|
|
public static function notFound(string $detail = 'not_found'): never
|
|
{
|
|
self::error($detail, 404);
|
|
}
|
|
|
|
public static function methodNotAllowed(): never
|
|
{
|
|
self::error('method_not_allowed', 405);
|
|
}
|
|
|
|
public static function validationError(array $errors): never
|
|
{
|
|
self::error('validation_error', 422, ['errors' => $errors]);
|
|
}
|
|
|
|
public static function tooManyRequests(int $retryAfter = 60): never
|
|
{
|
|
self::send(
|
|
['error' => 'rate_limit_exceeded'],
|
|
429,
|
|
'rate_limit_exceeded',
|
|
['Retry-After: ' . $retryAfter]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Transform a service result (['ok' => bool, ...] pattern) into an API response.
|
|
*/
|
|
public static function fromServiceResult(array $result, int $successStatus = 200): never
|
|
{
|
|
if ($result['ok'] ?? false) {
|
|
$data = $result;
|
|
unset($data['ok']);
|
|
self::success($data, $successStatus);
|
|
}
|
|
|
|
$status = (int) ($result['status'] ?? 0);
|
|
if ($status < 400) {
|
|
$status = 422;
|
|
}
|
|
$error = (string) ($result['error'] ?? 'operation_failed');
|
|
$errors = $result['errors'] ?? [];
|
|
$extra = [];
|
|
if ($errors) {
|
|
$extra['errors'] = $errors;
|
|
}
|
|
self::error($error, $status, $extra);
|
|
}
|
|
|
|
/**
|
|
* Read JSON request body into an array.
|
|
*/
|
|
public static function readJsonBody(): array
|
|
{
|
|
$raw = file_get_contents('php://input');
|
|
if ($raw === false || $raw === '') {
|
|
return [];
|
|
}
|
|
$data = json_decode($raw, true);
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
|
|
/**
|
|
* Require specific HTTP method(s).
|
|
*/
|
|
public static function requireMethod(string ...$methods): void
|
|
{
|
|
$method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
|
if (!in_array($method, $methods, true)) {
|
|
self::methodNotAllowed();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Require API authentication.
|
|
*/
|
|
public static function requireAuth(): void
|
|
{
|
|
if (!ApiAuth::isAuthenticated()) {
|
|
self::unauthorized();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Require a specific permission key.
|
|
*/
|
|
public static function requirePermission(string $key): void
|
|
{
|
|
self::requireAuth();
|
|
if (!ApiAuth::hasPermission($key)) {
|
|
self::forbidden();
|
|
}
|
|
}
|
|
|
|
private static function send(?array $body, int $status, ?string $errorCode = null, array $headers = []): never
|
|
{
|
|
http_response_code($status);
|
|
foreach ($headers as $headerLine) {
|
|
header($headerLine);
|
|
}
|
|
|
|
if ($body === null) {
|
|
ApiAuditService::finish($status, $errorCode);
|
|
die();
|
|
}
|
|
|
|
$json = json_encode($body, JSON_UNESCAPED_UNICODE);
|
|
if (!is_string($json)) {
|
|
$status = 500;
|
|
http_response_code($status);
|
|
$errorCode = $errorCode ?: 'serialization_error';
|
|
$json = json_encode(['error' => 'serialization_error'], JSON_UNESCAPED_UNICODE) ?: '{"error":"serialization_error"}';
|
|
}
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
ApiAuditService::finish($status, $errorCode);
|
|
die($json);
|
|
}
|
|
}
|