diff --git a/.gitignore b/.gitignore
index d6913264..65486b6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,4 +31,5 @@ yarn-error.log
QWEN.md
.kilocode
.qwen
-/template_ai/
\ No newline at end of file
+/template_ai/
+docs
\ No newline at end of file
diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php
index 578b97ae..afe51c7a 100644
--- a/app/Http/Controllers/DashboardController.php
+++ b/app/Http/Controllers/DashboardController.php
@@ -62,23 +62,33 @@ public function datatableDesaBaru(Request $request)
public function datatableSemuaDesa(Request $request)
{
if ($request->excel) {
+ // Export Excel hanya untuk user yang sudah login
+ if (! auth()->check()) {
+ abort(403, 'Unauthorized');
+ }
$paramDatatable = json_decode($request->get('params'), 1);
$request->merge($paramDatatable);
}
$filters = [
- 'kode_provinsi' => $request->kode_provinsi,
+ 'kode_provinsi' => $request->kode_provinsi,
'kode_kabupaten' => $request->kode_kabupaten,
'kode_kecamatan' => $request->kode_kecamatan,
- 'status' => $request->status,
- 'akses' => $request->akses,
- 'tte' => $request->tte,
- 'versi_lokal' => '',
- 'versi_hosting' => '',
+ 'status' => $request->status,
+ 'akses' => $request->akses,
+ 'tte' => $request->tte,
+ 'versi_lokal' => '',
+ 'versi_hosting' => '',
];
if ($request->ajax() || $request->excel) {
- $query = DataTables::of($this->desa->fillter($filters)->semuaDesa());
+ // Pilih scope berdasarkan status autentikasi
+ // Auth: kolom lebih lengkap (+url_hosting), Publik: hanya kolom aman tanpa PII
+ $queryBuilder = auth()->check()
+ ? $this->desa->fillter($filters)->semuaDesaAuth()
+ : $this->desa->fillter($filters)->semuaDesaPublik();
+
+ $query = DataTables::of($queryBuilder);
if ($request->excel) {
$query->filtering();
@@ -405,32 +415,46 @@ public function datatablePenggunaOpenkab(Request $request)
public function datatablePenggunaOpensid(Request $request)
{
+ // Batasi jumlah baris per request untuk mencegah dump massal data
+ // Auth: max 100 baris, Publik: max 25 baris
+ $maxLength = auth()->check() ? 100 : 25;
+ if ($request->has('length') && (int) $request->length > $maxLength) {
+ $request->merge(['length' => $maxLength]);
+ }
+
$filters = [
- 'kode_provinsi' => $request->kode_provinsi,
+ 'kode_provinsi' => $request->kode_provinsi,
'kode_kabupaten' => $request->kode_kabupaten,
'kode_kecamatan' => $request->kode_kecamatan,
- 'status' => $request->status,
- 'akses' => $request->akses,
- 'tte' => $request->tte,
- 'versi_lokal' => '',
- 'versi_hosting' => '',
+ 'status' => $request->status,
+ 'akses' => $request->akses,
+ 'tte' => $request->tte,
+ 'versi_lokal' => '',
+ 'versi_hosting' => '',
];
if ($request->ajax()) {
- return DataTables::of($this->desa->fillter($filters)->semuaDesa())
+ // Pilih scope berdasarkan status autentikasi
+ // Auth: kolom lebih lengkap (+url_hosting), Publik: hanya kolom aman tanpa PII
+ $query = auth()->check()
+ ? $this->desa->fillter($filters)->semuaDesaAuth()
+ : $this->desa->fillter($filters)->semuaDesaPublik();
+
+ return DataTables::of($query)
->editColumn('modul_tte', function ($item) {
if ($item->modul_tte == 0) {
return 'Tidak Aktif';
} elseif ($item->modul_tte == 1) {
return 'Aktif';
}
- })->editColumn('tanggal', static fn ($item) => ''.formatDateTimeForHuman($item->updated_at).'')
- ->addIndexColumn() // Menambahkan kolom indeks
+ })
+ ->editColumn('tanggal', static fn ($item) => ''.formatDateTimeForHuman($item->updated_at).'')
+ ->addIndexColumn()
->escapeColumns()
->rawColumns(['modul_tte', 'tanggal'])
->make(true);
}
- abort(404); // Mengembalikan 404 jika bukan permintaan AJAX
+ abort(404);
}
}
diff --git a/app/Models/Desa.php b/app/Models/Desa.php
index f239b281..f42dce14 100644
--- a/app/Models/Desa.php
+++ b/app/Models/Desa.php
@@ -217,6 +217,74 @@ public function scopeSemuaDesa($query)
});
}
+ /**
+ * Scope semua desa untuk tampilan publik — whitelist kolom aman, tanpa PII.
+ *
+ * Kolom yang TIDAK disertakan (PII & infrastruktur):
+ * email_desa, telepon, kontak, ip_lokal, ip_hosting,
+ * url_lokal, url_hosting, lat, lng, alamat_kantor,
+ * kode_pos, anjungan, tgl_rekam_*, tgl_akses_*, opensid_valid, jenis.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeSemuaDesaPublik($query)
+ {
+ return $query
+ ->select([
+ 'nama_desa',
+ 'nama_kecamatan',
+ 'kode_kecamatan',
+ 'nama_kabupaten',
+ 'kode_kabupaten',
+ 'nama_provinsi',
+ 'kode_provinsi',
+ 'versi_lokal',
+ 'versi_hosting',
+ 'modul_tte',
+ 'jml_surat_tte',
+ 'updated_at',
+ 'created_at',
+ ])
+ ->selectRaw('(CASE WHEN (versi_hosting IS NULL) THEN versi_lokal WHEN (versi_lokal IS NULL) THEN versi_hosting WHEN (tgl_rekam_hosting > tgl_rekam_lokal) THEN versi_hosting ELSE versi_lokal END) as versi')
+ ->when(session('provinsi'), function ($query, $provinsi) {
+ $query->where('kode_provinsi', $provinsi->kode_prov);
+ });
+ }
+
+ /**
+ * Scope semua desa untuk user terautentikasi — kolom lebih lengkap termasuk URL.
+ * Tetap TIDAK menyertakan PII individu (email_desa, telepon, kontak, IP).
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeSemuaDesaAuth($query)
+ {
+ return $query
+ ->select([
+ 'nama_desa',
+ 'kode_desa',
+ 'nama_kecamatan',
+ 'kode_kecamatan',
+ 'nama_kabupaten',
+ 'kode_kabupaten',
+ 'nama_provinsi',
+ 'kode_provinsi',
+ 'versi_lokal',
+ 'versi_hosting',
+ 'modul_tte',
+ 'jml_surat_tte',
+ 'updated_at',
+ 'created_at',
+ 'url_hosting',
+ ])
+ ->selectRaw('(CASE WHEN (versi_hosting IS NULL) THEN versi_lokal WHEN (versi_lokal IS NULL) THEN versi_hosting WHEN (tgl_rekam_hosting > tgl_rekam_lokal) THEN versi_hosting ELSE versi_lokal END) as versi')
+ ->when(session('provinsi'), function ($query, $provinsi) {
+ $query->where('kode_provinsi', $provinsi->kode_prov);
+ });
+ }
+
public function scopeDesaValid($query)
{
return $query->whereRaw('1 = 1');
diff --git a/catatan_rilis.md b/catatan_rilis.md
index 91ce11ee..9f030bc1 100644
--- a/catatan_rilis.md
+++ b/catatan_rilis.md
@@ -11,4 +11,4 @@ Di rilis v2607.0.0 berisi perbaikan yang diminta Komunitas Open Desa.
#### Penyesuaian Teknis
1. [#695](https://github.com/OpenSID/pantau/issues/695) Upgrade Laravel 13.
-
\ No newline at end of file
+2. [#51] (https://github.com/OpenSID/wiki-keamanan/issues/51) [P0][CRITICAL] Pantau - Endpoint Publik /datatables/pengguna-opensid Bocor Massal 26.099 Desa (PII + Infra).
\ No newline at end of file
diff --git a/routes/web.php b/routes/web.php
index 9b9d724c..f6fb2692 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -95,12 +95,13 @@
});
});
-// datatable
+// datatable — publik dengan rate limiting
+// Data yang dikembalikan dikontrol di controller berdasarkan auth status (Option C Issue #51).
Route::prefix('datatables')->as('datatables:')
+ ->middleware('throttle:60,1')
->group(function () {
Route::get('desa-baru', [DashboardController::class, 'datatableDesaBaru'])->name('desa-baru');
Route::get('semua-desa', [DashboardController::class, 'datatableSemuaDesa'])->name('semua-desa');
- Route::get('kabupaten-kosong', [DashboardController::class, 'datatableKabupatenKosong'])->name('kabupaten-kosong');
Route::get('opendk-baru', [DashboardController::class, 'datatableOpendkBaru'])->name('opendk-baru');
Route::get('openkab-baru', [DashboardController::class, 'datatableOpenkabBaru'])->name('openkab-baru');
Route::get('opensid-baru', [DashboardController::class, 'datatableOpensidBaru'])->name('opensid-baru');
@@ -115,6 +116,13 @@
Route::get('pengguna-pbb', [DashboardController::class, 'datatablePenggunaPbb'])->name('pengguna-pbb');
});
+// datatable — khusus admin, tidak dipakai di halaman publik manapun
+Route::prefix('datatables')->as('datatables:')
+ ->middleware(['auth', 'throttle:60,1'])
+ ->group(function () {
+ Route::get('kabupaten-kosong', [DashboardController::class, 'datatableKabupatenKosong'])->name('kabupaten-kosong');
+ });
+
// Peta
Route::get('peta', [PetaController::class, 'index']);