From 89c9ea93c87c94758c98f173687a494e4e290108 Mon Sep 17 00:00:00 2001 From: mwpn Date: Wed, 17 Dec 2025 13:56:32 +0700 Subject: [PATCH] feat: Add CORS middleware untuk akses dari browser lokal --- README.md | 9 +++ src/Bootstrap/app.php | 4 + src/Middleware/CorsMiddleware.php | 129 ++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 src/Middleware/CorsMiddleware.php diff --git a/README.md b/README.md index 7313b97..9078cdb 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Sistem API Retribusi berbasis Slim Framework 4 dengan arsitektur modular untuk i - **Modular Architecture** - Struktur code yang terorganisir dan mudah di-scale - **JWT Authentication** - Secure authentication dengan role-based access +- **CORS Support** - Cross-Origin Resource Sharing untuk akses dari browser - **CRUD Master Data** - Locations, Gates, Tariffs dengan audit logging - **Realtime Dashboard** - SSE (Server-Sent Events) untuk update real-time - **Data Aggregation** - Daily & Hourly summary untuk reporting @@ -137,6 +138,14 @@ JWT_ISSUER=api-btekno # API Key RETRIBUSI_API_KEY=your-api-key-here + +# CORS (Cross-Origin Resource Sharing) +# Set '*' untuk allow semua origin (development) +# Atau list origin yang diizinkan dipisah koma: http://localhost:3000,https://app.example.com +CORS_ALLOWED_ORIGINS=* +CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS +CORS_ALLOWED_HEADERS=Content-Type,Authorization,X-API-KEY,Accept,Origin +CORS_ALLOW_CREDENTIALS=true ``` ## 📡 API Endpoints diff --git a/src/Bootstrap/app.php b/src/Bootstrap/app.php index 33b2c9e..56bfd3b 100644 --- a/src/Bootstrap/app.php +++ b/src/Bootstrap/app.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Bootstrap; +use App\Middleware\CorsMiddleware; use Slim\App; use Slim\Factory\AppFactory; use Slim\Middleware\BodyParsingMiddleware; @@ -19,6 +20,9 @@ class AppBootstrap { $app = AppFactory::create(); + // Add CORS middleware FIRST (before routing) + $app->add(new CorsMiddleware()); + // Add body parsing middleware $app->addBodyParsingMiddleware(); diff --git a/src/Middleware/CorsMiddleware.php b/src/Middleware/CorsMiddleware.php new file mode 100644 index 0000000..7b00e5b --- /dev/null +++ b/src/Middleware/CorsMiddleware.php @@ -0,0 +1,129 @@ +allowedOrigins = $originsEnv === '*' + ? ['*'] + : array_map('trim', explode(',', $originsEnv)); + + // Allowed HTTP methods + $methodsEnv = AppConfig::get('CORS_ALLOWED_METHODS', 'GET,POST,PUT,DELETE,OPTIONS'); + $this->allowedMethods = array_map('trim', explode(',', $methodsEnv)); + + // Allowed headers + $headersEnv = AppConfig::get( + 'CORS_ALLOWED_HEADERS', + 'Content-Type,Authorization,X-API-KEY,Accept,Origin' + ); + $this->allowedHeaders = array_map('trim', explode(',', $headersEnv)); + + // Allow credentials + $this->allowCredentials = AppConfig::get('CORS_ALLOW_CREDENTIALS', 'true') === 'true'; + } + + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + $origin = $request->getHeaderLine('Origin'); + + // Handle preflight OPTIONS request + if ($request->getMethod() === 'OPTIONS') { + $responseFactory = new ResponseFactory(); + $response = $responseFactory->createResponse(204); // No Content + return $this->addCorsHeaders($response, $origin); + } + + // Process the request + $response = $handler->handle($request); + + // Add CORS headers to response + return $this->addCorsHeaders($response, $origin); + } + + private function addCorsHeaders( + ResponseInterface $response, + string $origin + ): ResponseInterface { + // Determine allowed origin + $allowedOrigin = $this->getAllowedOrigin($origin); + + if ($allowedOrigin) { + $response = $response->withHeader('Access-Control-Allow-Origin', $allowedOrigin); + } + + if ($this->allowCredentials && $allowedOrigin !== '*') { + $response = $response->withHeader('Access-Control-Allow-Credentials', 'true'); + } + + $response = $response->withHeader( + 'Access-Control-Allow-Methods', + implode(', ', $this->allowedMethods) + ); + + $response = $response->withHeader( + 'Access-Control-Allow-Headers', + implode(', ', $this->allowedHeaders) + ); + + $response = $response->withHeader('Access-Control-Max-Age', '86400'); // 24 hours + + return $response; + } + + private function getAllowedOrigin(string $origin): ?string + { + // If no origin header, return null (not a CORS request) + if (empty($origin)) { + return null; + } + + // If wildcard is allowed, return it + if (in_array('*', $this->allowedOrigins, true)) { + return '*'; + } + + // Check if origin is in allowed list + if (in_array($origin, $this->allowedOrigins, true)) { + return $origin; + } + + // Check for localhost variations + $localhostPatterns = [ + 'http://localhost', + 'http://127.0.0.1', + 'http://localhost:', + 'http://127.0.0.1:', + ]; + + foreach ($localhostPatterns as $pattern) { + if (str_starts_with($origin, $pattern)) { + return $origin; + } + } + + // Default: return first allowed origin (fallback) + return $this->allowedOrigins[0] ?? null; + } +} +