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->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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,7 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => 'Webstatement',
|
'name' => 'Webstatement',
|
||||||
|
|
||||||
|
// ZIP file password configuration
|
||||||
|
'zip_password' => env('WEBSTATEMENT_ZIP_PASSWORD', 'statement123'),
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user