diff --git a/app/Http/Controllers/Api/AttachmentController.php b/app/Http/Controllers/Api/AttachmentController.php new file mode 100644 index 0000000..b245d5e --- /dev/null +++ b/app/Http/Controllers/Api/AttachmentController.php @@ -0,0 +1,53 @@ +user(); + + // 1. Authorization Logic + // Load relationships needed for checking + $attachment->load(['reply.ticket']); + $ticket = $attachment->reply->ticket; + + // Check if user is owner of the ticket OR is Admin/Owner + if ($ticket->user_id !== $user->id && !$user->isAdminOrOwner()) { + abort(403, 'Unauthorized access to this attachment.'); + } + + // 2. Fetch File from Private R2 Bucket + // We assume new uploads store 'relative/path.ext' in DB + // But legacy uploads stored 'https://cdn...' + + $path = $attachment->file_path; + $disk = 'r2-private'; + + // Detect if it's a full public URL (Legacy) or Relative Path (New) + if (filter_var($path, FILTER_VALIDATE_URL)) { + // It's a URL. It might be on the Public Bucket (old uploads). + // Strategy: Redirect to public URL? Or try to serve it? + // Since legacy files are on Public Bucket, we can just redirect or return URL. + // But if specific requirement is "Attachment Private", we should ideally migrate them. + // For now, if it's a URL, we assume it's public and redirect. + return redirect($path); + } + + // It is a relative path intended for Private Bucket + if (!Storage::disk($disk)->exists($path)) { + abort(404, 'File not found on secure storage.'); + } + + return Storage::disk($disk)->download($path, $attachment->file_name); + } +} diff --git a/app/Http/Controllers/Api/TicketController.php b/app/Http/Controllers/Api/TicketController.php index d474d43..6d12042 100644 --- a/app/Http/Controllers/Api/TicketController.php +++ b/app/Http/Controllers/Api/TicketController.php @@ -82,12 +82,12 @@ class TicketController extends Controller // Handle Attachments if ($request->hasFile('attachments')) { foreach ($request->file('attachments') as $file) { - $path = $file->store('ticket-attachments', 'r2'); - $url = Storage::disk('r2')->url($path); + $path = $file->store('ticket-attachments', 'r2-private'); + // $url = Storage::disk('r2')->url($path); // Removed public URL generation TicketAttachment::create([ 'ticket_reply_id' => $reply->id, 'file_name' => $file->getClientOriginalName(), - 'file_path' => $url, + 'file_path' => $path, // Store relative path 'file_type' => $file->getClientMimeType(), 'file_size' => $file->getSize(), ]); @@ -170,12 +170,12 @@ class TicketController extends Controller // Handle Attachments if ($request->hasFile('attachments')) { foreach ($request->file('attachments') as $file) { - $path = $file->store('ticket-attachments', 'r2'); - $url = Storage::disk('r2')->url($path); + $path = $file->store('ticket-attachments', 'r2-private'); + // $url = Storage::disk('r2')->url($path); TicketAttachment::create([ 'ticket_reply_id' => $reply->id, 'file_name' => $file->getClientOriginalName(), - 'file_path' => $url, + 'file_path' => $path, // Store relative path 'file_type' => $file->getClientMimeType(), 'file_size' => $file->getSize(), ]); diff --git a/app/Models/TicketAttachment.php b/app/Models/TicketAttachment.php index c9a36be..ac20b94 100644 --- a/app/Models/TicketAttachment.php +++ b/app/Models/TicketAttachment.php @@ -22,4 +22,18 @@ class TicketAttachment extends Model { return $this->belongsTo(TicketReply::class, 'ticket_reply_id'); } + + protected $appends = ['download_url']; + + public function getDownloadUrlAttribute() + { + // Legacy: if it's already a full URL, return it + if (filter_var($this->file_path, FILTER_VALIDATE_URL)) { + return $this->file_path; + } + + // Secure: return the API endpoint + // Assuming route prefix is /api (standard Laravel) + return url("/api/support/attachments/{$this->id}"); + } } diff --git a/config/filesystems.php b/config/filesystems.php index afbaa59..9919305 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -60,7 +60,34 @@ return [ 'report' => false, ], - 'r2' => [ + + 'r2-public' => [ + 'driver' => 's3', + 'key' => env('R2_ACCESS_KEY_ID'), + 'secret' => env('R2_SECRET_ACCESS_KEY'), + 'region' => 'auto', + 'bucket' => env('R2_BUCKET'), + 'url' => env('R2_URL'), + 'endpoint' => env('R2_ENDPOINT'), + 'use_path_style_endpoint' => env('R2_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + 'r2-private' => [ + 'driver' => 's3', + 'key' => env('R2_ACCESS_KEY_ID'), + 'secret' => env('R2_SECRET_ACCESS_KEY'), + 'region' => 'auto', + 'bucket' => env('R2_PRIVATE_BUCKET'), + 'endpoint' => env('R2_ENDPOINT'), + 'use_path_style_endpoint' => env('R2_USE_PATH_STYLE_ENDPOINT', false), + 'visibility' => 'private', + 'throw' => false, + 'report' => false, + ], + + 'r2' => [ // Legacy alias pointing to r2-public 'driver' => 's3', 'key' => env('R2_ACCESS_KEY_ID'), 'secret' => env('R2_SECRET_ACCESS_KEY'), diff --git a/routes/api.php b/routes/api.php index 74b13b8..1db6e64 100644 --- a/routes/api.php +++ b/routes/api.php @@ -88,6 +88,7 @@ Route::middleware(['auth:sanctum'])->group(function () { Route::get('/support/tickets/{id}', [\App\Http\Controllers\Api\TicketController::class, 'show']); Route::post('/support/tickets/{id}/reply', [\App\Http\Controllers\Api\TicketController::class, 'reply']); Route::patch('/support/tickets/{id}/close', [\App\Http\Controllers\Api\TicketController::class, 'close']); + Route::get('/support/attachments/{attachment}', [\App\Http\Controllers\Api\AttachmentController::class, 'download']); // User Management (Admin Only) Route::apiResource('/admin/users', UserApiController::class);