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 @@
{{ $surat->file_hash }}
+ + 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 +
| 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 ?? '-' }}) | +
| Status | +Arsip | +
| Hash File (SHA-256) | +{{ $surat->file_hash }} |
+