From c08e0c7983c0c043eb5970ab3879ee8fab5cb560 Mon Sep 17 00:00:00 2001 From: mwpn Date: Wed, 17 Dec 2025 11:08:04 +0700 Subject: [PATCH] feat: Add Swagger UI documentation di root URL --- public/docs/index.html | 48 ++++ public/docs/openapi.json | 604 +++++++++++++++++++++++++++++++++++++++ public/index.php | 41 +++ 3 files changed, 693 insertions(+) create mode 100644 public/docs/index.html create mode 100644 public/docs/openapi.json diff --git a/public/docs/index.html b/public/docs/index.html new file mode 100644 index 0000000..2ed8030 --- /dev/null +++ b/public/docs/index.html @@ -0,0 +1,48 @@ + + + + + + API Retribusi - Documentation + + + + +
+ + + + + + diff --git a/public/docs/openapi.json b/public/docs/openapi.json new file mode 100644 index 0000000..9775579 --- /dev/null +++ b/public/docs/openapi.json @@ -0,0 +1,604 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "API Retribusi", + "description": "Sistem API Retribusi berbasis Slim Framework 4 untuk infrastruktur pemerintah", + "version": "1.0.0", + "contact": { + "name": "BTekno Development Team" + } + }, + "servers": [ + { + "url": "https://api.btekno.cloud", + "description": "Production Server" + }, + { + "url": "http://localhost", + "description": "Local Development" + } + ], + "tags": [ + { + "name": "Health", + "description": "Health check endpoint" + }, + { + "name": "Authentication", + "description": "JWT authentication" + }, + { + "name": "Ingest", + "description": "Event ingestion (mesin YOLO)" + }, + { + "name": "Frontend", + "description": "Frontend CRUD operations" + }, + { + "name": "Summary", + "description": "Data summary & aggregation" + }, + { + "name": "Dashboard", + "description": "Dashboard visualization data" + }, + { + "name": "Realtime", + "description": "Real-time events (SSE)" + } + ], + "components": { + "securitySchemes": { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "JWT token untuk frontend API" + }, + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-KEY", + "description": "API Key untuk ingest endpoint" + } + }, + "schemas": { + "Error": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error code" + }, + "message": { + "type": "string", + "description": "Error message" + }, + "fields": { + "type": "object", + "description": "Validation errors (optional)" + } + } + }, + "Success": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object" + }, + "timestamp": { + "type": "integer", + "description": "Unix timestamp" + } + } + } + } + }, + "paths": { + "/health": { + "get": { + "tags": ["Health"], + "summary": "Health check", + "description": "Check API health status", + "responses": { + "200": { + "description": "API is healthy", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + }, + "time": { + "type": "integer", + "example": 1735123456 + } + } + } + } + } + } + } + } + }, + "/auth/v1/login": { + "post": { + "tags": ["Authentication"], + "summary": "Login", + "description": "Authenticate user dan dapatkan JWT token", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["username", "password"], + "properties": { + "username": { + "type": "string", + "example": "admin" + }, + "password": { + "type": "string", + "format": "password", + "example": "password123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Login successful", + "content": { + "application/json": { + "schema": { + "allOf": [ + {"$ref": "#/components/schemas/Success"}, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "user": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "username": {"type": "string"}, + "role": {"type": "string"} + } + } + } + } + } + } + ] + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (user inactive)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/retribusi/v1/ingest": { + "post": { + "tags": ["Ingest"], + "summary": "Ingest event data", + "description": "Masukkan event data dari mesin YOLO", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["timestamp", "location_code", "gate_code", "category"], + "properties": { + "timestamp": { + "type": "integer", + "description": "Unix timestamp", + "example": 1735123456 + }, + "location_code": { + "type": "string", + "example": "kerkof_01" + }, + "gate_code": { + "type": "string", + "example": "gate01" + }, + "category": { + "type": "string", + "example": "motor" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Event stored successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Success" + } + } + } + }, + "401": { + "description": "Unauthorized (invalid API key)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found (location/gate/tariff not found)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/retribusi/v1/frontend/locations": { + "get": { + "tags": ["Frontend"], + "summary": "Get locations list", + "description": "Mendapatkan daftar lokasi dengan pagination", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1, + "minimum": 1 + }, + "description": "Page number" + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 20, + "minimum": 1, + "maximum": 100 + }, + "description": "Items per page" + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Success" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": ["Frontend"], + "summary": "Create location", + "description": "Membuat lokasi baru (operator/admin only)", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["code", "name", "type", "is_active"], + "properties": { + "code": { + "type": "string", + "example": "kerkof_01" + }, + "name": { + "type": "string", + "example": "Kerkof Garut" + }, + "type": { + "type": "string", + "example": "parkir" + }, + "is_active": { + "type": "integer", + "enum": [0, 1], + "example": 1 + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Location created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Success" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden (insufficient permissions)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Conflict (code already exists)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/retribusi/v1/summary/daily": { + "get": { + "tags": ["Summary"], + "summary": "Get daily summary", + "description": "Mendapatkan rekap harian", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "date", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date", + "example": "2025-01-01" + }, + "description": "Date (Y-m-d format)" + }, + { + "name": "location_code", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "gate_code", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Success" + } + } + } + } + } + } + }, + "/retribusi/v1/dashboard/daily": { + "get": { + "tags": ["Dashboard"], + "summary": "Get daily chart data", + "description": "Data untuk line chart harian", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "start_date", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "end_date", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "location_code", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "gate_code", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Success" + } + } + } + } + } + } + }, + "/retribusi/v1/realtime/stream": { + "get": { + "tags": ["Realtime"], + "summary": "SSE Stream", + "description": "Server-Sent Events stream untuk real-time updates", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "last_id", + "in": "query", + "schema": { + "type": "integer" + }, + "description": "Last event ID received" + }, + { + "name": "location_code", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "SSE stream", + "content": { + "text/event-stream": { + "schema": { + "type": "string" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + } +} + diff --git a/public/index.php b/public/index.php index 9d56a31..d85d1bd 100644 --- a/public/index.php +++ b/public/index.php @@ -19,6 +19,47 @@ AppConfig::loadEnv(__DIR__ . '/..'); // Bootstrap application $app = AppBootstrap::create(); +// Root route - redirect to docs +$app->get('/', function ($request, $response) { + return $response + ->withHeader('Location', '/docs') + ->withStatus(302); +}); + +// Docs route - serve Swagger UI +$app->get('/docs', function ($request, $response) { + $docsPath = __DIR__ . '/docs/index.html'; + + if (!file_exists($docsPath)) { + return $response + ->withStatus(404) + ->withHeader('Content-Type', 'text/html') + ->getBody()->write('

Documentation not found

'); + } + + $html = file_get_contents($docsPath); + $response->getBody()->write($html); + + return $response->withHeader('Content-Type', 'text/html'); +}); + +// Serve OpenAPI JSON +$app->get('/docs/openapi.json', function ($request, $response) { + $openApiPath = __DIR__ . '/docs/openapi.json'; + + if (!file_exists($openApiPath)) { + return $response + ->withStatus(404) + ->withHeader('Content-Type', 'application/json') + ->getBody()->write(json_encode(['error' => 'OpenAPI spec not found'])); + } + + $json = file_get_contents($openApiPath); + $response->getBody()->write($json); + + return $response->withHeader('Content-Type', 'application/json'); +}); + // Register module routes HealthRoutes::register($app); AuthRoutes::register($app);