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']);