diff --git a/04_Laravel/app/Providers/AppServiceProvider.php b/04_Laravel/app/Providers/AppServiceProvider.php index 8997fab..19f76f8 100644 --- a/04_Laravel/app/Providers/AppServiceProvider.php +++ b/04_Laravel/app/Providers/AppServiceProvider.php @@ -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()); + }); } -} +} \ No newline at end of file diff --git a/04_Laravel/routes/api.php b/04_Laravel/routes/api.php index 61262df..0060ad5 100644 --- a/04_Laravel/routes/api.php +++ b/04_Laravel/routes/api.php @@ -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']); \ No newline at end of file diff --git a/04_Laravel/scripts/test_pdf_generation.php b/04_Laravel/scripts/test_pdf_generation.php deleted file mode 100644 index 3edfb42..0000000 --- a/04_Laravel/scripts/test_pdf_generation.php +++ /dev/null @@ -1,157 +0,0 @@ -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";