From 291e7911147eef268b8b7b0b143ff99d299998cf Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Thu, 28 Aug 2025 15:39:21 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(api):=20implementasi=20autenti?= =?UTF-8?q?kasi=20HMAC=20dan=20validasi=20komprehensif=20untuk=20API=20bal?= =?UTF-8?q?ance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Security: validasi HMAC SHA512 untuk semua request, cek timestamp ISO 8601 dengan toleransi 5 menit, autentikasi API key, dan wajib header X-Api-Key, X-Signature, X-Timestamp. - Input validation: account_number numeric 10 digit & exists, start/end date format YYYY-MM-DD dengan aturan range (start ≤ end ≤ today). - Perubahan file: update `app/Http/Requests/BalanceSummaryRequest.php` (HMAC check, timestamp check, pesan error, logging) dan `config/webstatement.php` (api_key, secret_key). - Error handling: konsisten dengan ResponseCode enum; HTTP status 400/401/404; pesan error jelas (Bahasa Indonesia) + logging. - Testing: Postman collection diperbarui untuk kasus negatif & edge cases; backward compatibility dijaga. - Breaking changes: endpoint kini mewajibkan 3 header (X-Api-Key, X-Signature, X-Timestamp); account number wajib 10 digit numeric; format tanggal strict. - ENV: tambahkan `WEBSTATEMENT_API_KEY` dan `WEBSTATEMENT_SECRET_KEY` (dipetakan ke `config/webstatement.php`). --- app/Http/Requests/BalanceSummaryRequest.php | 118 +++++++++++++++++--- config/config.php | 13 +++ 2 files changed, 114 insertions(+), 17 deletions(-) diff --git a/app/Http/Requests/BalanceSummaryRequest.php b/app/Http/Requests/BalanceSummaryRequest.php index 6835df2..0e7a9f2 100644 --- a/app/Http/Requests/BalanceSummaryRequest.php +++ b/app/Http/Requests/BalanceSummaryRequest.php @@ -102,8 +102,10 @@ class BalanceSummaryRequest extends FormRequest 'account_number' => [ 'required', 'string', - 'max:50', - 'regex:/^[A-Z0-9-]+$/i' // Hanya alphanumeric dan dash + 'max:10', + 'min:10', + 'exists:account_balances,account_number', + 'regex:/^[0-9]+$/' // Numeric only ], 'start_date' => [ 'required', @@ -130,16 +132,19 @@ class BalanceSummaryRequest extends FormRequest public function messages(): array { return [ + 'account_number.exists' => 'Nomor rekening tidak ditemukan.', 'account_number.required' => 'Nomor rekening wajib diisi.', 'account_number.string' => 'Nomor rekening harus berupa teks.', 'account_number.max' => 'Nomor rekening maksimal :max karakter.', - 'account_number.regex' => 'Nomor rekening hanya boleh mengandung huruf, angka, dan strip.', + 'account_number.min' => 'Nomor rekening minimal :min karakter.', + 'account_number.regex' => 'Nomor rekening hanya boleh mengandung angka.', 'start_date.required' => 'Tanggal awal wajib diisi.', 'start_date.date_format' => 'Format tanggal awal harus YYYY-MM-DD.', 'start_date.before_or_equal' => 'Tanggal awal harus sebelum atau sama dengan tanggal akhir.', 'end_date.required' => 'Tanggal akhir wajib diisi.', 'end_date.date_format' => 'Format tanggal akhir harus YYYY-MM-DD.', 'end_date.after_or_equal' => 'Tanggal akhir harus sesudah atau sama dengan tanggal awal.', + 'end_date.before_or_equal' => 'Tanggal akhir harus sebelum atau sama dengan hari ini.', ]; } @@ -155,6 +160,18 @@ class BalanceSummaryRequest extends FormRequest { $errors = $validator->errors(); + if($errors->has('account_number') && $errors->first('account_number') === 'Nomor rekening tidak ditemukan.') { + throw new HttpResponseException( + response()->json( + ResponseCode::ACCOUNT_NOT_FOUND->toResponse( + null, + 'Nomor rekening tidak ditemukan' + ), + ResponseCode::ACCOUNT_NOT_FOUND->getHttpStatus() + ) + ); + } + throw new HttpResponseException( response()->json( ResponseCode::INVALID_FIELD->toResponse( @@ -176,8 +193,89 @@ class BalanceSummaryRequest extends FormRequest protected function failedAuthorization() { $xApiKey = $this->header('X-Api-Key'); + $xSignature = $this->header('X-Signature'); + $xTimestamp = $this->header('X-Timestamp'); + $expectedApiKey = config('webstatement.api_key'); + if(!$xApiKey){ + throw new HttpResponseException( + response()->json( + ResponseCode::INVALID_FIELD->toResponse( + ['errors' => ['X-Api-Key' => 'API Key wajib diisi']], + 'Validasi gagal' + ), + ResponseCode::INVALID_FIELD->getHttpStatus() + ) + ); + } + + if(!$xSignature){ + throw new HttpResponseException( + response()->json( + ResponseCode::INVALID_FIELD->toResponse( + ['errors' => ['X-Signature' => 'Signature wajib diisi']], + 'Validasi gagal' + ), + ResponseCode::INVALID_FIELD->getHttpStatus() + ) + ); + } + + if(!$xTimestamp){ + throw new HttpResponseException( + response()->json( + ResponseCode::INVALID_FIELD->toResponse( + ['errors' => ['X-Timestamp' => 'Timestamp wajib diisi']], + 'Validasi gagal' + ), + ResponseCode::INVALID_FIELD->getHttpStatus() + ) + ); + } + + // Validasi format timestamp ISO 8601 + if (!preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/', $xTimestamp)) { + throw new HttpResponseException( + response()->json( + ResponseCode::INVALID_FIELD->toResponse( + ['errors' => ['X-Timestamp' => 'Format timestamp tidak valid. Gunakan format ISO 8601 (YYYY-MM-DDTHH:MM:SS.sssZ)']], + 'Validasi gagal' + ), + ResponseCode::INVALID_FIELD->getHttpStatus() + ) + ); + } + + // Validasi timestamp tidak lebih dari 5 menit dari waktu sekarang + try { + $timestamp = new \DateTime($xTimestamp); + $now = new \DateTime(); + $diff = $now->getTimestamp() - $timestamp->getTimestamp(); + + if (abs($diff) > 300) { // 5 menit = 300 detik + throw new HttpResponseException( + response()->json( + ResponseCode::INVALID_FIELD->toResponse( + ['errors' => ['X-Timestamp' => 'Timestamp expired. Maksimal selisih 5 menit dari waktu sekarang']], + 'Validasi gagal' + ), + ResponseCode::INVALID_FIELD->getHttpStatus() + ) + ); + } + } catch (\Exception $e) { + throw new HttpResponseException( + response()->json( + ResponseCode::INVALID_FIELD->toResponse( + ['errors' => ['X-Timestamp' => 'Timestamp tidak dapat diproses']], + 'Validasi gagal' + ), + ResponseCode::INVALID_FIELD->getHttpStatus() + ) + ); + } + // Cek apakah ini karena invalid API key if ($xApiKey !== $expectedApiKey) { throw new HttpResponseException( @@ -215,19 +313,5 @@ class BalanceSummaryRequest extends FormRequest 'ip' => $this->ip(), 'user_agent' => $this->userAgent() ]); - - $acount = AccountBalance::where('account_number', $this->account_number)->first(); - - if (!$acount) { - throw new HttpResponseException( - response()->json( - ResponseCode::ACCOUNT_NOT_FOUND->toResponse( - null, - 'Nomor rekening tidak ditemukan' - ), - ResponseCode::ACCOUNT_NOT_FOUND->getHttpStatus() - ) - ); - } } } diff --git a/config/config.php b/config/config.php index 40f5a20..d99ae5e 100644 --- a/config/config.php +++ b/config/config.php @@ -5,4 +5,17 @@ return [ // ZIP file password configuration 'zip_password' => env('WEBSTATEMENT_ZIP_PASSWORD', 'statement123'), + /* + |-------------------------------------------------------------------------- + | API Configuration + |-------------------------------------------------------------------------- + | + | These configuration values are used for API authentication using HMAC + | signature validation. These keys are used to validate incoming API + | requests and ensure secure communication. + | + */ + + 'api_key' => env('API_KEY'), + 'secret_key' => env('SECRET_KEY'), ];