diff --git a/app/Http/Controllers/Api/AssetApiController.php b/app/Http/Controllers/Api/AssetApiController.php new file mode 100644 index 0000000..ccf6d6a --- /dev/null +++ b/app/Http/Controllers/Api/AssetApiController.php @@ -0,0 +1,38 @@ +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', + ]) + ); + } +} diff --git a/app/Http/Controllers/Api/AssetTransactionApiController.php b/app/Http/Controllers/Api/AssetTransactionApiController.php new file mode 100644 index 0000000..55b7f5b --- /dev/null +++ b/app/Http/Controllers/Api/AssetTransactionApiController.php @@ -0,0 +1,57 @@ +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); + } +} diff --git a/app/Http/Middleware/VerifyApiKey.php b/app/Http/Middleware/VerifyApiKey.php new file mode 100644 index 0000000..aafb7f1 --- /dev/null +++ b/app/Http/Middleware/VerifyApiKey.php @@ -0,0 +1,27 @@ +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); + } +} diff --git a/app/Services/Integration/WebhookDispatcher.php b/app/Services/Integration/WebhookDispatcher.php new file mode 100644 index 0000000..1b63c74 --- /dev/null +++ b/app/Services/Integration/WebhookDispatcher.php @@ -0,0 +1,44 @@ +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()]); + } + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 06f58c6..d8c6858 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -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) { diff --git a/resources/views/setup/database.blade.php b/resources/views/setup/database.blade.php index d5226de..36163bf 100644 --- a/resources/views/setup/database.blade.php +++ b/resources/views/setup/database.blade.php @@ -15,8 +15,8 @@ $bodyClass = 'bg-white';
  1. Buat database (mis. asset_management_system).
  2. Isi kredensial di file .env (DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD).
  3. -
  4. Jalankan migrasi dan seeder: php artisan migrate --seed.
  5. -
  6. Setelah itu, jalankan wizard di /setup bila diperlukan.
  7. +
  8. Klik salah satu tombol di bawah untuk menjalankan migrasi (dengan/atau tanpa sample data).
  9. +
  10. Setelah migrasi, lanjutkan wizard di /setup.
@if($errors->any())
@@ -25,9 +25,17 @@ $bodyClass = 'bg-white'; @endforeach
@endif -
- Kembali - Buka Setup +
+
+ @csrf + +
+
+ @csrf + + +
+ Kembali
diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..3c67e07 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,14 @@ +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']); +});