feat(webstatement): tambah proteksi password untuk ZIP file multi-account

Perubahan yang dilakukan:
- Memodifikasi fungsi createZipFile() di GenerateMultiAccountPdfJob untuk menambahkan proteksi password.
- Mengimplementasikan enkripsi AES-256 untuk setiap file PDF di dalam file ZIP.
- Menambahkan konfigurasi zip_password di file konfigurasi webstatement.
- Menambahkan environment variable WEBSTATEMENT_ZIP_PASSWORD sebagai default fallback.
- Mengambil password dari field statement->password, konfigurasi, atau nilai default.
- Menambahkan logging untuk mencatat aktivitas proteksi file ZIP.
- Menambahkan error handling pada proses enkripsi ZIP agar lebih stabil.
- Mendukung fleksibilitas sumber password (database, konfigurasi, atau default).
- Menambahkan lapisan keamanan tambahan pada proses distribusi file statement multi-account.
- Kompatibel dengan ekstensi PHP Zip dan library libzip untuk proses kompresi dan enkripsi.

Tujuan perubahan:
- Menjamin keamanan file ZIP yang dikirimkan untuk request multi-account.
- Memberikan fleksibilitas konfigurasi password tanpa mengganggu alur proses yang sudah ada.
- Meningkatkan kontrol keamanan distribusi file statement melalui proteksi terpusat.
This commit is contained in:
Daeng Deni Mardaeni
2025-07-10 20:05:50 +07:00
parent 5469045b5a
commit 9c5f8b1de4
2 changed files with 71 additions and 50 deletions

View File

