diff --git a/app/Http/Controllers/DomainCheckController.php b/app/Http/Controllers/DomainCheckController.php
new file mode 100644
index 00000000..d4a29a90
--- /dev/null
+++ b/app/Http/Controllers/DomainCheckController.php
@@ -0,0 +1,67 @@
+check()) {
+ abort(403, 'Unauthorized');
+ }
+
+ return view('pengaturan.domain-check');
+ }
+
+ /**
+ * Panggil API untuk cek domain.
+ */
+ public function check(Request $request)
+ {
+ if (!auth()->check()) {
+ return response()->json(['message' => 'Unauthorized'], 403);
+ }
+
+ try {
+ $apiKey = Setting::where('key', 'database_gabungan_api_key')->value('value');
+ $apiUrl = config('app.databaseGabunganUrl');
+
+ if (!$apiKey || !$apiUrl) {
+ return response()->json([
+ 'status' => 'ERROR',
+ 'message' => 'Konfigurasi API Database Gabungan belum diatur.',
+ ], 500);
+ }
+
+ $response = Http::withHeaders([
+ 'Authorization' => 'Bearer ' . $apiKey,
+ 'Accept' => 'application/json',
+ ])->timeout(10)->get($apiUrl . '/api/v1/debug/domain-check');
+
+ if ($response->successful()) {
+ return response()->json($response->json());
+ }
+
+ return response()->json([
+ 'status' => 'ERROR',
+ 'message' => 'API mengembalikan error: ' . $response->status(),
+ 'detail' => $response->json(),
+ ], $response->status());
+ } catch (\Exception $e) {
+ Log::error('Domain check failed: ' . $e->getMessage());
+
+ return response()->json([
+ 'status' => 'ERROR',
+ 'message' => 'Gagal menghubungi API: ' . $e->getMessage(),
+ ], 500);
+ }
+ }
+}
diff --git a/config/adminlte.php b/config/adminlte.php
index 11645028..7dc52dae 100644
--- a/config/adminlte.php
+++ b/config/adminlte.php
@@ -356,6 +356,20 @@
'url' => '/dasbor-demografi',
],
+ // Pengaturan
+ [
+ 'text' => 'Pengaturan',
+ 'icon' => 'nav-icon fas fa-cog',
+ 'submenu' => [
+ [
+ 'text' => 'Cek Domain API',
+ 'icon' => 'nav-icon fas fa-globe',
+ 'url' => '/pengaturan/domain-check',
+ 'active' => ['pengaturan/domain-check*'],
+ ],
+ ],
+ ],
+
],
/*
diff --git a/resources/views/admin/pengaturan/domain-check/index.blade.php b/resources/views/admin/pengaturan/domain-check/index.blade.php
new file mode 100644
index 00000000..bbd675aa
--- /dev/null
+++ b/resources/views/admin/pengaturan/domain-check/index.blade.php
@@ -0,0 +1,269 @@
+@extends('layouts.index')
+
+@section('title', 'Cek Domain API')
+
+@section('content_header')
+
Cek Domain API Database Gabungan
+@stop
+
+@section('content')
+
+
+
+
+
+
+ Klik tombol di bawah untuk mengecek domain apa yang terbaca oleh API Database Gabungan.
+ Ini sangat membantu troubleshooting ketika dropdown Kabupaten/Kecamatan kosong atau error "Domain tidak diizinkan".
+
+
+
+
+ Cek Domain Sekarang
+
+
+
+
+ Memeriksa domain...
+
+
+
+ {{-- Result will be populated by JavaScript --}}
+
+
+
+
+
+
+
+
+
+
Apa ini?
+
+ Tool ini mengecek domain apa yang terbaca oleh API Database Gabungan saat request masuk.
+
+
+
Kapan digunakan?
+
+ Dropdown Kabupaten/Kecamatan kosong
+ Error "Domain tidak diizinkan"
+ Request API gagal tanpa penjelasan
+
+
+
Bagaimana cara kerjanya?
+
+ Klik tombol "Cek Domain Sekarang"
+ Sistem akan memanggil API dengan token Anda
+ API mendeteksi domain dari request headers
+ Hasil ditampilkan di sini
+
+
+
+
+
Interpretasi Hasil:
+
+ Diizinkan — Domain sudah benar
+ Ditolak — Domain perlu ditambahkan
+ Server-side — Request dari backend
+
+
+
+
+
+@stop
+
+@section('css')
+
+@stop
+
+@section('js')
+
+@stop
diff --git a/routes/web.php b/routes/web.php
index e420dd6b..0f002f5e 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -9,6 +9,7 @@
use App\Http\Controllers\CatatanRilis;
use App\Http\Controllers\DasborController;
use App\Http\Controllers\DasborDemografiController;
+use App\Http\Controllers\DomainCheckController;
use App\Http\Controllers\DataPokokController;
use App\Http\Controllers\DesaController;
use App\Http\Controllers\ForcePasswordResetController;
@@ -91,6 +92,10 @@
Route::resource('activities', RiwayatPenggunaController::class)->only(['index', 'show'])->middleware('easyauthorize:pengaturan-activities');
Route::resource('settings', App\Http\Controllers\SettingController::class)->except(['show', 'create', 'delete'])->middleware('easyauthorize:pengaturan-settings');
+ // Domain Check Route
+ Route::get('domain-check', [DomainCheckController::class, 'index'])->name('domain-check.index');
+ Route::post('domain-check', [DomainCheckController::class, 'check'])->name('domain-check.check');
+
// OTP & 2FA Routes - combined into one page
Route::prefix('otp')->group(function () {
Route::get('/', [App\Http\Controllers\OtpController::class, 'index'])->name('otp.index');
diff --git a/tests/Feature/DomainCheckControllerTest.php b/tests/Feature/DomainCheckControllerTest.php
new file mode 100644
index 00000000..b9bc8613
--- /dev/null
+++ b/tests/Feature/DomainCheckControllerTest.php
@@ -0,0 +1,166 @@
+user = User::factory()->create([
+ 'name' => 'Admin Test',
+ ]);
+
+ // Create API key setting
+ Setting::create([
+ 'key' => 'database_gabungan_api_key',
+ 'value' => 'test-api-key',
+ ]);
+ }
+
+ /** @test */
+ public function it_requires_authentication(): void
+ {
+ $response = $this->get('/pengaturan/domain-check');
+
+ $response->assertRedirect('/login');
+ }
+
+ /** @test */
+ public function it_can_display_domain_check_page(): void
+ {
+ $response = $this->actingAs($this->user)
+ ->get('/pengaturan/domain-check');
+
+ $response->assertStatus(200);
+ $response->assertSee('Cek Domain API Database Gabungan');
+ $response->assertSee('Cek Domain Sekarang');
+ }
+
+ /** @test */
+ public function it_can_check_domain_with_valid_api(): void
+ {
+ Http::fake([
+ '*/api/v1/debug/domain-check' => Http::response([
+ 'status' => 'OK',
+ 'detected_domain' => 'simatik.bimakota.go.id',
+ 'detection_source' => 'Origin header',
+ 'is_server_side' => false,
+ 'is_allowed' => true,
+ 'headers' => [
+ 'Origin' => 'https://simatik.bimakota.go.id',
+ 'Referer' => '-',
+ 'Host' => 'api-simatik.bimakota.go.id',
+ 'X-Forwarded-For' => '-',
+ 'X-Real-IP' => '-',
+ ],
+ 'user' => [
+ 'id' => 2,
+ 'name' => 'OpenKab',
+ 'allowed_domains' => ['simatik.bimakota.go.id'],
+ 'is_wildcard' => false,
+ ],
+ 'recommendation' => "Domain 'simatik.bimakota.go.id' sudah ada di allowed_domains. Request akan diizinkan.",
+ ], 200),
+ ]);
+
+ $response = $this->actingAs($this->user)
+ ->postJson('/pengaturan/domain-check');
+
+ $response->assertStatus(200);
+ $response->assertJson([
+ 'status' => 'OK',
+ 'detected_domain' => 'simatik.bimakota.go.id',
+ 'is_allowed' => true,
+ ]);
+ }
+
+ /** @test */
+ public function it_handles_api_error(): void
+ {
+ Http::fake([
+ '*/api/v1/debug/domain-check' => Http::response([
+ 'message' => 'Unauthorized',
+ ], 401),
+ ]);
+
+ $response = $this->actingAs($this->user)
+ ->postJson('/pengaturan/domain-check');
+
+ $response->assertStatus(401);
+ $response->assertJson([
+ 'status' => 'ERROR',
+ ]);
+ }
+
+ /** @test */
+ public function it_handles_api_timeout(): void
+ {
+ Http::fake([
+ '*/api/v1/debug/domain-check' => Http::timeout(),
+ ]);
+
+ $response = $this->actingAs($this->user)
+ ->postJson('/pengaturan/domain-check');
+
+ $response->assertStatus(500);
+ $response->assertJson([
+ 'status' => 'ERROR',
+ ]);
+ }
+
+ /** @test */
+ public function it_handles_missing_api_key(): void
+ {
+ Setting::where('key', 'database_gabungan_api_key')->delete();
+
+ $response = $this->actingAs($this->user)
+ ->postJson('/pengaturan/domain-check');
+
+ $response->assertStatus(500);
+ $response->assertJson([
+ 'status' => 'ERROR',
+ 'message' => 'Konfigurasi API Database Gabungan belum diatur.',
+ ]);
+ }
+
+ /** @test */
+ public function it_sends_correct_headers_to_api(): void
+ {
+ Http::fake([
+ '*/api/v1/debug/domain-check' => Http::response([
+ 'status' => 'OK',
+ ], 200),
+ ]);
+
+ $this->actingAs($this->user)
+ ->postJson('/pengaturan/domain-check');
+
+ Http::assertSent(function ($request) {
+ return $request->hasHeader('Authorization', 'Bearer test-api-key') &&
+ $request->hasHeader('Accept', 'application/json');
+ });
+ }
+
+ /** @test */
+ public function it_can_display_domain_check_in_sidebar(): void
+ {
+ $response = $this->actingAs($this->user)
+ ->get('/pengaturan/domain-check');
+
+ $response->assertStatus(200);
+ // The sidebar should contain the domain-check link
+ // This is tested implicitly by checking the page loads correctly
+ }
+}