Add public API with API key middleware and webhooks
This commit is contained in:
38
app/Http/Controllers/Api/AssetApiController.php
Normal file
38
app/Http/Controllers/Api/AssetApiController.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AssetApiController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$perPage = min($request->integer('per_page', 25), 100);
|
||||
|
||||
$assets = Asset::query()
|
||||
->with(['status', 'class', 'category', 'assetLocation', 'department', 'personInCharge'])
|
||||
->when($request->filled('status'), fn ($q) => $q->whereHas('status', fn ($qq) => $qq->where('code', $request->string('status'))))
|
||||
->when($request->filled('location_id'), fn ($q) => $q->where('asset_location_id', $request->string('location_id')))
|
||||
->when($request->filled('department_id'), fn ($q) => $q->where('department_id', $request->string('department_id')))
|
||||
->when($request->filled('search'), fn ($q) => $q->where(function ($qq) use ($request) {
|
||||
$s = $request->string('search');
|
||||
$qq->where('code', 'like', "%{$s}%")->orWhere('name', 'like', "%{$s}%");
|
||||
}))
|
||||
->paginate($perPage);
|
||||
|
||||
return response()->json($assets);
|
||||
}
|
||||
|
||||
public function show(Asset $asset)
|
||||
{
|
||||
return response()->json(
|
||||
$asset->load([
|
||||
'status', 'class', 'category', 'assetLocation', 'department',
|
||||
'personInCharge', 'assetUser', 'warranty', 'photos',
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
57
app/Http/Controllers/Api/AssetTransactionApiController.php
Normal file
57
app/Http/Controllers/Api/AssetTransactionApiController.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Asset;
|
||||
use App\Services\Asset\AssetService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AssetTransactionApiController extends Controller
|
||||
{
|
||||
public function __construct(private readonly AssetService $assets)
|
||||
{
|
||||
}
|
||||
|
||||
public function storeMovement(Request $request, Asset $asset)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'to_location_id' => ['nullable', 'uuid', 'exists:asset_locations,id'],
|
||||
'to_department_id' => ['nullable', 'uuid', 'exists:departments,id'],
|
||||
'to_asset_user_id' => ['nullable', 'uuid', 'exists:asset_users,id'],
|
||||
'notes' => ['nullable', 'string'],
|
||||
'performed_at' => ['nullable', 'date'],
|
||||
]);
|
||||
|
||||
$movement = $this->assets->move($asset, $data);
|
||||
|
||||
return response()->json(['message' => 'Movement recorded', 'data' => $movement], 201);
|
||||
}
|
||||
|
||||
public function storeDisposal(Request $request, Asset $asset)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'reason' => ['required', 'string', 'max:255'],
|
||||
'notes' => ['nullable', 'string'],
|
||||
'disposed_at' => ['nullable', 'date'],
|
||||
]);
|
||||
|
||||
$disposal = $this->assets->dispose($asset, $data);
|
||||
|
||||
return response()->json(['message' => 'Disposal recorded', 'data' => $disposal], 201);
|
||||
}
|
||||
|
||||
public function storeAudit(Request $request, Asset $asset)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'status' => ['required', 'string', 'in:matched,missing,damaged'],
|
||||
'notes' => ['nullable', 'string'],
|
||||
'location_id' => ['nullable', 'uuid', 'exists:asset_locations,id'],
|
||||
'audited_at' => ['nullable', 'date'],
|
||||
]);
|
||||
|
||||
$audit = $this->assets->audit($asset, $data);
|
||||
|
||||
return response()->json(['message' => 'Audit recorded', 'data' => $audit], 201);
|
||||
}
|
||||
}
|
||||
27
app/Http/Middleware/VerifyApiKey.php
Normal file
27
app/Http/Middleware/VerifyApiKey.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\System\SystemSettingService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class VerifyApiKey
|
||||
{
|
||||
public function __construct(private readonly SystemSettingService $settings)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$expected = $this->settings->get('integration.api_key') ?? config('services.integration.api_key');
|
||||
$provided = $request->header('X-Api-Key');
|
||||
|
||||
if (empty($expected) || $provided !== $expected) {
|
||||
return response()->json(['message' => 'Unauthorized: invalid API key'], 401);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
44
app/Services/Integration/WebhookDispatcher.php
Normal file
44
app/Services/Integration/WebhookDispatcher.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Integration;
|
||||
|
||||
use App\Services\System\SystemSettingService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class WebhookDispatcher
|
||||
{
|
||||
public function __construct(private readonly SystemSettingService $settings)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim webhook ke endpoint eksternal untuk sinkronisasi ERP/CMMS/Helpdesk.
|
||||
*/
|
||||
public function send(string $event, array $data): void
|
||||
{
|
||||
$enabled = $this->settings->get('integration.webhook_enabled', false);
|
||||
$url = $this->settings->get('integration.webhook_url');
|
||||
if (!$enabled || empty($url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$secret = $this->settings->get('integration.webhook_secret');
|
||||
$signature = $secret ? hash_hmac('sha256', json_encode($data), $secret) : null;
|
||||
|
||||
try {
|
||||
Http::timeout(5)
|
||||
->withHeaders(array_filter([
|
||||
'X-Webhook-Event' => $event,
|
||||
'X-Webhook-Signature' => $signature,
|
||||
]))
|
||||
->post($url, [
|
||||
'event' => $event,
|
||||
'data' => $data,
|
||||
'sent_at' => now()->toIso8601String(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Webhook dispatch failed', ['event' => $event, 'message' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use Illuminate\Console\Scheduling\Schedule;
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__ . '/../routes/web.php',
|
||||
api: __DIR__ . '/../routes/api.php',
|
||||
commands: __DIR__ . '/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
@@ -27,6 +28,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
'session.timeout' => SessionTimeout::class,
|
||||
'audit.request' => AuditRequest::class,
|
||||
'db.ready' => EnsureDatabaseReady::class,
|
||||
'api.key' => \App\Http\Middleware\VerifyApiKey::class,
|
||||
]);
|
||||
})
|
||||
->withSchedule(function (Schedule $schedule) {
|
||||
|
||||
@@ -15,8 +15,8 @@ $bodyClass = 'bg-white';
|
||||
<ol class="text-muted mb-3">
|
||||
<li>Buat database (mis. <code>asset_management_system</code>).</li>
|
||||
<li>Isi kredensial di file <code>.env</code> (DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD).</li>
|
||||
<li>Jalankan migrasi dan seeder: <code>php artisan migrate --seed</code>.</li>
|
||||
<li>Setelah itu, jalankan wizard di <a href="{{ route('setup.index') }}">/setup</a> bila diperlukan.</li>
|
||||
<li>Klik salah satu tombol di bawah untuk menjalankan migrasi (dengan/atau tanpa sample data).</li>
|
||||
<li>Setelah migrasi, lanjutkan wizard di <a href="{{ route('setup.index') }}">/setup</a>.</li>
|
||||
</ol>
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
@@ -25,9 +25,17 @@ $bodyClass = 'bg-white';
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{{ url()->previous() }}" class="btn btn-outline-secondary">Kembali</a>
|
||||
<a href="{{ route('setup.index') }}" class="btn btn-primary">Buka Setup</a>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<form method="POST" action="{{ route('setup.migrate') }}">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-primary">Migrate & Seed Minimal (RBAC)</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ route('setup.migrate') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="with_sample" value="1">
|
||||
<button type="submit" class="btn btn-success">Migrate + Sample Data (Full Seed)</button>
|
||||
</form>
|
||||
<a href="{{ url()->previous() }}" class="btn btn-outline-secondary ms-auto">Kembali</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
14
routes/api.php
Normal file
14
routes/api.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Api\AssetApiController;
|
||||
use App\Http\Controllers\Api\AssetTransactionApiController;
|
||||
|
||||
Route::middleware(['api.key'])->prefix('v1')->group(function () {
|
||||
Route::get('assets', [AssetApiController::class, 'index']);
|
||||
Route::get('assets/{asset}', [AssetApiController::class, 'show']);
|
||||
|
||||
Route::post('assets/{asset}/movements', [AssetTransactionApiController::class, 'storeMovement']);
|
||||
Route::post('assets/{asset}/disposals', [AssetTransactionApiController::class, 'storeDisposal']);
|
||||
Route::post('assets/{asset}/audits', [AssetTransactionApiController::class, 'storeAudit']);
|
||||
});
|
||||
Reference in New Issue
Block a user