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