-
Notifications
You must be signed in to change notification settings - Fork 12
Tambahkan label kolom pengguna Dasbor SiapPakai atau Lisensi Premium #649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3a1627d
6f1dc2c
41800f7
52ce26e
f0f017c
a34b455
a26be15
3f50d64
d9e295a
4c9fa9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,4 +28,7 @@ yarn-error.log | |
| .env.e2e | ||
|
|
||
| # Ignore AI files | ||
| QWEN.md | ||
| QWEN.md | ||
| .kilocode | ||
| .qwen | ||
| /template_ai/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| # AGENTS.md | ||
|
|
||
| This file provides guidance to agents when working with code in this repository. | ||
|
|
||
| ## Commands | ||
|
|
||
| **Testing:** | ||
| - Run PHPUnit tests: `php artisan test` or `./vendor/bin/phpunit` | ||
| - Run single test: `php artisan test --filter TestName` | ||
| - Run E2E tests: `npm run test:e2e` (auto-starts server and runs migrations) | ||
| - Run E2E with UI: `npm run test:e2e:ui` | ||
| - Run E2E in debug mode: `npm run test:e2e:debug` | ||
|
|
||
| **Code Quality:** | ||
| - Fix PHP code style: `vendor/bin/php-cs-fixer fix` | ||
| - Check PHP code style: `vendor/bin/php-cs-fixer fix --dry-run` | ||
|
|
||
| **Development:** | ||
| - Start server: `php artisan serve` | ||
| - Compile assets: `npm run dev` (development) or `npm run prod` (production) | ||
|
|
||
| ## Critical Patterns | ||
|
|
||
| **Security - GitHub API (MANDATORY):** | ||
| - All GitHub API calls MUST use `lastrelease()` helper which validates URLs via `is_trusted_github_api_url()` | ||
| - Only these endpoints are allowed: `/repos/OpenSID/rilis-premium/releases/latest`, `/repos/OpenSID/rilis-pbb/releases/latest`, `/repos/OpenSID/opendk/releases/latest`, `/repos/OpenSID/rilis-opensid-api/releases/latest` | ||
| - Never make direct HTTP requests to GitHub without this validation (SSRF protection) | ||
|
|
||
| **Custom Helpers (Auto-loaded):** | ||
| - `app/Helpers/helper.php` - Core helpers (version checking, domain validation, backup functions) | ||
| - `helpers/general_helper.php` - General helpers (date formatting, image handling, number formatting) | ||
| - Both are auto-loaded via composer.json, no manual imports needed | ||
|
|
||
| **Database & Models:** | ||
| - Desa model uses complex scopes: `jumlahDesa()`, `desaValid()`, `filterWilayah()`, `hostingOnline()`, `hostingOffline()`, `aktif()` | ||
| - Region filtering via `FilterWilayahTrait` and `HasRegionAccess` trait | ||
| - Version checking uses cached GitHub API calls with fallback defaults | ||
| - TEMA_PRO themes: ['Silir', 'Batuah', 'Pusako', 'DeNava', 'Lestari'] | ||
|
|
||
| **E2E Testing:** | ||
| - Configuration loaded from `.env.e2e` file (not `.env`) | ||
| - Test credentials: `eddie.ridwan@gmail.com` / `Admin100%` (from UserSeeder) | ||
| - Authentication state saved in `test-results/storage-state/auth.json` | ||
| - Playwright auto-starts Laravel server via webServer config | ||
| - Global setup runs `php artisan migrate:fresh --seed` automatically | ||
|
|
||
| **Code Style:** | ||
| - PHP CS Fixer configured with Laravel preset (`.php-cs-fixer.php`) | ||
| - Single quotes for strings, short array syntax | ||
| - Ordered imports alphabetically | ||
| - No trailing whitespace, specific brace positioning | ||
| - StyleCI config (`.styleci.yml`) overrides: disables `no_unused_imports` rule | ||
|
|
||
| **Gotchas:** | ||
| - `sudahInstal()` helper checks for `storage_path('installed')` file existence | ||
| - Domain validation uses `fixDomainName()` to normalize URLs | ||
| - Local IP detection via `is_local()` helper (localhost, 192.168, 127.0, 10.x) | ||
| - Backup functions check for `/usr/bin/rclone` existence for cloud storage sync | ||
| - Log permissions can be changed via `changeLogPermissions()` helper | ||
|
|
||
| ## Security & Bug Review Focus | ||
|
|
||
| **Celah Keamanan yang Perlu Diperiksa:** | ||
| - SQL injection pada query builder raw SQL | ||
| - XSS pada output user input | ||
| - CSRF token validation | ||
| - Command injection pada exec() calls | ||
| - Path traversal pada file operations | ||
| - Authentication bypass pada login | ||
| - Authorization bypass pada Admin Wilayah | ||
| - Hardcoded secrets di config files | ||
| - SSRF pada GitHub API calls (cek `is_trusted_github_api_url()`) | ||
|
|
||
| **Bug Logika yang Sering Terjadi:** | ||
| - Race condition pada backup operations | ||
| - Null pointer pada helper functions | ||
| - Memory leak pada large dataset queries | ||
| - Infinite loop pada recursive functions | ||
| - Error handling yang hilang pada HTTP requests | ||
| - Edge case pada date/time calculations |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| <?php | ||
|
|
||
| namespace App\Enums; | ||
|
|
||
| enum Layanan: string | ||
| { | ||
| case SIAPPAKAI = 'siappakai'; | ||
| case PREMIUM = 'premium'; | ||
| case UMUM = 'umum'; | ||
|
|
||
| /** | ||
| * Get all available layanan options as an array. | ||
| * | ||
| * @return array<string, string> | ||
| */ | ||
| public static function toArray(): array | ||
| { | ||
| return [ | ||
| self::SIAPPAKAI->value => 'Siappakai', | ||
| self::PREMIUM->value => 'Premium', | ||
| self::UMUM->value => 'Umum', | ||
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * Get the label for the layanan. | ||
| * | ||
| * @return string | ||
| */ | ||
| public function label(): string | ||
| { | ||
| return match ($this) { | ||
| self::SIAPPAKAI => 'Siappakai', | ||
| self::PREMIUM => 'Premium', | ||
| self::UMUM => 'Umum', | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Get all layanan values. | ||
| * | ||
| * @return array<string> | ||
| */ | ||
| public static function values(): array | ||
| { | ||
| return array_column(self::cases(), 'value'); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ | |
| */ | ||
| function pantau_versi() | ||
| { | ||
| return 'v2603.0.0'; | ||
| return 'v2604.0.0'; | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,9 +2,11 @@ | |
|
|
||
| namespace App\Http\Controllers; | ||
|
|
||
| use App\Enums\Layanan; | ||
| use App\Exports\DesaExport; | ||
| use App\Models\Desa; | ||
| use App\Models\Scopes\RegionAccessScope; | ||
| use App\Services\SebutanDesaService; | ||
| use Illuminate\Http\Request; | ||
| use Maatwebsite\Excel\Facades\Excel; | ||
| use Yajra\DataTables\Facades\DataTables; | ||
|
|
@@ -14,9 +16,13 @@ class LaporanController extends Controller | |
| /** @var Desa */ | ||
| protected $desa; | ||
|
|
||
| public function __construct(Desa $desa) | ||
| /** @var SebutanDesaService */ | ||
| protected $sebutanDesaService; | ||
|
|
||
| public function __construct(Desa $desa, SebutanDesaService $sebutanDesaService) | ||
| { | ||
| $this->desa = $desa; | ||
| $this->sebutanDesaService = $sebutanDesaService; | ||
| } | ||
|
|
||
| public function desa(Request $request) | ||
|
|
@@ -36,43 +42,49 @@ public function desa(Request $request) | |
| 'versi_hosting' => $request->versi_hosting, | ||
| 'tte' => $request->tte, | ||
| 'tipe_pengguna' => $request->tipe_pengguna, | ||
| 'layanan' => $request->layanan, | ||
| 'sebutan_desa' => $request->sebutan_desa, | ||
| ]; | ||
| $hiddenColumns = []; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [CRITICAL] 🚨 Performance: N+1 Query - Missing Eager Loading Masalah: Query DataTables tidak menggunakan eager loading untuk relasi yang diakses di editColumn. Setiap row akan trigger query terpisah untuk Kode: ->make(true);Dampak: Dengan 100 desa di tabel, akan terjadi:
Fix: $query = Desa::query()
->with(['region', 'opendk', 'pbb']) // Eager load relasi
->fillter($request);
return DataTables::eloquent($query)
// ... rest of code
->make(true);
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kode baris berapa, saya tidak menemukan code yang dimaksud |
||
| $adminWilayah = auth()->check() && auth()->user()->isAdminWilayah(); | ||
| if ($adminWilayah) { | ||
| $hiddenColumns[] = 'aksi'; | ||
| $hiddenColumns[] = 'kontak'; | ||
| $hiddenColumns[] = 'kontak'; | ||
| } | ||
|
|
||
| if ($request->ajax() || $request->excel) { | ||
| $query = DataTables::of($this->desa->fillter($fillters)->laporan()); | ||
| if ($request->excel) { | ||
| $query->filtering(); | ||
| if(in_array('aksi', $hiddenColumns)){ | ||
| if (in_array('aksi', $hiddenColumns)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] Kategori: PHP Quality / Best Practices Fix: // Tambahkan parameter ketiga true untuk strict comparison
if (in_array('aksi', $hiddenColumns, true)) {
$dt->removeColumn('aksi');
} |
||
| unset($hiddenColumns['aksi']); | ||
| } | ||
| } | ||
| return Excel::download(new DesaExport($query->results(), $hiddenColumns), 'Desa-yang-memasang-OpenSID.xlsx'); | ||
| } | ||
|
|
||
| return $query->addIndexColumn() | ||
| ->editColumn('kontak', function ($q) { | ||
| $identitas = $q->kontak; | ||
| if ($identitas) { | ||
| return '<div><div>'.$identitas['nama'].'</div><div>'.$identitas['hp'].'</div></div>'; | ||
| // Escape output untuk mencegah XSS | ||
| $nama = e($identitas['nama'] ?? '-'); | ||
| $hp = e($identitas['hp'] ?? '-'); | ||
| return '<div><div>' . $nama . '</div><div>' . $hp . '</div></div>'; | ||
| } | ||
|
|
||
| return ''; | ||
| }) | ||
| ->addColumn('action', function ($data) { | ||
| $delete = '<button data-href="'.url('laporan/desa/'.$data->id).'" class="btn btn-sm btn-danger" data-toggle="modal" data-target="#confirm-delete"><i class="fas fa-trash"></i></button>'; | ||
| $delete = '<button data-href="' . url('laporan/desa/' . $data->id) . '" class="btn btn-sm btn-danger" data-toggle="modal" data-target="#confirm-delete"><i class="fas fa-trash"></i></button>'; | ||
|
|
||
| return '<div class="btn btn-group">'.$delete.'</div>'; | ||
| }) | ||
| ->rawColumns(['action', 'kontak']) | ||
| return '<div class="btn btn-group">' . $delete . '</div>'; | ||
| })->editColumn('layanan', function ($data) { | ||
| return (Layanan::tryFrom($data->layanan))?->label() ?? '-'; | ||
| })->rawColumns(['action', 'kontak']) | ||
| ->make(true); | ||
| } | ||
|
|
||
| return view('laporan.desa', compact('fillters', 'hiddenColumns')); | ||
| $sebutanDesaList = (new SebutanDesaService())->getSebutanDesaList(); | ||
| return view('laporan.desa', compact('fillters', 'hiddenColumns', 'sebutanDesaList')); | ||
| } | ||
|
|
||
| public function deleteDesa(Desa $desa) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
|
|
||
| namespace App\Http\Requests; | ||
|
|
||
| use App\Enums\Layanan; | ||
| use App\Models\Region; | ||
| use Illuminate\Foundation\Http\FormRequest; | ||
| use Illuminate\Validation\Rule; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] Kategori: PHP Quality / Validation Fix: public function attributes(): array
{
return [
'region_id' => 'Wilayah',
'nama' => 'Nama',
'kode' => 'Kode',
'hp' => 'No. HP',
'email' => 'Email',
'sebutan_desa' => 'Sebutan Desa', // Tambahkan ini
'layanan' => 'Layanan', // Tambahkan ini
];
} |
||
|
|
@@ -58,6 +59,13 @@ public function rules() | |
| 'sometimes', | ||
| Rule::in(['0', '1']), | ||
| ], | ||
| 'sebutan_desa' => 'nullable|string|max:255', | ||
| 'layanan' => [ | ||
| 'nullable', | ||
| 'string', | ||
| 'max:255', | ||
| Rule::enum(Layanan::class), | ||
| ], | ||
| ]; | ||
| } | ||
|
|
||
|
|
@@ -183,6 +191,8 @@ public function requestData() | |
| 'anjungan', | ||
| 'kontak', | ||
| 'tema', | ||
| 'sebutan_desa', | ||
| 'layanan', | ||
| ]); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,8 @@ class Desa extends Model | |
| 'kontak' => 'array', | ||
| 'anjungan' => 'bool', | ||
| 'tema' => 'string', | ||
| 'sebutan_desa' => 'string', | ||
| 'layanan' => 'string', | ||
| ]; | ||
|
|
||
| /** {@inheritdoc} */ | ||
|
|
@@ -378,9 +380,9 @@ public function scopeLaporan($query) | |
| { | ||
| return $query | ||
| // ->select(['*']) | ||
| ->select(['nama_desa', 'kode_desa', 'nama_kecamatan', 'nama_kabupaten', 'kode_kecamatan', 'kode_kabupaten', 'nama_provinsi', 'kode_provinsi', 'versi_lokal', 'versi_hosting', 'jml_surat_tte', 'modul_tte', 'jml_penduduk', 'jml_artikel', 'jml_surat_keluar', 'jml_bantuan', 'jml_mandiri', 'jml_pengguna', 'jml_unsur_peta', 'jml_persil', 'jml_dokumen', 'jml_keluarga', 'kontak', 'tema']) | ||
| ->select(['nama_desa', 'kode_desa', 'nama_kecamatan', 'nama_kabupaten', 'kode_kecamatan', 'kode_kabupaten', 'nama_provinsi', 'kode_provinsi', 'versi_lokal', 'versi_hosting', 'jml_surat_tte', 'modul_tte', 'jml_penduduk', 'jml_artikel', 'jml_surat_keluar', 'jml_bantuan', 'jml_mandiri', 'jml_pengguna', 'jml_unsur_peta', 'jml_persil', 'jml_dokumen', 'jml_keluarga', 'kontak', 'tema', 'layanan', 'sebutan_desa']) | ||
| ->selectRaw('greatest(coalesce(tgl_akses_lokal, 0), coalesce(tgl_akses_hosting, 0)) as tgl_akses') | ||
| ->when(auth()->check() == true, function ($query) { | ||
| ->when(auth()->check() === true, function ($query) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MEDIUM] 🔒 Security: Missing Authorization Check on scopeFillter Masalah: Kode: ->when(auth()->check() === true, function ($query) {
if (auth()->user()->kode_desa) {
$query->where('kode_desa', auth()->user()->kode_desa);
}
})Risiko: PoC (Chrome Console): // Skenario: User dengan role "operator desa" tapi kode_desa = null
// Langkah 1: Login sebagai user dengan kode_desa kosong
// Langkah 2: Akses endpoint laporan desa
const testIDOR = async () => {
// Request ke endpoint laporan desa
const resp = await fetch('/laporan/desa', {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
const data = await resp.json();
// Jika kode_desa user = null, response akan berisi SEMUA desa
console.log('Total desa yang bisa diakses:', data.recordsTotal);
console.log('Data desa:', data.data);
// Expected: Hanya desa user sendiri
// Actual: Semua desa jika kode_desa = null
};
testIDOR();Fix: ->when(auth()->check() === true, function ($query) {
$user = auth()->user();
// Jika user memiliki kode_desa, filter berdasarkan itu
if ($user->kode_desa) {
$query->where('kode_desa', $user->kode_desa);
}
// Jika user tidak punya kode_desa tapi bukan superadmin, block access
elseif (!$user->hasRole('superadmin')) {
// Return empty result untuk user tanpa kode_desa dan bukan superadmin
$query->whereRaw('1 = 0');
}
// Superadmin bisa lihat semua (no additional filter)
})Rekomendasi Tambahan: // app/Policies/DesaPolicy.php
public function viewAny(User $user)
{
return $user->hasRole('superadmin') || $user->kode_desa !== null;
}
public function view(User $user, Desa $desa)
{
return $user->hasRole('superadmin') || $user->kode_desa === $desa->kode_desa;
} |
||
| $query->selectRaw('url_lokal, url_hosting'); | ||
| }) | ||
| ->when(session('provinsi'), function ($query, $provinsi) { | ||
|
|
@@ -403,6 +405,8 @@ public function scopeFillter($query, array $fillters) | |
| 'versi_hosting' => null, | ||
| 'tte' => null, | ||
| 'tipe_pengguna' => null, | ||
| 'layanan' => null, | ||
| 'sebutan_desa' => null, | ||
| ], $fillters); | ||
|
|
||
| return $query->select(['*']) | ||
|
|
@@ -466,6 +470,12 @@ public function scopeFillter($query, array $fillters) | |
| $sub->where($this->getTable().'.versi_lokal', 'NOT LIKE', '%-premium%') | ||
| ->orWhereNull($this->getTable().'.versi_lokal'); | ||
| }); | ||
| }) | ||
| ->when($fillters['layanan'], function ($query, $layanan) { | ||
| $query->layanan($layanan); | ||
| }) | ||
| ->when($fillters['sebutan_desa'], function ($query, $sebutanDesa) { | ||
| $query->sebutanDesa($sebutanDesa); | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -624,6 +634,38 @@ public function scopeHostingOffline($query) | |
| return $query->whereNotNull($this->getTable() . '.versi_lokal')->whereNull($this->getTable() . '.versi_hosting'); | ||
| } | ||
|
|
||
| /** | ||
| * Scope a query by layanan. | ||
| * | ||
| * @param \Illuminate\Database\Eloquent\Builder $query | ||
| * @param string|null $layanan | ||
| * @return \Illuminate\Database\Eloquent\Builder | ||
| */ | ||
| public function scopeLayanan($query, $layanan = null) | ||
| { | ||
| if ($layanan === null) { | ||
| return $query; | ||
| } | ||
|
|
||
| return $query->where($this->getTable() . '.layanan', $layanan); | ||
| } | ||
|
|
||
| /** | ||
| * Scope a query by sebutan desa. | ||
| * | ||
| * @param \Illuminate\Database\Eloquent\Builder $query | ||
| * @param string|null $sebutanDesa | ||
| * @return \Illuminate\Database\Eloquent\Builder | ||
| */ | ||
| public function scopeSebutanDesa($query, $sebutanDesa = null) | ||
| { | ||
| if ($sebutanDesa === null) { | ||
| return $query; | ||
| } | ||
|
|
||
| return $query->where($this->getTable() . '.sebutan_desa', $sebutanDesa); | ||
| } | ||
|
|
||
| /** | ||
| * Scope a query kecamatan OpenSID. | ||
| * | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[CRITICAL] 🚨 Performance: Missing Pagination - Memory Exhaustion Risk
Masalah: Export menggunakan
->get()tanpa limit, load semua desa ke memory sekaligusKode:
Dampak: Dengan sistem tracking 100 juta+ request/hari:
Fix:
Atau gunakan cursor() untuk memory efficiency: