feat(security): implement secure file storage and shipment access control
Introduce a 'secure' filesystem disk to prevent public access to sensitive commitment form uploads. Files are now stored in a non-public directory and served via a protected controller method that validates user ownership. Additionally, implement shipment authorization policies to ensure users can only access PDF documents (AWB, invoice, labels) belonging to their own orders. Other changes: - Add production environment check for Zarinpal gateway configuration to prevent accidental use of sandbox credentials. - Move discount code and commitment form routes under authentication middleware for improved security. - Add `ShipmentPolicy` to handle resource authorization.
This commit is contained in:
parent
8213ee7ee1
commit
8cf407553b
@ -106,7 +106,9 @@ class CommitmentFormController extends Controller
|
||||
'file_type' => strtoupper(pathinfo($form->file_path, PATHINFO_EXTENSION)),
|
||||
'direction' => $form->direction,
|
||||
'upload_status' => $upload ? $upload->status : 'pending',
|
||||
'uploaded_file_url' => $upload && $upload->uploaded_file_path ? asset('storage/' . $upload->uploaded_file_path) : null,
|
||||
'uploaded_file_url' => $upload && $upload->uploaded_file_path
|
||||
? route('customer.commitment-forms.download', ['shipment' => $shipment->id, 'form' => $form->id])
|
||||
: null,
|
||||
'uploaded_at' => $upload ? $upload->created_at : null,
|
||||
'notes' => $upload ? $upload->notes : null,
|
||||
];
|
||||
@ -141,7 +143,8 @@ class CommitmentFormController extends Controller
|
||||
|
||||
try {
|
||||
$file = $request->file('file');
|
||||
$path = $file->store("commitment-forms/{$shipment->id}", 'public');
|
||||
// 🛡️ ذخیره در دیسک امن (غیرعمومی) — فقط از طریق route محافظتشده قابل دانلود
|
||||
$path = $file->store("commitment-forms/{$shipment->id}", 'secure');
|
||||
|
||||
$upload = ShipmentCommitmentForm::updateOrCreate(
|
||||
[
|
||||
@ -163,7 +166,10 @@ class CommitmentFormController extends Controller
|
||||
'message' => 'فایل با موفقیت آپلود شد.',
|
||||
'data' => [
|
||||
'id' => $upload->id,
|
||||
'file_url' => asset('storage/' . $path),
|
||||
'file_url' => route('customer.commitment-forms.download', [
|
||||
'shipment' => $shipment->id,
|
||||
'form' => $form->id,
|
||||
]),
|
||||
'file_type' => $upload->uploaded_file_type,
|
||||
'status' => $upload->status,
|
||||
'uploaded_at' => $upload->created_at,
|
||||
@ -176,5 +182,38 @@ class CommitmentFormController extends Controller
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* دانلود فرم تعهدنامه امزاشده (محافظتشده با بررسی مالکیت).
|
||||
* GET /api/v1/customer/orders/{shipment}/commitment-forms/{form}/download
|
||||
*/
|
||||
public function downloadSigned(Shipment $shipment, CommitmentForm $form)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if ($shipment->user_id !== $user->id) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'شما به این سفارش دسترسی ندارید.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$upload = ShipmentCommitmentForm::where('shipment_id', $shipment->id)
|
||||
->where('commitment_form_id', $form->id)
|
||||
->firstOrFail();
|
||||
|
||||
if (!$upload->uploaded_file_path) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'فایلی برای این تعهدنامه آپلود نشده است.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return Storage::disk('secure')->download(
|
||||
$upload->uploaded_file_path,
|
||||
'commitment-' . $form->id . '-' . $shipment->awb_no . '.' . $upload->uploaded_file_type
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,8 +15,10 @@ class ShipmentPdfController extends Controller
|
||||
/**
|
||||
* تولید AWB PDF (برای همه نوع محمولهها)
|
||||
*/
|
||||
public function awb(Shipment $shipment)
|
||||
public function awb(Shipment $shipment)
|
||||
{
|
||||
$this->authorize('view', $shipment);
|
||||
|
||||
try {
|
||||
Log::info('Generating AWB PDF', ['shipment_id' => $shipment->id]);
|
||||
|
||||
@ -47,6 +49,7 @@ class ShipmentPdfController extends Controller
|
||||
*/
|
||||
public function invoice(Shipment $shipment)
|
||||
{
|
||||
$this->authorize('view', $shipment);
|
||||
try {
|
||||
// بارگذاری items برای بررسی
|
||||
$shipment->loadMissing(['items']);
|
||||
@ -109,6 +112,7 @@ class ShipmentPdfController extends Controller
|
||||
*/
|
||||
public function label(Shipment $shipment)
|
||||
{
|
||||
$this->authorize('view', $shipment);
|
||||
try {
|
||||
$content = $this->pdf->label($shipment);
|
||||
|
||||
@ -134,6 +138,7 @@ class ShipmentPdfController extends Controller
|
||||
*/
|
||||
public function importInvoice(Shipment $shipment, Request $request)
|
||||
{
|
||||
$this->authorize('view', $shipment);
|
||||
try {
|
||||
// دریافت پارامترها از درخواست
|
||||
$options = $request->only([
|
||||
|
||||
23
04_Laravel/app/Policies/ShipmentPolicy.php
Normal file
23
04_Laravel/app/Policies/ShipmentPolicy.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Shipment;
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* سیاست دسترسی به سفارشها (و اسنادشان: AWB/Invoice/Label/PDF).
|
||||
* - super_admin/admin/staff: هر سفارشی.
|
||||
* - customer: فقط سفارشهای خودش.
|
||||
*/
|
||||
class ShipmentPolicy
|
||||
{
|
||||
public function view(User $user, Shipment $shipment): bool
|
||||
{
|
||||
if ($user->hasAnyRole(['super_admin', 'admin', 'staff'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $shipment->user_id === $user->id;
|
||||
}
|
||||
}
|
||||
@ -93,6 +93,16 @@ class OrderPaymentService
|
||||
public function initiateGatewayPayment(Shipment $shipment, Wallet $wallet): array
|
||||
{
|
||||
try {
|
||||
// 🛡️ محافظ Production: اگه در محیط تولید، درگاه Mock فعال باشد، رد کن
|
||||
if (app()->environment('production')
|
||||
&& (config('ifnex.zarinpal.merchant_id') === 'fake-merchant-id-for-testing'
|
||||
|| config('ifnex.zarinpal.sandbox', true))) {
|
||||
throw new \RuntimeException(
|
||||
'درگاه پرداخت در محیط تولید بهدرستی تنظیم نشده. '
|
||||
. 'ZARINPAL_MERCHANT_ID واقعی یا ZARINPAL_SANDBOX=false در .env تنظیم کنید.'
|
||||
);
|
||||
}
|
||||
|
||||
// ۱. ایجاد تراکنش pending
|
||||
$transaction = WalletTransaction::create([
|
||||
'wallet_id' => $wallet->id,
|
||||
|
||||
@ -38,7 +38,7 @@ return [
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||
@ -47,6 +47,15 @@ return [
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
// 🛡️ دیسک امن برای فایلهای حساس (تعهدنامههای امزاشده).
|
||||
// بدون url/serve → قابل دسترسی عمومی نیست؛ فقط از طریق route محافظتشده.
|
||||
'secure' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/secure'),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
|
||||
@ -64,6 +64,14 @@ Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
|
||||
Route::post('/wallet/{wallet}/unfreeze', [WalletController::class, 'unfreeze']);
|
||||
Route::get('/wallet/{wallet}/activity-log', [WalletController::class, 'activityLog']);
|
||||
|
||||
// ─── Discount Codes (نیازمند احراز هویت) ───
|
||||
Route::get('/discount-codes', [DiscountCodeController::class, 'index']);
|
||||
Route::post('/discount-codes/validate', [DiscountCodeController::class, 'validate']);
|
||||
|
||||
// ─── Commitment Forms (نیازمند احراز هویت) ───
|
||||
Route::get('/commitment-forms', [CommitmentFormController::class, 'index']);
|
||||
Route::get('/commitment-forms/{direction}', [CommitmentFormController::class, 'byDirection']);
|
||||
|
||||
// ─── Customer Orders API (جدید) ───
|
||||
Route::prefix('customer')->group(function () {
|
||||
// پروفایل و آمار
|
||||
@ -88,7 +96,8 @@ Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
|
||||
// تعهدنامهها
|
||||
Route::get('/orders/{shipment}/commitment-forms', [CommitmentFormController::class, 'shipmentForms']);
|
||||
Route::post('/orders/{shipment}/commitment-forms/{form}/upload', [CommitmentFormController::class, 'uploadSigned']);
|
||||
|
||||
Route::get('/orders/{shipment}/commitment-forms/{form}/download', [CommitmentFormController::class, 'downloadSigned'])
|
||||
->name('customer.commitment-forms.download');
|
||||
// نوتیفیکیشنها
|
||||
Route::get('/notifications', [CustomerOrderController::class, 'notifications']);
|
||||
Route::post('/notifications/{notification}/read', [CustomerOrderController::class, 'markNotificationRead']);
|
||||
@ -150,15 +159,9 @@ Route::any('/v1/payment/callback', [PaymentController::class, 'callback'])
|
||||
->name('payment.callback');
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// APIهای عمومی (بدون auth)
|
||||
// APIهای عمومی (بدون auth) — فقط استعلام قیمت همگانی است
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
Route::post('/v1/calculate', [PricingController::class, 'calculate']);
|
||||
Route::get('/v1/discount-codes', [DiscountCodeController::class, 'index']);
|
||||
Route::post('/v1/discount-codes/validate', [DiscountCodeController::class, 'validate']);
|
||||
|
||||
// API فایلهای تعهدنامه (عمومی)
|
||||
Route::get('/v1/commitment-forms', [CommitmentFormController::class, 'index']);
|
||||
Route::get('/v1/commitment-forms/{direction}', [CommitmentFormController::class, 'byDirection']);
|
||||
|
||||
// API تأیید موبایل (عمومی)
|
||||
Route::post('/v1/verify/send-code', [MobileVerificationController::class, 'sendCode']);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user