handleImageUpload($request, 'card_images', 'cards'); } /** POST /v2/uploads/profile-image */ public function profileImage(Request $request): JsonResponse { return $this->handleImageUpload($request, 'imageProfileCaptain', 'profiles'); } /** POST /v2/uploads/document */ public function document(Request $request): JsonResponse { $request->validate([ 'image' => 'required|file', 'doc_type' => 'required|string|in:license,registration,criminal,id_front,id_back,driver_license_front,driver_license_back,car_license_front,car_license_back', ]); return $this->handleImageUpload($request, 'driver_documents', 'documents'); } /** POST /v2/uploads/id-front */ public function idFront(Request $request): JsonResponse { return $this->handleImageUpload($request, 'card_images', 'ids/front'); } /** POST /v2/uploads/id-back */ public function idBack(Request $request): JsonResponse { return $this->handleImageUpload($request, 'card_images', 'ids/back'); } /** POST /v2/uploads/audio */ public function audio(Request $request): JsonResponse { $request->validate(['audio' => 'required|file']); $file = $request->file('audio'); // Validate MIME $mime = $file->getMimeType(); if (!in_array($mime, self::ALLOWED_AUDIO_MIMES)) { return response()->json(['status' => 'failure', 'message' => 'Invalid audio format'], 400); } if ($file->getSize() > self::MAX_AUDIO_SIZE) { return response()->json(['status' => 'failure', 'message' => 'File too large'], 400); } $userId = $request->attributes->get('_jwt_user_id'); $ext = $file->getClientOriginalExtension() ?: 'mp3'; $filename = 'audio_' . Str::random(24) . '.' . $ext; $uploadPath = public_path('uploads/audio'); if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true); $file->move($uploadPath, $filename); $link = config('intaleq.upload_base_url') . '/uploads/audio/' . $filename; return response()->json([ 'status' => 'success', 'data' => ['link' => $link, 'filename' => $filename], ], 201); } /** * Core image upload handler */ private function handleImageUpload(Request $request, string $table, string $subDir): JsonResponse { $request->validate(['image' => 'required|file']); $file = $request->file('image'); // Validate MIME type $mime = $file->getMimeType(); if (!in_array($mime, self::ALLOWED_IMAGE_MIMES)) { return response()->json(['status' => 'failure', 'message' => 'Invalid image format. Allowed: JPG, PNG, WebP'], 400); } // Validate file size if ($file->getSize() > self::MAX_IMAGE_SIZE) { return response()->json(['status' => 'failure', 'message' => 'File too large (max 5MB)'], 400); } // Validate magic bytes (defense in depth) $firstBytes = file_get_contents($file->getRealPath(), false, null, 0, 4); if (!$this->validateMagicBytes($firstBytes, $mime)) { return response()->json(['status' => 'failure', 'message' => 'File content does not match type'], 400); } // Generate safe filename $userId = $request->attributes->get('_jwt_user_id'); $ext = match ($mime) { 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', default => 'jpg', }; $filename = $subDir . '_' . Str::random(24) . '.' . $ext; $uploadPath = public_path('uploads/' . $subDir); if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true); $file->move($uploadPath, $filename); $link = config('intaleq.upload_base_url') . '/uploads/' . $subDir . '/' . $filename; // Save to DB $dbData = [ 'driverID' => $userId, 'image_name' => $filename, 'link' => $link, 'upload_date' => now(), ]; if ($request->has('doc_type')) { $dbData['doc_type'] = $request->input('doc_type'); } DB::connection('ride')->table($table)->insert($dbData); return response()->json([ 'status' => 'success', 'data' => ['link' => $link, 'filename' => $filename], ], 201); } /** * Validate file magic bytes match the declared MIME type */ private function validateMagicBytes(string $bytes, string $mime): bool { return match ($mime) { 'image/jpeg' => str_starts_with($bytes, "\xFF\xD8\xFF"), 'image/png' => str_starts_with($bytes, "\x89\x50\x4E\x47"), 'image/webp' => str_starts_with($bytes, "RIFF"), default => false, }; } }