feat: Add CORS middleware untuk akses dari browser lokal

This commit is contained in:
mwpn
2025-12-17 13:56:32 +07:00
parent bbe4cc000b
commit 89c9ea93c8
3 changed files with 142 additions and 0 deletions

View File

@@ -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 - **Modular Architecture** - Struktur code yang terorganisir dan mudah di-scale
- **JWT Authentication** - Secure authentication dengan role-based access - **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 - **CRUD Master Data** - Locations, Gates, Tariffs dengan audit logging
- **Realtime Dashboard** - SSE (Server-Sent Events) untuk update real-time - **Realtime Dashboard** - SSE (Server-Sent Events) untuk update real-time
- **Data Aggregation** - Daily & Hourly summary untuk reporting - **Data Aggregation** - Daily & Hourly summary untuk reporting
@@ -137,6 +138,14 @@ JWT_ISSUER=api-btekno
# API Key # API Key
RETRIBUSI_API_KEY=your-api-key-here 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 ## 📡 API Endpoints

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Bootstrap; namespace App\Bootstrap;
use App\Middleware\CorsMiddleware;
use Slim\App; use Slim\App;
use Slim\Factory\AppFactory; use Slim\Factory\AppFactory;
use Slim\Middleware\BodyParsingMiddleware; use Slim\Middleware\BodyParsingMiddleware;
@@ -19,6 +20,9 @@ class AppBootstrap
{ {
$app = AppFactory::create(); $app = AppFactory::create();
// Add CORS middleware FIRST (before routing)
$app->add(new CorsMiddleware());
// Add body parsing middleware // Add body parsing middleware
$app->addBodyParsingMiddleware(); $app->addBodyParsingMiddleware();

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Middleware;
use App\Config\AppConfig;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Factory\ResponseFactory;
class CorsMiddleware implements MiddlewareInterface
{
private array $allowedOrigins;
private array $allowedMethods;
private array $allowedHeaders;
private bool $allowCredentials;
public function __construct()
{
// Load allowed origins from ENV or use defaults
$originsEnv = AppConfig::get('CORS_ALLOWED_ORIGINS', '*');
$this->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;
}
}