Initial V2 commit
This commit is contained in:
171
app/Http/Controllers/UploadController.php
Normal file
171
app/Http/Controllers/UploadController.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Upload Controller
|
||||
* Replaces: uploadImage.php, uploadImagePortrate.php, uploadImageType.php, etc.
|
||||
*
|
||||
* Security improvements over V1:
|
||||
* - MIME type + magic bytes validation (not just extension)
|
||||
* - Randomized filenames (prevents path traversal)
|
||||
* - Max file size enforcement
|
||||
* - No directory traversal possible
|
||||
*/
|
||||
class UploadController extends Controller
|
||||
{
|
||||
private const ALLOWED_IMAGE_MIMES = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
private const ALLOWED_AUDIO_MIMES = ['audio/mpeg', 'audio/mp4', 'audio/wav', 'audio/ogg'];
|
||||
private const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
private const MAX_AUDIO_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
/** POST /v2/uploads/card-image */
|
||||
public function cardImage(Request $request): JsonResponse
|
||||
{
|
||||
return $this->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',
|
||||
]);
|
||||
|
||||
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->input('_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->input('_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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user