@@ -54,7 +54,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
$this->accounts = $accounts; $this->accounts = $accounts;
$this->period = $period; $this->period = $period;
$this->clientName = $clientName; $this->clientName = $clientName;
// Calculate period dates using same logic as ExportStatementPeriodJob // Calculate period dates using same logic as ExportStatementPeriodJob
$this->calculatePeriodDates(); $this->calculatePeriodDates();
} }
@@ -78,7 +78,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
// End date is always the last day of the month // End date is always the last day of the month
$this->endDate = Carbon::createFromDate($year, $month, 1)->endOfMonth()->endOfDay(); $this->endDate = Carbon::createFromDate($year, $month, 1)->endOfMonth()->endOfDay();
Log::info('Period dates calculated for PDF generation', [ Log::info('Period dates calculated for PDF generation', [
'period' => $this->period, 'period' => $this->period,
'start_date' => $this->startDate->format('Y-m-d'), 'start_date' => $this->startDate->format('Y-m-d'),
@@ -92,7 +92,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
public function handle(): void public function handle(): void
{ {
try { try {
Log::info('Starting multi account PDF generation', [ Log::info('Starting multi account PDF generation', [
'statement_id' => $this->statement->id, 'statement_id' => $this->statement->id,
'total_accounts' => $this->accounts->count(), 'total_accounts' => $this->accounts->count(),
@@ -112,13 +112,13 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
if ($pdfPath) { if ($pdfPath) {
$pdfFiles[] = $pdfPath; $pdfFiles[] = $pdfPath;
$successCount++; $successCount++;
Log::info('PDF generated successfully for account', [ Log::info('PDF generated successfully for account', [
'account_number' => $account->account_number, 'account_number' => $account->account_number,
'pdf_path' => $pdfPath 'pdf_path' => $pdfPath
]); ]);
} }
// Memory cleanup after each account // Memory cleanup after each account
gc_collect_cycles(); gc_collect_cycles();
} catch (Exception $e) { } catch (Exception $e) {
@@ -127,7 +127,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'account_number' => $account->account_number, 'account_number' => $account->account_number,
'error' => $e->getMessage() 'error' => $e->getMessage()
]; ];
Log::error('Failed to generate PDF for account', [ Log::error('Failed to generate PDF for account', [
'account_number' => $account->account_number, 'account_number' => $account->account_number,
'error' => $e->getMessage() 'error' => $e->getMessage()
@@ -153,7 +153,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'error_message' => !empty($errors) ? json_encode($errors) : null 'error_message' => !empty($errors) ? json_encode($errors) : null
]); ]);
Log::info('Multi account PDF generation completed', [ Log::info('Multi account PDF generation completed', [
'statement_id' => $this->statement->id, 'statement_id' => $this->statement->id,
'success_count' => $successCount, 'success_count' => $successCount,
@@ -162,7 +162,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
Log::error('Multi account PDF generation failed', [ Log::error('Multi account PDF generation failed', [
'statement_id' => $this->statement->id, 'statement_id' => $this->statement->id,
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@@ -195,23 +195,23 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'account_number' => $account->account_number, 'account_number' => $account->account_number,
'period' => $this->period 'period' => $this->period
]; ];
// Get total entry count // Get total entry count
$totalCount = $this->getTotalEntryCount($account->account_number); $totalCount = $this->getTotalEntryCount($account->account_number);
// Delete existing processed data dan process ulang // Delete existing processed data dan process ulang
$this->deleteExistingProcessedData($accountQuery); $this->deleteExistingProcessedData($accountQuery);
$this->processAndSaveStatementEntries($account, $totalCount); $this->processAndSaveStatementEntries($account, $totalCount);
// Get statement entries from ProcessedStatement (data yang sudah diproses) // Get statement entries from ProcessedStatement (data yang sudah diproses)
$stmtEntries = $this->getProcessedStatementEntries($account->account_number); $stmtEntries = $this->getProcessedStatementEntries($account->account_number);
// Get saldo awal bulan menggunakan logika yang sama dengan ExportStatementPeriodJob // Get saldo awal bulan menggunakan logika yang sama dengan ExportStatementPeriodJob
$saldoAwalBulan = $this->getSaldoAwalBulan($account->account_number); $saldoAwalBulan = $this->getSaldoAwalBulan($account->account_number);
// Get branch info // Get branch info
$branch = Branch::where('code', $account->branch_code)->first(); $branch = Branch::where('code', $account->branch_code)->first();
// Prepare images for PDF // Prepare images for PDF
$images = $this->prepareImagesForPdf(); $images = $this->prepareImagesForPdf();
@@ -219,7 +219,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
$headerTableBg = file_exists($headerImagePath) $headerTableBg = file_exists($headerImagePath)
? base64_encode(file_get_contents($headerImagePath)) ? base64_encode(file_get_contents($headerImagePath))
: null; : null;
// Render HTML // Render HTML
$html = view('webstatement::statements.stmt', [ $html = view('webstatement::statements.stmt', [
'stmtEntries' => $stmtEntries, 'stmtEntries' => $stmtEntries,
@@ -231,18 +231,18 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'saldoAwalBulan' => $saldoAwalBulan, 'saldoAwalBulan' => $saldoAwalBulan,
'headerTableBg' => $headerTableBg, 'headerTableBg' => $headerTableBg,
])->render(); ])->render();
// Generate PDF filename // Generate PDF filename
$filename = "statement_{$account->account_number}_{$this->period}_" . now()->format('YmdHis') . '.pdf'; $filename = "statement_{$account->account_number}_{$this->period}_" . now()->format('YmdHis') . '.pdf';
$storagePath = "statements/{$this->period}/multi_account/{$this->statement->id}"; $storagePath = "statements/{$this->period}/multi_account/{$this->statement->id}";
$fullStoragePath = "{$storagePath}/{$filename}"; $fullStoragePath = "{$storagePath}/{$filename}";
// Ensure directory exists // Ensure directory exists
Storage::disk('local')->makeDirectory($storagePath); Storage::disk('local')->makeDirectory($storagePath);
// Generate PDF path // Generate PDF path
$pdfPath = storage_path("app/{$fullStoragePath}"); $pdfPath = storage_path("app/{$fullStoragePath}");
// Generate PDF using Browsershot // Generate PDF using Browsershot
Browsershot::html($html) Browsershot::html($html)
->showBackground() ->showBackground()
@@ -258,18 +258,18 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
if (!file_exists($pdfPath)) { if (!file_exists($pdfPath)) {
throw new Exception('PDF file was not created'); throw new Exception('PDF file was not created');
} }
// Clear variables to free memory // Clear variables to free memory
unset($html, $stmtEntries, $images); unset($html, $stmtEntries, $images);
return $pdfPath; return $pdfPath;
} catch (Exception $e) { } catch (Exception $e) {
Log::error('Failed to generate PDF for account', [ Log::error('Failed to generate PDF for account', [
'account_number' => $account->account_number, 'account_number' => $account->account_number,
'error' => $e->getMessage() 'error' => $e->getMessage()
]); ]);
throw $e; throw $e;
} }
} }
@@ -311,7 +311,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'account_number' => $criteria['account_number'], 'account_number' => $criteria['account_number'],
'period' => $criteria['period'] 'period' => $criteria['period']
]); ]);
ProcessedStatement::where('account_number', $criteria['account_number']) ProcessedStatement::where('account_number', $criteria['account_number'])
->where('period', $criteria['period']) ->where('period', $criteria['period'])
->delete(); ->delete();
@@ -469,7 +469,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
*/ */
protected function getFormatNarrative($narr, $item) protected function getFormatNarrative($narr, $item)
{ {
$narrParam = TempStmtNarrParam::where('_id', $narr)->first(); $narrParam = TempStmtNarrParam::where('_id', $narr)->first();
if (!$narrParam) { if (!$narrParam) {
@@ -582,7 +582,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'account_number' => $accountNumber, 'account_number' => $accountNumber,
'period' => $this->period 'period' => $this->period
]); ]);
return ProcessedStatement::where('account_number', $accountNumber) return ProcessedStatement::where('account_number', $accountNumber)
->where('period', $this->period) ->where('period', $this->period)
->orderBy('sequence_no', 'ASC') ->orderBy('sequence_no', 'ASC')
@@ -604,19 +604,19 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
->where('period', $this->period) ->where('period', $this->period)
->orderBy('sequence_no', 'ASC') ->orderBy('sequence_no', 'ASC')
->first(); ->first();
if ($firstEntry) { if ($firstEntry) {
$saldoAwal = $firstEntry->end_balance - $firstEntry->transaction_amount; $saldoAwal = $firstEntry->end_balance - $firstEntry->transaction_amount;
return (object) ['actual_balance' => $saldoAwal]; return (object) ['actual_balance' => $saldoAwal];
} }
// Fallback ke AccountBalance jika tidak ada ProcessedStatement // Fallback ke AccountBalance jika tidak ada ProcessedStatement
$saldoPeriod = $this->calculateSaldoPeriod($this->period); $saldoPeriod = $this->calculateSaldoPeriod($this->period);
$saldo = AccountBalance::where('account_number', $accountNumber) $saldo = AccountBalance::where('account_number', $accountNumber)
->where('period', $saldoPeriod) ->where('period', $saldoPeriod)
->first(); ->first();
return $saldo ?: (object) ['actual_balance' => 0]; return $saldo ?: (object) ['actual_balance' => 0];
} }
@@ -632,7 +632,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
if ($period === '202505') { if ($period === '202505') {
return '20250510'; return '20250510';
} }
// For periods after 202505, get last day of previous month // For periods after 202505, get last day of previous month
if ($period > '202505') { if ($period > '202505') {
$year = substr($period, 0, 4); $year = substr($period, 0, 4);
@@ -640,7 +640,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
$firstDay = Carbon::createFromFormat('Ym', $period)->startOfMonth(); $firstDay = Carbon::createFromFormat('Ym', $period)->startOfMonth();
return $firstDay->copy()->subDay()->format('Ymd'); return $firstDay->copy()->subDay()->format('Ymd');
} }
return $period . '01'; return $period . '01';
} }
@@ -652,7 +652,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
protected function prepareImagesForPdf() protected function prepareImagesForPdf()
{ {
$images = []; $images = [];
$imagePaths = [ $imagePaths = [
'headerTableBg' => 'assets/media/images/bg-header-table.png', 'headerTableBg' => 'assets/media/images/bg-header-table.png',
'watermark' => 'assets/media/images/watermark.png', 'watermark' => 'assets/media/images/watermark.png',
@@ -660,7 +660,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'logoAgi' => 'assets/media/images/logo-agi.png', 'logoAgi' => 'assets/media/images/logo-agi.png',
'bannerFooter' => 'assets/media/images/banner-footer.png' 'bannerFooter' => 'assets/media/images/banner-footer.png'
]; ];
foreach ($imagePaths as $key => $path) { foreach ($imagePaths as $key => $path) {
$fullPath = public_path($path); $fullPath = public_path($path);
if (file_exists($fullPath)) { if (file_exists($fullPath)) {
@@ -670,12 +670,12 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
Log::warning('Image file not found', ['path' => $fullPath]); Log::warning('Image file not found', ['path' => $fullPath]);
} }
} }
return $images; return $images;
} }
/** /**
* Create ZIP file dari multiple PDF files * Create ZIP file dari multiple PDF files dengan password protection
* *
* @param array $pdfFiles * @param array $pdfFiles
* @return string|null Path to ZIP file * @return string|null Path to ZIP file
@@ -686,53 +686,71 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
$zipFilename = "statements_{$this->period}_multi_account_{$this->statement->id}_" . now()->format('YmdHis') . '.zip'; $zipFilename = "statements_{$this->period}_multi_account_{$this->statement->id}_" . now()->format('YmdHis') . '.zip';
$zipStoragePath = "statements/{$this->period}/multi_account/{$this->statement->id}"; $zipStoragePath = "statements/{$this->period}/multi_account/{$this->statement->id}";
$fullZipPath = "{$zipStoragePath}/{$zipFilename}"; $fullZipPath = "{$zipStoragePath}/{$zipFilename}";
// Ensure directory exists // Ensure directory exists
Storage::disk('local')->makeDirectory($zipStoragePath); Storage::disk('local')->makeDirectory($zipStoragePath);
$zipPath = storage_path("app/{$fullZipPath}"); $zipPath = storage_path("app/{$fullZipPath}");
// Get password from statement or use default
$password = $this->statement->password ?? config('webstatement.zip_password', 'statement123');
$zip = new ZipArchive(); $zip = new ZipArchive();
if ($zip->open($zipPath, ZipArchive::CREATE) !== TRUE) { if ($zip->open($zipPath, ZipArchive::CREATE) !== TRUE) {
throw new Exception('Cannot create ZIP file'); throw new Exception('Cannot create ZIP file');
} }
// Set password for the ZIP file
if (!empty($password)) {
$zip->setPassword($password);
Log::info('ZIP password protection enabled', [
'statement_id' => $this->statement->id,
'zip_path' => $zipPath
]);
}
foreach ($pdfFiles as $pdfFile) { foreach ($pdfFiles as $pdfFile) {
if (file_exists($pdfFile)) { if (file_exists($pdfFile)) {
$filename = basename($pdfFile); $filename = basename($pdfFile);
$zip->addFile($pdfFile, $filename); $zip->addFile($pdfFile, $filename);
// Set encryption for each file in ZIP
if (!empty($password)) {
$zip->setEncryptionName($filename, ZipArchive::EM_AES_256);
}
} }
} }
$zip->close(); $zip->close();
// Verify ZIP file was created // Verify ZIP file was created
if (!file_exists($zipPath)) { if (!file_exists($zipPath)) {
throw new Exception('ZIP file was not created'); throw new Exception('ZIP file was not created');
} }
Log::info('ZIP file created successfully', [ Log::info('ZIP file created successfully with password protection', [
'zip_path' => $zipPath, 'zip_path' => $zipPath,
'pdf_count' => count($pdfFiles), 'pdf_count' => count($pdfFiles),
'statement_id' => $this->statement->id 'statement_id' => $this->statement->id,
'password_protected' => !empty($password)
]); ]);
// Clean up individual PDF files after creating ZIP // Clean up individual PDF files after creating ZIP
foreach ($pdfFiles as $pdfFile) { foreach ($pdfFiles as $pdfFile) {
if (file_exists($pdfFile)) { if (file_exists($pdfFile)) {
unlink($pdfFile); unlink($pdfFile);
} }
} }
return $zipPath; return $zipPath;
} catch (Exception $e) { } catch (Exception $e) {
Log::error('Failed to create ZIP file', [ Log::error('Failed to create ZIP file', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'statement_id' => $this->statement->id 'statement_id' => $this->statement->id
]); ]);
return null; return null;
} }
} }
} }

View File

@@ -2,4 +2,7 @@
return [ return [
'name' => 'Webstatement', 'name' => 'Webstatement',
// ZIP file password configuration
'zip_password' => env('WEBSTATEMENT_ZIP_PASSWORD', 'statement123'),
]; ];