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:
parent
93ebbb1fc8
commit
a5fd01dde1
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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']);
|
||||
@ -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";
|
||||
Loading…
Reference in New Issue
Block a user