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