From e1b7df6b4faa626fb3341145b59140d84e78937e Mon Sep 17 00:00:00 2001 From: lukman48 Date: Fri, 12 Jun 2026 13:13:53 +0700 Subject: [PATCH 1/5] feat: sediakan pengelolaan QR Code pengganti TTE - Generate QR Code per surat menggunakan endroid/qr-code - Sematkan QR Code di halaman terakhir surat menggunakan FPDI - QR Code berisi tautan ke halaman verifikasi surat - Simpan hash SHA-256 dari isi file untuk verifikasi keaslian - Sediakan fitur verifikasi file unggahan (unggah PDF, sistem cocokkan hash) - Penyematan QR Code hanya bisa dilakukan oleh pengguna camat - Tampilkan kolom hash di arsip surat dan halaman verifikasi Closes #219 --- .../Surat/PermohonanController.php | 95 +++++++++++++++++++ .../Controllers/Surat/SuratController.php | 43 ++++++++- app/Models/Surat.php | 1 + composer.json | 2 + ...00001_add_file_hash_to_log_surat_table.php | 22 +++++ .../views/layouts/fragments/sidebar.blade.php | 2 + resources/views/surat/arsip.blade.php | 7 ++ .../views/surat/permohonan/show.blade.php | 59 +++++++++++- resources/views/surat/qrcode.blade.php | 23 +++-- .../views/surat/verifikasi/hasil.blade.php | 70 ++++++++++++++ .../views/surat/verifikasi/index.blade.php | 43 +++++++++ routes/web.php | 5 + 12 files changed, 363 insertions(+), 9 deletions(-) create mode 100644 database/migrations/2026_06_12_000001_add_file_hash_to_log_surat_table.php create mode 100644 resources/views/surat/verifikasi/hasil.blade.php create mode 100644 resources/views/surat/verifikasi/index.blade.php diff --git a/app/Http/Controllers/Surat/PermohonanController.php b/app/Http/Controllers/Surat/PermohonanController.php index d101b9a0e0..43885fd3e7 100644 --- a/app/Http/Controllers/Surat/PermohonanController.php +++ b/app/Http/Controllers/Surat/PermohonanController.php @@ -38,12 +38,18 @@ use App\Models\DataDesa; use App\Models\LogTte; use App\Models\Surat; +use Endroid\QrCode\Builder\Builder; +use Endroid\QrCode\Encoding\Encoding; +use Endroid\QrCode\ErrorCorrectionLevel; +use Endroid\QrCode\RoundBlockSizeMode; +use Endroid\QrCode\Writer\PngWriter; use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Psr7; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; +use setasign\Fpdi\Fpdi; use Yajra\DataTables\DataTables; class PermohonanController extends Controller @@ -289,6 +295,95 @@ public function passphrase(Request $request, $id) } } + public function tandatanganQr($id) + { + $surat = Surat::findOrFail($id); + + if ($surat->log_verifikasi != LogVerifikasiSurat::ProsesTTE) { + return response()->json(['status' => false, 'pesan_error' => 'Surat tidak dalam tahap penandatanganan.'], 400); + } + + $user = auth()->user()->pengurus_id; + if ($user != $this->akun_camat->id) { + return response()->json(['status' => false, 'pesan_error' => 'Hanya camat yang dapat menandatangani surat.'], 403); + } + + DB::beginTransaction(); + + try { + $file_path = public_path("storage/surat/{$surat->file}"); + $file_info = pathinfo($file_path); + $signed_path = public_path("storage/surat/{$file_info['filename']}_signed.pdf"); + + $verificationUrl = route('surat.arsip.qrcode', $surat->id); + + $qrCode = Builder::create() + ->writer(new PngWriter()) + ->data($verificationUrl) + ->encoding(new Encoding('UTF-8')) + ->errorCorrectionLevel(ErrorCorrectionLevel::High) + ->size(200) + ->margin(10) + ->roundBlockSizeMode(RoundBlockSizeMode::Margin) + ->build(); + + $qrTempPath = public_path('storage/surat/qr_temp_' . $surat->id . '.png'); + $qrCode->saveToFile($qrTempPath); + + $pdf = new Fpdi(); + $pageCount = $pdf->setSourceFile($file_path); + + for ($i = 1; $i <= $pageCount; $i++) { + $templateId = $pdf->importPage($i); + $size = $pdf->getTemplateSize($templateId); + $pdf->AddPage($size['orientation'], [$size['width'], $size['height']]); + $pdf->useTemplate($templateId); + + if ($i === $pageCount) { + $qrSize = 40; + $margin = 10; + $pdf->Image($qrTempPath, $size['width'] - $qrSize - $margin, $size['height'] - $qrSize - $margin, $qrSize, $qrSize); + } + } + + $pdf->Output('F', $signed_path); + + @unlink($qrTempPath); + + $fileHash = hash_file('sha256', $signed_path); + + @unlink($file_path); + rename($signed_path, $file_path); + + $surat->update([ + 'status' => StatusSurat::Arsip, + 'log_verifikasi' => LogVerifikasiSurat::SudahTTE, + 'file_hash' => $fileHash, + ]); + + DB::commit(); + + return response()->json([ + 'status' => true, + 'pesan_error' => 'success', + 'jenis' => 'success', + ]); + } catch (\Exception $e) { + DB::rollback(); + Log::error('QR Code signing failed', [ + 'error' => $e->getMessage(), + 'user_id' => auth()->id(), + 'surat_id' => $id, + ]); + + return response()->json([ + 'status' => false, + 'pesan_error' => $e->getMessage(), + 'jenis' => 'Exception', + ]); + } + } + protected function response($notif = []) { LogTte::create([ diff --git a/app/Http/Controllers/Surat/SuratController.php b/app/Http/Controllers/Surat/SuratController.php index 91bee0f39a..2140b0ffc0 100644 --- a/app/Http/Controllers/Surat/SuratController.php +++ b/app/Http/Controllers/Surat/SuratController.php @@ -37,6 +37,7 @@ use App\Models\Profil; use App\Models\SettingAplikasi; use App\Models\Surat; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Yajra\DataTables\DataTables; @@ -79,7 +80,13 @@ public function getData() } return $row->nama_penduduk; }) - ->rawColumns(['aksi'])->make(); + ->addColumn('hash', function ($row) { + if ($row->file_hash) { + return '' . substr($row->file_hash, 0, 16) . '...'; + } + return '-'; + }) + ->rawColumns(['aksi', 'hash'])->make(); } public function download($id) @@ -136,4 +143,38 @@ public function qrcode($id) return view('surat.qrcode', compact('surat', 'profil')); } + + public function verifikasi() + { + $page_title = 'Verifikasi Surat'; + $page_description = 'Verifikasi keaslian surat digital'; + + return view('surat.verifikasi.index', compact('page_title', 'page_description')); + } + + public function verifikasiStore(Request $request) + { + $request->validate([ + 'file' => 'required|mimes:pdf|max:5120', + ]); + + try { + $uploadedFile = $request->file('file'); + $uploadedHash = hash_file('sha256', $uploadedFile->getRealPath()); + + $surat = Surat::where('file_hash', $uploadedHash)->where('status', StatusSurat::Arsip)->first(); + + if (!$surat) { + return back()->with('error', 'Surat tidak ditemukan atau file tidak sesuai dengan surat yang diterbitkan.'); + } + + return view('surat.verifikasi.hasil', compact('surat')); + } catch (\Exception $e) { + Log::error('Verifikasi surat failed', [ + 'error' => $e->getMessage(), + ]); + + return back()->with('error', 'Terjadi kesalahan saat memverifikasi surat.'); + } + } } diff --git a/app/Models/Surat.php b/app/Models/Surat.php index de192b530b..ecc3bea612 100644 --- a/app/Models/Surat.php +++ b/app/Models/Surat.php @@ -50,6 +50,7 @@ class Surat extends Model 'nomor', 'nama', 'file', + 'file_hash', 'keterangan', 'log_verifikasi', 'verifikasi_operator', diff --git a/composer.json b/composer.json index 857ec7c501..10a46adbc0 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,8 @@ "cocur/slugify": "4.6.0", "cviebrock/eloquent-sluggable": "^11.0", "doctrine/dbal": "^3.6", + "endroid/qr-code": "^5.0", + "setasign/fpdi": "^2.3", "guzzlehttp/guzzle": "^7.2", "hexadog/laravel-themes-manager": "^1.13", "jaybizzle/crawler-detect": "1.*", diff --git a/database/migrations/2026_06_12_000001_add_file_hash_to_log_surat_table.php b/database/migrations/2026_06_12_000001_add_file_hash_to_log_surat_table.php new file mode 100644 index 0000000000..613a7f92db --- /dev/null +++ b/database/migrations/2026_06_12_000001_add_file_hash_to_log_surat_table.php @@ -0,0 +1,22 @@ +string('file_hash', 64)->nullable()->after('file'); + }); + } + + public function down() + { + Schema::table('das_log_surat', function (Blueprint $table) { + $table->dropColumn('file_hash'); + }); + } +} diff --git a/resources/views/layouts/fragments/sidebar.blade.php b/resources/views/layouts/fragments/sidebar.blade.php index e9502a2ab4..ac14fdb897 100644 --- a/resources/views/layouts/fragments/sidebar.blade.php +++ b/resources/views/layouts/fragments/sidebar.blade.php @@ -385,6 +385,8 @@
  • Arsip
  • +
  • Verifikasi +
  • Pengaturan
  • diff --git a/resources/views/surat/arsip.blade.php b/resources/views/surat/arsip.blade.php index c303985160..c3507de3ea 100644 --- a/resources/views/surat/arsip.blade.php +++ b/resources/views/surat/arsip.blade.php @@ -38,6 +38,7 @@ Nama Penduduk Ditandatangani oleh Tanggal + Hash @@ -105,6 +106,12 @@ class: 'text-center', data: 'tanggal', name: 'tanggal' }, + { + data: 'hash', + name: 'hash', + orderable: false, + searchable: false + }, ] }); diff --git a/resources/views/surat/permohonan/show.blade.php b/resources/views/surat/permohonan/show.blade.php index 8b35e72669..8849cad2e6 100644 --- a/resources/views/surat/permohonan/show.blade.php +++ b/resources/views/surat/permohonan/show.blade.php @@ -39,7 +39,10 @@
    @if ($surat->log_verifikasi == 4) - + + @if ($settings['tte'] && $settings['tte_api'] !== 'demo') + + @endif @else @@ -154,6 +157,60 @@ }) }); + $('#tandatangan-qr').on('click', function() { + Swal.fire({ + title: 'Apakah anda yakin ingin menandatangani surat ini dengan QR Code?', + text: 'Tanda tangan QR Code akan disematkan pada halaman terakhir surat.', + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#3085d6', + cancelButtonColor: '#d33', + confirmButtonText: 'Ya, Tandatangani!', + cancelButtonText: 'Batal', + showLoaderOnConfirm: true, + preConfirm: () => { + return fetch(`{{ route('surat.permohonan.tandatangan_qr', $surat->id) }}`, { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Accept': 'application/json', + }, + }) + .then(response => { + if (!response.ok) { + return response.json().then(err => { throw new Error(err.pesan_error) }); + } + return response.json() + }) + .catch(error => { + Swal.showValidationMessage( + `Request failed: ${error}` + ) + }) + }, + allowOutsideClick: () => !Swal.isLoading() + }).then((result) => { + if (result.isConfirmed) { + let response = result.value + if (response.status == false) { + Swal.fire({ + icon: 'error', + title: 'Gagal', + text: response.pesan_error, + }) + } else { + Swal.fire({ + icon: 'success', + title: 'Surat berhasil ditandatangani dengan QR Code', + showConfirmButton: true, + }).then((result) => { + return window.location.replace(`{{ route('surat.arsip') }}`); + }) + } + } + }) + }); + $('#passphrase').on('click', function() { Swal.fire({ title: 'Apakah anda yakin ingin menandatangani surat ini?', diff --git a/resources/views/surat/qrcode.blade.php b/resources/views/surat/qrcode.blade.php index 3a756c21da..afd4280f84 100644 --- a/resources/views/surat/qrcode.blade.php +++ b/resources/views/surat/qrcode.blade.php @@ -5,7 +5,6 @@ {{ $page_title ?? config('app.name', 'Laravel') }} | {{ $browser_title }} - Document @@ -72,7 +71,7 @@ - penduduk->nama ?> + penduduk->nama ?? $surat->nama_penduduk) ?> Ditandatangani oleh : @@ -87,15 +86,25 @@ : {{ $surat->pengurus->jabatan->nama }} + @if ($surat->file_hash) + + Hash File + : + {{ $surat->file_hash }} + + @endif
    -
    -
    Telah ditandatangani secara elektronik
    -
    -
    - logo bsre +
    +
    Telah ditandatangani
    +

    + Surat ini telah ditandatangani dan diverifikasi oleh Kecamatan {{ $profil->nama_kecamatan }}. + @if ($surat->file_hash) + Keaslian file dapat diverifikasi dengan mengunggah file pada halaman Verifikasi Surat. + @endif +

    diff --git a/resources/views/surat/verifikasi/hasil.blade.php b/resources/views/surat/verifikasi/hasil.blade.php new file mode 100644 index 0000000000..3c09f74ac6 --- /dev/null +++ b/resources/views/surat/verifikasi/hasil.blade.php @@ -0,0 +1,70 @@ +@extends('layouts.dashboard_template') + +@section('title') + Hasil Verifikasi Surat +@endsection + +@section('content') +
    +

    + Hasil Verifikasi Surat + Verifikasi keaslian surat digital +

    + +
    +
    +
    +

    Surat Terverifikasi!

    + File surat yang diunggah telah sesuai dengan data yang diterbitkan oleh Kecamatan. +
    +
    +
    +

    Detail Surat

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Nomor Surat{{ $surat->nomor }}
    Tanggal Surat{{ format_date($surat->tanggal) }}
    Nama Surat{{ $surat->nama }}
    {{ config('setting.sebutan_desa') }}{{ $surat->desa->nama ?? '-' }}
    Atas Nama{{ $surat->penduduk->nama ?? $surat->nama_penduduk }}
    Ditandatangani Oleh{{ $surat->pengurus->nama ?? '-' }} ({{ $surat->pengurus->jabatan->nama ?? '-' }})
    StatusArsip
    Hash File (SHA-256){{ $surat->file_hash }}
    +
    + +
    +
    +@endsection diff --git a/resources/views/surat/verifikasi/index.blade.php b/resources/views/surat/verifikasi/index.blade.php new file mode 100644 index 0000000000..104db6192c --- /dev/null +++ b/resources/views/surat/verifikasi/index.blade.php @@ -0,0 +1,43 @@ +@extends('layouts.dashboard_template') + +@section('title') + Verifikasi Surat +@endsection + +@section('content') +
    +

    + {{ $page_title ?? 'Page Title' }} + {{ $page_description ?? '' }} +

    + +
    +
    + @include('partials.flash_message') +
    +
    +

    Verifikasi Keaslian Surat Digital

    +
    +
    +
    +

    Info!

    + Unggah file surat (PDF) untuk memverifikasi keasliannya. Sistem akan memeriksa apakah file ini benar-benar + diterbitkan oleh Kecamatan {{ $profil->nama_kecamatan ?? '' }}. +
    +
    + @csrf +
    + + +
    +
    + +
    +
    +
    +
    +
    +@endsection diff --git a/routes/web.php b/routes/web.php index 36a60f133a..ef49dbb430 100644 --- a/routes/web.php +++ b/routes/web.php @@ -844,6 +844,7 @@ Route::get('ditolak', ['as' => 'surat.permohonan.ditolak', 'uses' => 'PermohonanController@ditolak']); Route::get('getdataditolak', ['as' => 'surat.permohonan.getdataditolak', 'uses' => 'PermohonanController@getDataDitolak']); Route::post('passphrase/{surat}', ['as' => 'surat.permohonan.passphrase', 'uses' => 'PermohonanController@passphrase']); + Route::post('tandatangan-qr/{surat}', ['as' => 'surat.permohonan.tandatangan_qr', 'uses' => 'PermohonanController@tandatanganQr']); }); // arsip @@ -852,6 +853,10 @@ Route::get('/arsip/qrcode/{surat}', ['as' => 'surat.arsip.qrcode', 'uses' => 'SuratController@qrcode']); Route::get('/arsip/download/{surat}', ['as' => 'surat.arsip.download', 'uses' => 'SuratController@download']); + // verifikasi + Route::get('/verifikasi', ['as' => 'surat.verifikasi', 'uses' => 'SuratController@verifikasi']); + Route::post('/verifikasi', ['as' => 'surat.verifikasi.store', 'uses' => 'SuratController@verifikasiStore']); + // pengaturan Route::get('/pengaturan', ['as' => 'surat.pengaturan', 'uses' => 'SuratController@pengaturan']); Route::put('/pengaturan/update', ['as' => 'surat.pengaturan.update', 'uses' => 'SuratController@pengaturan_update']); From c520601ef2a907c877ee6debe3832e0532086ff4 Mon Sep 17 00:00:00 2001 From: Ahmad Afandi Date: Thu, 16 Jul 2026 16:15:24 +0700 Subject: [PATCH 2/5] fix conflict --- composer.json | 8 +- composer.lock | 253 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 255 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 0edbc580af..6c9c15c030 100644 --- a/composer.json +++ b/composer.json @@ -11,10 +11,9 @@ "require": { "php": "^8.4", "bensampo/laravel-enum": "^6.0", - "cocur/slugify": "4.6.0", - "endroid/qr-code": "^5.0", - "setasign/fpdi": "^2.3", + "cocur/slugify": "4.6.0", "cviebrock/eloquent-sluggable": "^13.0", + "endroid/qr-code": "^6.1", "guzzlehttp/guzzle": "^7.2", "hexadog/laravel-themes-manager": "^1.13", "intervention/image": "3.11.3", @@ -32,6 +31,7 @@ "proengsoft/laravel-jsvalidation": "^4.4", "rap2hpoutre/laravel-log-viewer": "^3.0", "sentry/sentry-laravel": "^4.0", + "setasign/fpdi": "^2.6", "simplepie/simplepie": "^1.0", "spatie/laravel-backup": "^10.0", "spatie/laravel-fractal": "*", @@ -113,4 +113,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 9df1b18e2d..cdde5e5be0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,63 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "af7dbd4efc87c76122a09f5d75843c39", + "content-hash": "f65ee89e64960bfa404b0112140ce22a", "packages": [ + { + "name": "bacon/bacon-qr-code", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^8.1" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.12", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", + "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.1.1" + }, + "time": "2026-04-05T21:06:35+00:00" + }, { "name": "bensampo/laravel-enum", "version": "v6.14.0", @@ -595,6 +650,56 @@ ], "time": "2026-03-19T14:42:10+00:00" }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -968,6 +1073,78 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "endroid/qr-code", + "version": "6.1.3", + "source": { + "type": "git", + "url": "https://github.com/endroid/qr-code.git", + "reference": "5fa534856ed95649d67c0eab0cabc03ab1d8e0e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/endroid/qr-code/zipball/5fa534856ed95649d67c0eab0cabc03ab1d8e0e2", + "reference": "5fa534856ed95649d67c0eab0cabc03ab1d8e0e2", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^3.0", + "php": "^8.4" + }, + "require-dev": { + "endroid/quality": "dev-main", + "ext-gd": "*", + "khanamiryan/qrcode-detector-decoder": "^2.0.3", + "setasign/fpdf": "^1.8.2" + }, + "suggest": { + "ext-gd": "Enables you to write PNG images", + "khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator", + "roave/security-advisories": "Makes sure package versions with known security issues are not installed", + "setasign/fpdf": "Enables you to use the PDF writer" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Endroid\\QrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeroen van den Enden", + "email": "info@endroid.nl" + } + ], + "description": "Endroid QR Code", + "homepage": "https://github.com/endroid/qr-code", + "keywords": [ + "code", + "endroid", + "php", + "qr", + "qrcode" + ], + "support": { + "issues": "https://github.com/endroid/qr-code/issues", + "source": "https://github.com/endroid/qr-code/tree/6.1.3" + }, + "funding": [ + { + "url": "https://github.com/endroid", + "type": "github" + } + ], + "time": "2026-02-05T07:01:58+00:00" + }, { "name": "ezyang/htmlpurifier", "version": "v4.19.0", @@ -6063,6 +6240,78 @@ ], "time": "2026-06-11T13:22:14+00:00" }, + { + "name": "setasign/fpdi", + "version": "v2.6.8", + "source": { + "type": "git", + "url": "https://github.com/Setasign/FPDI.git", + "reference": "881945be29a4996ad3d008eb18ddc01fa3df890c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/881945be29a4996ad3d008eb18ddc01fa3df890c", + "reference": "881945be29a4996ad3d008eb18ddc01fa3df890c", + "shasum": "" + }, + "require": { + "ext-zlib": "*", + "php": ">=7.2 <=8.5.99999" + }, + "conflict": { + "setasign/tfpdf": "<1.31" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.52", + "setasign/fpdf": "^1.9.0", + "setasign/tfpdf": "~1.33", + "squizlabs/php_codesniffer": "^3.5", + "tecnickcom/tcpdf": "^6.8" + }, + "suggest": { + "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." + }, + "type": "library", + "autoload": { + "psr-4": { + "setasign\\Fpdi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Slabon", + "email": "jan.slabon@setasign.com", + "homepage": "https://www.setasign.com" + }, + { + "name": "Maximilian Kresse", + "email": "maximilian.kresse@setasign.com", + "homepage": "https://www.setasign.com" + } + ], + "description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.", + "homepage": "https://www.setasign.com/fpdi", + "keywords": [ + "fpdf", + "fpdi", + "pdf" + ], + "support": { + "issues": "https://github.com/Setasign/FPDI/issues", + "source": "https://github.com/Setasign/FPDI/tree/v2.6.8" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", + "type": "tidelift" + } + ], + "time": "2026-06-11T10:37:24+00:00" + }, { "name": "shalvah/upgrader", "version": "0.6.0", @@ -15981,5 +16230,5 @@ "platform-overrides": { "php": "8.4" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } From 9031a57dc27eeb1b0d7371cc7586f6afa57a0d5c Mon Sep 17 00:00:00 2001 From: lukman48 Date: Fri, 17 Jul 2026 10:33:07 +0700 Subject: [PATCH 3/5] fix: perbaiki code review PR #1580 - QR Code TTE - Fix race condition: rename file signed sebelum hapus file asal - Tambahkan LogTte audit logging untuk penandatangan QR Code - Fix temp file naming collision dengan uniqid() - Hapus @unlink error suppression, gunakan file_exists check - Escape HTML di DataTable hash column dengan e() - Gunakan enum LogVerifikasiSurat::ProsesTTE bukan magic number 4 - Null safety untuk $profil dan $surat->penduduk - Pindahkan route verifikasi ke publik (tanpa login) --- .../Controllers/Surat/PermohonanController.php | 16 ++++++++++++---- app/Http/Controllers/Surat/SuratController.php | 2 +- resources/views/surat/permohonan/show.blade.php | 2 +- resources/views/surat/qrcode.blade.php | 2 +- resources/views/surat/verifikasi/hasil.blade.php | 2 +- resources/views/surat/verifikasi/index.blade.php | 2 +- routes/web.php | 10 ++++++---- 7 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app/Http/Controllers/Surat/PermohonanController.php b/app/Http/Controllers/Surat/PermohonanController.php index 43885fd3e7..0df2486178 100644 --- a/app/Http/Controllers/Surat/PermohonanController.php +++ b/app/Http/Controllers/Surat/PermohonanController.php @@ -327,7 +327,7 @@ public function tandatanganQr($id) ->roundBlockSizeMode(RoundBlockSizeMode::Margin) ->build(); - $qrTempPath = public_path('storage/surat/qr_temp_' . $surat->id . '.png'); + $qrTempPath = public_path('storage/surat/qr_temp_' . uniqid() . '_' . $surat->id . '.png'); $qrCode->saveToFile($qrTempPath); $pdf = new Fpdi(); @@ -348,12 +348,15 @@ public function tandatanganQr($id) $pdf->Output('F', $signed_path); - @unlink($qrTempPath); + if (file_exists($qrTempPath)) { + unlink($qrTempPath); + } $fileHash = hash_file('sha256', $signed_path); - @unlink($file_path); - rename($signed_path, $file_path); + if (!rename($signed_path, $file_path)) { + throw new \RuntimeException('Gagal memindahkan file signed ke lokasi asal.'); + } $surat->update([ 'status' => StatusSurat::Arsip, @@ -361,6 +364,11 @@ public function tandatanganQr($id) 'file_hash' => $fileHash, ]); + LogTte::create([ + 'pesan_error' => 'success', + 'jenis' => 'QRCode', + ]); + DB::commit(); return response()->json([ diff --git a/app/Http/Controllers/Surat/SuratController.php b/app/Http/Controllers/Surat/SuratController.php index 2140b0ffc0..bc03f72f24 100644 --- a/app/Http/Controllers/Surat/SuratController.php +++ b/app/Http/Controllers/Surat/SuratController.php @@ -82,7 +82,7 @@ public function getData() }) ->addColumn('hash', function ($row) { if ($row->file_hash) { - return '' . substr($row->file_hash, 0, 16) . '...'; + return '' . e(substr($row->file_hash, 0, 16)) . '...'; } return '-'; }) diff --git a/resources/views/surat/permohonan/show.blade.php b/resources/views/surat/permohonan/show.blade.php index 8849cad2e6..9530a19348 100644 --- a/resources/views/surat/permohonan/show.blade.php +++ b/resources/views/surat/permohonan/show.blade.php @@ -38,7 +38,7 @@