chore(api): implement rate limiting for sensitive endpoints

Introduce granular rate limiting across various API categories to
improve security and prevent abuse. This includes protection against
brute-force attacks on authentication and SMS endpoints, as well as
resource management for public, customer, wallet, and staff APIs.

- Add `auth` rate limiter (5 requests/min per IP)
- Add `sms` rate limiter (1 request/min per phone/IP)
- Add `public` rate limiter (30 requests/min per IP)
- Add `customer` rate limiter (60 requests/min per user)
- Add `wallet` rate limiter (30 requests/min per user)
- Add `staff` rate limiter (60 requests/min per user)
- Apply middleware to corresponding routes in `api.php`
- Remove obsolete `test_pdf_generation.php` script
This commit is contained in:
Kazem Alghasi 2026-10-04 01:35:22 +03:30
parent 93ebbb1fc8
commit a5fd01dde1
3 changed files with 67 additions and 177 deletions

View File

@ -3,6 +3,9 @@
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use App\Models\Shipment;
use App\Observers\ShipmentObserver;
@ -19,7 +22,7 @@ class AppServiceProvider extends ServiceProvider
/**
* Bootstrap any application services.
*/
public function boot(): void
public function boot(): void
{
// هک برای رفع مشکل دیسک اکسل در لاراول ۱۱/۱۲
config(['excel.temporary_files.disk' => 'local']);
@ -27,12 +30,43 @@ class AppServiceProvider extends ServiceProvider
// ثبت Observer برای اطلاعیه‌های تغییر سفارش
Shipment::observe(ShipmentObserver::class);
// Explicitly require the IFNEX number helper file so
// Explicitly require the IFNEX number helper file so
// ifnex_format_number() / ifnex_persian_digits() / ifnex_trend_percent()
// are available even before `composer dump-autoload` is re-run.
$helperPath = base_path('app/Helpers/number.php');
if (is_file($helperPath)) {
require_once $helperPath;
}
// ─── Rate Limiting برای APIهای حساس ───
RateLimiter::for('auth', function (Request $request) {
// لاگین: ۵ درخواست در دقیقه per IP (جلوگیری از brute-force)
return Limit::perMinute(5)->by($request->ip());
});
RateLimiter::for('sms', function (Request $request) {
// ارسال کد پیامکی: ۱ درخواست در دقیقه per شماره موبایل
return Limit::perMinute(1)->by($request->input('phone') ?: $request->ip());
});
RateLimiter::for('public', function (Request $request) {
// API عمومی (رهگیری، استعلام قیمت): ۳۰ درخواست در دقیقه per IP
return Limit::perMinute(30)->by($request->ip());
});
RateLimiter::for('customer', function (Request $request) {
// API مشتری (لاگین‌شده): ۶۰ درخواست در دقیقه per user
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('wallet', function (Request $request) {
// عملیات مالی حساس (پرداخت، شارژ، freeze): ۳۰ درخواست در دقیقه per user
return Limit::perMinute(30)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('staff', function (Request $request) {
// API کارمند: ۶۰ درخواست در دقیقه per user
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}
}
}

View File

@ -22,46 +22,53 @@ use App\Http\Controllers\ShipmentPdfController;
// ══════════════════════════════════════════════════════════════
// Bridge Auth API (فقط برای پلاگین وردپرس - با API Key محافظت می‌شود)
// ══════════════════════════════════════════════════════════════
Route::post('/v1/bridge/login', [BridgeAuthController::class, 'login']); // Login
Route::post('/v1/bridge/login', [BridgeAuthController::class, 'login'])
->middleware('throttle:auth'); // Login — ۵/min per IP
// ══════════════════════════════════════════════════════════════
// API عمومی با API Key
// ══════════════════════════════════════════════════════════════
Route::middleware([ApiKeyMiddleware::class])->prefix('v1')->group(function () {
Route::middleware([ApiKeyMiddleware::class, 'throttle:public'])->prefix('v1')->group(function () {
Route::get('/track/{awb_no}', [TrackController::class, 'show']);
});
// ══════════════════════════════════════════════════════════════
// API عمومی (بدون نیاز به احراز هویت)
// ══════════════════════════════════════════════════════════════
Route::prefix('v1')->group(function () {
Route::middleware(['throttle:public'])->prefix('v1')->group(function () {
Route::get('/countries', [CustomerOrderController::class, 'countries']);
});
// ══════════════════════════════════════════════════════════════
// Auth API (عمومی - برای دریافت توکن)
// ══════════════════════════════════════════════════════════════
Route::post('/v1/auth/login', [AuthController::class, 'login']);
Route::post('/v1/auth/login', [AuthController::class, 'login'])
->middleware('throttle:auth');
Route::middleware(['auth:sanctum'])->post('/v1/auth/logout', [AuthController::class, 'logout']);
// ══════════════════════════════════════════════════════════════
// API برای کاربران لاگین‌شده (با Sanctum / Bearer Token)
// ══════════════════════════════════════════════════════════════
Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
Route::middleware(['auth:sanctum', 'throttle:customer'])->prefix('v1')->group(function () {
// ─── Wallet API (کاربر عادی) ───
Route::get('/wallet/balance', [WalletController::class, 'balance']);
Route::get('/wallet/transactions', [WalletController::class, 'transactions']);
// ─── Payment API ───
Route::post('/payment/redirect', [PaymentController::class, 'redirectToGateway']);
Route::get('/payment/check/{transaction}', [PaymentController::class, 'checkStatus']);
Route::post('/payment/redirect', [PaymentController::class, 'redirectToGateway'])
->middleware('throttle:wallet');
Route::get('/payment/check/{transaction}', [PaymentController::class, 'checkStatus'])
->middleware('throttle:wallet');
// ─── Admin Wallet API ───
Route::post('/wallet/admin-adjust', [WalletController::class, 'adminAdjust']);
Route::post('/wallet/{wallet}/freeze', [WalletController::class, 'freeze']);
Route::post('/wallet/{wallet}/unfreeze', [WalletController::class, 'unfreeze']);
Route::post('/wallet/admin-adjust', [WalletController::class, 'adminAdjust'])
->middleware('throttle:wallet');
Route::post('/wallet/{wallet}/freeze', [WalletController::class, 'freeze'])
->middleware('throttle:wallet');
Route::post('/wallet/{wallet}/unfreeze', [WalletController::class, 'unfreeze'])
->middleware('throttle:wallet');
Route::get('/wallet/{wallet}/activity-log', [WalletController::class, 'activityLog']);
// ─── Discount Codes (نیازمند احراز هویت) ───
@ -106,12 +113,14 @@ Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
Route::post('/notifications/{notification}/read', [CustomerOrderController::class, 'markNotificationRead']);
// پرداخت سفارش
Route::post('/orders/{shipment}/pay-wallet', [CustomerOrderController::class, 'payFromWallet']);
Route::post('/orders/{shipment}/pay-gateway', [CustomerOrderController::class, 'payViaGateway']);
Route::post('/orders/{shipment}/pay-wallet', [CustomerOrderController::class, 'payFromWallet'])
->middleware('throttle:wallet');
Route::post('/orders/{shipment}/pay-gateway', [CustomerOrderController::class, 'payViaGateway'])
->middleware('throttle:wallet');
});
// ─── Staff Orders API (تأیید سفارشات) ───
Route::middleware([StaffApiMiddleware::class])
Route::middleware([StaffApiMiddleware::class, 'throttle:staff'])
->prefix('staff')
->group(function () {
@ -159,16 +168,20 @@ Route::prefix('v1/payment')->group(function () {
// Callback از درگاه (بدون auth)
Route::any('/v1/payment/callback', [PaymentController::class, 'callback'])
->name('payment.callback');
->name('payment.callback')
->middleware('throttle:public');
// ══════════════════════════════════════════════════════════════
// APIهای عمومی (بدون auth) — فقط استعلام قیمت همگانی است
// ══════════════════════════════════════════════════════════════
Route::post('/v1/calculate', [PricingController::class, 'calculate']);
Route::post('/v1/calculate', [PricingController::class, 'calculate'])
->middleware('throttle:public');
// API تأیید موبایل (عمومی)
Route::post('/v1/verify/send-code', [MobileVerificationController::class, 'sendCode']);
Route::post('/v1/verify/check-code', [MobileVerificationController::class, 'checkCode']);
Route::post('/v1/verify/send-code', [MobileVerificationController::class, 'sendCode'])
->middleware('throttle:sms'); // ۱/min per phone
Route::post('/v1/verify/check-code', [MobileVerificationController::class, 'checkCode'])
->middleware('throttle:auth'); // ۵/min per IP
// API ارسال پیامک (نیاز به auth دارد)
Route::middleware(['auth:sanctum'])->post('/v1/verify/send-sms', [MobileVerificationController::class, 'sendSms']);

View File

@ -1,157 +0,0 @@
<?php
/**
* اسکریپت تست تولید PDF برای IFNEX
*
* این اسکریپت یه محموله نمونه می‌سازه و سه تا PDF (AWB, Invoice, Label) تولید می‌کنه
* و اون‌ها رو توی پوشه storage/app/pdf-test/ ذخیره می‌کنه.
*
* استفاده:
* php test_pdf_generation.php
*
* پس از اجرا، فایل‌های تولیدشده رو بررسی کنید:
* storage/app/pdf-test/AWB-test.pdf
* storage/app/pdf-test/INVOICE-test.pdf
* storage/app/pdf-test/LABEL-test.pdf
*/
require __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
use App\Models\Shipment;
use App\Models\ShipmentItem;
use App\Models\Country;
use App\Services\PdfService;
use Illuminate\Support\Facades\File;
echo "═══════════════════════════════════════════\n";
echo "🧪 IFNEX PDF Generation Test\n";
echo "═══════════════════════════════════════════\n\n";
// ─── ۱. بررسی نصب بودن کتابخانه بارکد ───
echo "1️⃣ Checking barcode library...\n";
if (class_exists(\Picqer\Barcode\BarcodeGeneratorPNG::class)) {
echo " ✅ picqer/php-barcode-generator is installed\n\n";
} else {
echo " ❌ picqer/php-barcode-generator is NOT installed\n";
echo " Run: composer require picqer/php-barcode-generator\n\n";
exit(1);
}
// ─── ۱.۵. تست تولید بارکد ───
echo "1.5️⃣ Testing barcode generation...\n";
try {
$generator = new \Picqer\Barcode\BarcodeGeneratorPNG();
$barcode = $generator->getBarcode('TEST123', $generator::TYPE_CODE_128, 3, 80);
$b64 = 'data:image/png;base64,' . base64_encode($barcode);
echo " ✅ Barcode generated, length: " . strlen($b64) . " chars\n";
echo " Preview: " . substr($b64, 0, 50) . "...\n\n";
} catch (\Throwable $e) {
echo " ❌ Barcode generation failed: " . $e->getMessage() . "\n\n";
}
// ─── ۲. ایجاد پوشه تست ───
$testDir = storage_path('app/pdf-test');
if (!File::exists($testDir)) {
File::makeDirectory($testDir, 0755, true);
echo "2️⃣ Created test directory: {$testDir}\n\n";
} else {
echo "2️⃣ Test directory exists: {$testDir}\n\n";
}
// ─── ۳. پیدا کردن یه محموله نمونه ───
echo "3️⃣ Finding sample shipment...\n";
$shipment = Shipment::with(['fromCountry', 'toCountry', 'items'])->latest()->first();
if (!$shipment) {
echo " ❌ No shipment found in database. Please create one first.\n";
exit(1);
}
echo " ✅ Found shipment: {$shipment->awb_no}\n";
echo " - Type: {$shipment->type?->value}\n";
echo " - Direction: {$shipment->direction?->value}\n";
echo " - From: {$shipment->fromCountry?->name}\n";
echo " - To: {$shipment->toCountry?->name}\n";
echo " - Items: {$shipment->items->count()}\n\n";
// ─── ۴. اگر آیتم نداره، یه آیتم تستی اضافه کن ───
if ($shipment->items->isEmpty()) {
echo " ⚠️ Shipment has no items. Adding test item...\n";
ShipmentItem::create([
'shipment_id' => $shipment->id,
'row_number' => 1,
'description' => 'Electronics PCB Board',
'hs_code' => '8542390001',
'quantity' => 104,
'unit_price' => 1.10,
'total_usd' => 114.40,
]);
$shipment->load('items');
echo " ✅ Added test item\n\n";
}
// ─── ۵. تولید AWB PDF ───
echo "4️⃣ Generating AWB PDF...\n";
try {
$pdfService = app(PdfService::class);
$awbContent = $pdfService->awb($shipment);
$awbPath = $testDir . '/AWB-' . $shipment->awb_no . '.pdf';
File::put($awbPath, $awbContent);
echo " ✅ AWB PDF saved: {$awbPath}\n";
echo " Size: " . number_format(strlen($awbContent) / 1024, 2) . " KB\n\n";
} catch (\Throwable $e) {
echo " ❌ AWB PDF failed: " . $e->getMessage() . "\n\n";
}
// ─── ۶. تولید Invoice PDF ───
echo "5️⃣ Generating Invoice PDF...\n";
try {
$invoiceContent = $pdfService->invoice($shipment);
$invoicePath = $testDir . '/INVOICE-' . $shipment->awb_no . '.pdf';
File::put($invoicePath, $invoiceContent);
echo " ✅ Invoice PDF saved: {$invoicePath}\n";
echo " Size: " . number_format(strlen($invoiceContent) / 1024, 2) . " KB\n\n";
} catch (\InvalidArgumentException $e) {
echo " ⚠️ Invoice skipped: " . $e->getMessage() . "\n";
echo " (این طبیعی است اگر محموله DOC است)\n\n";
} catch (\Throwable $e) {
echo " ❌ Invoice PDF failed: " . $e->getMessage() . "\n\n";
}
// ─── ۷. تولید Label PDF ───
echo "6️⃣ Generating Label PDF...\n";
try {
$labelContent = $pdfService->label($shipment);
$labelPath = $testDir . '/LABEL-' . $shipment->awb_no . '.pdf';
File::put($labelPath, $labelContent);
echo " ✅ Label PDF saved: {$labelPath}\n";
echo " Size: " . number_format(strlen($labelContent) / 1024, 2) . " KB\n\n";
} catch (\Throwable $e) {
echo " ❌ Label PDF failed: " . $e->getMessage() . "\n\n";
}
// ─── ۸. تست محموله DOC (بدون فاکتور) ───
echo "7️⃣ Testing DOC shipment (should skip invoice)...\n";
$docShipment = Shipment::where('type', 'DOC_NORMAL')->first();
if ($docShipment) {
try {
$docShipment->load('items');
if ($docShipment->items->isEmpty()) {
$pdfService->invoice($docShipment);
echo " ❌ ERROR: Should have thrown exception for DOC shipment!\n\n";
} else {
echo " ℹ️ DOC shipment has items, invoice would work\n\n";
}
} catch (\InvalidArgumentException $e) {
echo " ✅ Correctly skipped invoice for DOC: " . $e->getMessage() . "\n\n";
}
} else {
echo " ℹ️ No DOC shipment found for testing (skipped)\n\n";
}
echo "═══════════════════════════════════════════\n";
echo "✅ Test completed!\n";
echo "═══════════════════════════════════════════\n\n";
echo "📁 Check the generated PDFs at:\n";
echo " {$testDir}\n\n";