Posted in

Laravel 12 Tutorial for Beginners: File Uploads and Task Sharing (Part 5)

Laravel 12 tutorial part 5

Welcome back to our Laravel 12 Task Manager series! We’ve come a long way – from Part 1 (environment setup), Part 2 (authentication and database), Part 3 (CRUD operations), to Part 4 (categories and workflows). Now it’s time to add collaborative features that will transform your Task Manager into a team-ready application.

By the end of this tutorial, you’ll have a complete file attachment system and the ability to share tasks with other users, complete with permissions and notifications.

What You’ll Learn in This Tutorial

  • File upload and storage in Laravel
  • File validation and security
  • Displaying and downloading attachments
  • Deleting files from storage
  • Sharing tasks with other users
  • Permission management (view vs edit)
  • Building a sharing interface
  • Email notifications
  • Activity tracking
  • Storage optimization
  • File type restrictions
  • User collaboration features

Prerequisites

Before starting this tutorial:

Understanding File Storage in Laravel

Laravel provides a powerful filesystem abstraction that makes working with local filesystems, Amazon S3, and other cloud storage services incredibly simple. We’ll use local storage for this tutorial, but the code can easily be adapted for cloud storage.

Storage Locations:

  • Local: storage/app/ directory
  • Public: storage/app/public/ directory (symlinked to public/storage)
  • Cloud: S3, DigitalOcean Spaces, etc.

Step 1: Setting Up File Storage

First, create a symbolic link from public/storage to storage/app/public:

php artisan storage:link

Expected output:

The [public/storage] link has been connected to [storage/app/public].

This allows publicly accessible files to be served from the storage directory.

Configuring Filesystem

The filesystem configuration is in config/filesystems.php. The default local disk is perfect for our needs:

'disks' => [
    'local' => [
        'driver' => 'local',
        'root' => storage_path('app'),
    ],

    'public' => [
        'driver' => 'local',
        'root' => storage_path('app/public'),
        'url' => env('APP_URL').'/storage',
        'visibility' => 'public',
    ],
],

We’ll use the public disk for task attachments so they can be accessed via URLs.

Step 2: Creating the Attachment Controller

Let’s create a controller to handle file uploads.

php artisan make:controller AttachmentController

Edit app/Http/Controllers/AttachmentController.php:

<?php

namespace App\Http\Controllers;

use App\Models\Task;
use App\Models\Attachment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class AttachmentController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Store a new attachment.
     */
    public function store(Request $request, Task $task)
    {
        // Verify task ownership or share access
        if ($task->user_id !== auth()->id() && !$task->shares()->where('shared_with_user_id', auth()->id())->where('permission', 'edit')->exists()) {
            abort(403, 'Unauthorized to add attachments to this task.');
        }

        $request->validate([
            'file' => [
                'required',
                'file',
                'max:10240', // 10MB maximum
                'mimes:pdf,doc,docx,xls,xlsx,txt,jpg,jpeg,png,gif,zip,rar',
            ],
        ], [
            'file.max' => 'File size cannot exceed 10MB.',
            'file.mimes' => 'Invalid file type. Allowed types: PDF, DOC, DOCX, XLS, XLSX, TXT, JPG, PNG, GIF, ZIP, RAR.',
        ]);

        $file = $request->file('file');
        
        // Generate unique filename
        $filename = time() . '_' . $file->getClientOriginalName();
        
        // Store file in public disk under 'attachments' folder
        $path = $file->storeAs('attachments', $filename, 'public');
        
        // Create attachment record
        $attachment = $task->attachments()->create([
            'user_id' => auth()->id(),
            'file_name' => $file->getClientOriginalName(),
            'file_path' => $path,
            'file_size' => $file->getSize(),
            'mime_type' => $file->getMimeType(),
        ]);

        return redirect()
            ->route('tasks.show', $task)
            ->with('success', 'File uploaded successfully!');
    }

    /**
     * Download an attachment.
     */
    public function download(Task $task, Attachment $attachment)
    {
        // Verify attachment belongs to task
        if ($attachment->task_id !== $task->id) {
            abort(404);
        }

        // Verify access
        if ($task->user_id !== auth()->id() && !$task->shares()->where('shared_with_user_id', auth()->id())->exists()) {
            abort(403, 'Unauthorized to download this file.');
        }

        // Check if file exists
        if (!Storage::disk('public')->exists($attachment->file_path)) {
            return redirect()
                ->back()
                ->with('error', 'File not found.');
        }

        return Storage::disk('public')->download(
            $attachment->file_path,
            $attachment->file_name
        );
    }

    /**
     * Delete an attachment.
     */
    public function destroy(Task $task, Attachment $attachment)
    {
        // Verify attachment belongs to task
        if ($attachment->task_id !== $task->id) {
            abort(404);
        }

        // Only task owner or uploader can delete
        if ($task->user_id !== auth()->id() && $attachment->user_id !== auth()->id()) {
            abort(403, 'Unauthorized to delete this file.');
        }

        // Delete file from storage
        if (Storage::disk('public')->exists($attachment->file_path)) {
            Storage::disk('public')->delete($attachment->file_path);
        }

        // Delete database record
        $attachment->delete();

        return redirect()
            ->route('tasks.show', $task)
            ->with('success', 'File deleted successfully!');
    }
}

Key Features:

  • File validation (type, size)
  • Unique filename generation
  • Permission checking
  • File storage management
  • Download functionality
  • Secure deletion

Adding Routes

Add to routes/web.php:

Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
    
    Route::get('/tasks/kanban', [TaskController::class, 'kanban'])->name('tasks.kanban');
    Route::resource('tasks', TaskController::class);
    Route::resource('categories', CategoryController::class);
    
    // Attachment routes
    Route::post('/tasks/{task}/attachments', [AttachmentController::class, 'store'])->name('attachments.store');
    Route::get('/tasks/{task}/attachments/{attachment}/download', [AttachmentController::class, 'download'])->name('attachments.download');
    Route::delete('/tasks/{task}/attachments/{attachment}', [AttachmentController::class, 'destroy'])->name('attachments.destroy');
});

Step 3: Adding File Upload to Task View

Let’s update the task show page to include file upload and display attachments.

Update resources/views/tasks/show.blade.php:

Replace the attachments placeholder section with:

<!-- Attachments Section -->
<div class="border-t pt-4 mt-4">
    <div class="flex justify-between items-center mb-4">
        <h3 class="text-sm font-medium text-gray-700">Attachments</h3>
        @if($task->user_id === auth()->id() || $task->shares()->where('shared_with_user_id', auth()->id())->where('permission', 'edit')->exists())
            <button onclick="document.getElementById('attachment-form').classList.toggle('hidden')" 
                    class="text-sm text-blue-600 hover:text-blue-800">
                + Add File
            </button>
        @endif
    </div>
    
    <!-- Upload Form (Hidden by default) -->
    <div id="attachment-form" class="hidden mb-4 p-4 bg-gray-50 rounded-lg">
        <form method="POST" 
              action="{{ route('attachments.store', $task) }}" 
              enctype="multipart/form-data"
              class="space-y-3">
            @csrf
            
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-2">
                    Select File
                </label>
                <input type="file" 
                       name="file" 
                       required
                       class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
                <p class="mt-1 text-xs text-gray-500">
                    Maximum file size: 10MB. Allowed types: PDF, DOC, DOCX, XLS, XLSX, TXT, images (JPG, PNG, GIF), ZIP, RAR
                </p>
                @error('file')
                    <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
                @enderror
            </div>
            
            <div class="flex gap-2">
                <button type="submit" 
                        class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-sm">
                    Upload File
                </button>
                <button type="button" 
                        onclick="document.getElementById('attachment-form').classList.add('hidden')"
                        class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded text-sm">
                    Cancel
                </button>
            </div>
        </form>
    </div>
    
    <!-- Attachments List -->
    @if($task->attachments->count() > 0)
        <div class="space-y-2">
            @foreach($task->attachments as $attachment)
                <div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition">
                    <div class="flex items-center space-x-3 flex-1">
                        <!-- File Icon -->
                        <div class="flex-shrink-0">
                            @if(Str::contains($attachment->mime_type, 'image'))
                                <svg class="h-8 w-8 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
                                </svg>
                            @elseif(Str::contains($attachment->mime_type, 'pdf'))
                                <svg class="h-8 w-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
                                </svg>
                            @elseif(Str::contains($attachment->mime_type, 'word') || Str::contains($attachment->mime_type, 'document'))
                                <svg class="h-8 w-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
                                </svg>
                            @elseif(Str::contains($attachment->mime_type, 'spreadsheet') || Str::contains($attachment->mime_type, 'excel'))
                                <svg class="h-8 w-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
                                </svg>
                            @else
                                <svg class="h-8 w-8 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
                                </svg>
                            @endif
                        </div>
                        
                        <!-- File Info -->
                        <div class="flex-1 min-w-0">
                            <p class="text-sm font-medium text-gray-900 truncate">
                                {{ $attachment->file_name }}
                            </p>
                            <p class="text-xs text-gray-500">
                                {{ $attachment->file_size_human }} • 
                                Uploaded by {{ $attachment->user->name }} • 
                                {{ $attachment->created_at->diffForHumans() }}
                            </p>
                        </div>
                    </div>
                    
                    <!-- Actions -->
                    <div class="flex items-center gap-2 ml-4">
                        <a href="{{ route('attachments.download', [$task, $attachment]) }}" 
                           class="text-blue-600 hover:text-blue-800">
                            <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
                            </svg>
                        </a>
                        
                        @if($task->user_id === auth()->id() || $attachment->user_id === auth()->id())
                            <form method="POST" 
                                  action="{{ route('attachments.destroy', [$task, $attachment]) }}"
                                  onsubmit="return confirm('Are you sure you want to delete this file?');">
                                @csrf
                                @method('DELETE')
                                <button type="submit" class="text-red-600 hover:text-red-800">
                                    <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
                                    </svg>
                                </button>
                            </form>
                        @endif
                    </div>
                </div>
            @endforeach
        </div>
    @else
        <p class="text-gray-500 text-sm">No attachments yet.</p>
    @endif
</div>

Step 4: Building the Task Sharing System

Now let’s implement the ability to share tasks with other users.

Creating Task Share Controller

php artisan make:controller TaskShareController

Edit app/Http/Controllers/TaskShareController.php:

<?php

namespace App\Http\Controllers;

use App\Models\Task;
use App\Models\User;
use App\Models\TaskShare;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Mail\TaskShared;

class TaskShareController extends Controller
{
    /**
     * Show sharing form.
     */
    public function create(Task $task)
    {
        // Only task owner can share
        if ($task->user_id !== auth()->id()) {
            abort(403, 'Only the task owner can share this task.');
        }

        // Get users except current user and already shared users
        $sharedUserIds = $task->shares()->pluck('shared_with_user_id')->toArray();
        $sharedUserIds[] = auth()->id(); // Exclude current user
        
        $availableUsers = User::whereNotIn('id', $sharedUserIds)
            ->orderBy('name')
            ->get();
        
        $currentShares = $task->shares()->with('sharedWith')->get();

        return view('tasks.share', compact('task', 'availableUsers', 'currentShares'));
    }

    /**
     * Share task with user.
     */
    public function store(Request $request, Task $task)
    {
        // Only task owner can share
        if ($task->user_id !== auth()->id()) {
            abort(403, 'Only the task owner can share this task.');
        }

        $request->validate([
            'user_id' => ['required', 'exists:users,id', 'different:' . auth()->id()],
            'permission' => ['required', 'in:view,edit'],
        ], [
            'user_id.different' => 'You cannot share a task with yourself.',
        ]);

        // Check if already shared
        $existingShare = $task->shares()
            ->where('shared_with_user_id', $request->user_id)
            ->first();

        if ($existingShare) {
            return redirect()
                ->back()
                ->with('error', 'Task is already shared with this user.');
        }

        // Create share
        $share = $task->shares()->create([
            'shared_by_user_id' => auth()->id(),
            'shared_with_user_id' => $request->user_id,
            'permission' => $request->permission,
        ]);

        // Send email notification
        $sharedUser = User::find($request->user_id);
        try {
            Mail::to($sharedUser)->send(new TaskShared($task, $share, auth()->user()));
        } catch (\Exception $e) {
            // Log error but don't fail the share
            \Log::error('Failed to send task share email: ' . $e->getMessage());
        }

        return redirect()
            ->route('tasks.share.create', $task)
            ->with('success', "Task shared with {$sharedUser->name} successfully!");
    }

    /**
     * Update share permission.
     */
    public function update(Request $request, Task $task, TaskShare $share)
    {
        // Only task owner can update permissions
        if ($task->user_id !== auth()->id()) {
            abort(403);
        }

        // Verify share belongs to task
        if ($share->task_id !== $task->id) {
            abort(404);
        }

        $request->validate([
            'permission' => ['required', 'in:view,edit'],
        ]);

        $share->update(['permission' => $request->permission]);

        return redirect()
            ->back()
            ->with('success', 'Permission updated successfully!');
    }

    /**
     * Remove share.
     */
    public function destroy(Task $task, TaskShare $share)
    {
        // Only task owner can remove shares
        if ($task->user_id !== auth()->id()) {
            abort(403);
        }

        // Verify share belongs to task
        if ($share->task_id !== $task->id) {
            abort(404);
        }

        $userName = $share->sharedWith->name;
        $share->delete();

        return redirect()
            ->back()
            ->with('success', "Task access removed from {$userName}.");
    }

    /**
     * Show shared tasks (tasks shared with me).
     */
    public function index()
    {
        $sharedTasks = Task::whereHas('shares', function($query) {
            $query->where('shared_with_user_id', auth()->id());
        })->with(['user', 'categories', 'shares' => function($query) {
            $query->where('shared_with_user_id', auth()->id());
        }])->paginate(10);

        return view('tasks.shared', compact('sharedTasks'));
    }
}

Adding Share Routes

Add to routes/web.php:

// Task sharing routes
Route::get('/tasks/{task}/share', [TaskShareController::class, 'create'])->name('tasks.share.create');
Route::post('/tasks/{task}/share', [TaskShareController::class, 'store'])->name('tasks.share.store');
Route::put('/tasks/{task}/share/{share}', [TaskShareController::class, 'update'])->name('tasks.share.update');
Route::delete('/tasks/{task}/share/{share}', [TaskShareController::class, 'destroy'])->name('tasks.share.destroy');
Route::get('/shared-tasks', [TaskShareController::class, 'index'])->name('tasks.shared');

Step 5: Creating Share Views

Task Sharing Page

Create resources/views/tasks/share.blade.php:

<x-app-layout>
    <x-slot name="header">
        <div class="flex justify-between items-center">
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                Share Task: {{ $task->title }}
            </h2>
            <a href="{{ route('tasks.show', $task) }}" 
               class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
                Back to Task
            </a>
        </div>
    </x-slot>

    <div class="py-12">
        <div class="max-w-4xl mx-auto sm:px-6 lg:px-8 space-y-6">
            
            <!-- Share with New User -->
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6">
                    <h3 class="text-lg font-semibold mb-4">Share with User</h3>
                    
                    @if($availableUsers->count() > 0)
                        <form method="POST" action="{{ route('tasks.share.store', $task) }}" class="space-y-4">
                            @csrf
                            
                            <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
                                <!-- Select User -->
                                <div>
                                    <label for="user_id" class="block text-sm font-medium text-gray-700 mb-2">
                                        Select User
                                    </label>
                                    <select name="user_id" 
                                            id="user_id" 
                                            required
                                            class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
                                        <option value="">Choose a user...</option>
                                        @foreach($availableUsers as $user)
                                            <option value="{{ $user->id }}">
                                                {{ $user->name }} ({{ $user->email }})
                                            </option>
                                        @endforeach
                                    </select>
                                    @error('user_id')
                                        <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
                                    @enderror
                                </div>
                                
                                <!-- Permission Level -->
                                <div>
                                    <label for="permission" class="block text-sm font-medium text-gray-700 mb-2">
                                        Permission Level
                                    </label>
                                    <select name="permission" 
                                            id="permission" 
                                            required
                                            class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
                                        <option value="view">View Only - Can view task details</option>
                                        <option value="edit">Can Edit - Can edit task and add files</option>
                                    </select>
                                    @error('permission')
                                        <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
                                    @enderror
                                </div>
                            </div>
                            
                            <button type="submit" 
                                    class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
                                Share Task
                            </button>
                        </form>
                    @else
                        <div class="text-center py-8 bg-gray-50 rounded-lg">
                            <p class="text-gray-600">No more users available to share with.</p>
                            <p class="text-sm text-gray-500 mt-2">You've already shared this task with all registered users.</p>
                        </div>
                    @endif
                </div>
            </div>

            <!-- Current Shares -->
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6">
                    <h3 class="text-lg font-semibold mb-4">Currently Shared With</h3>
                    
                    @if($currentShares->count() > 0)
                        <div class="space-y-3">
                            @foreach($currentShares as $share)
                                <div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
                                    <div class="flex items-center space-x-3">
                                        <!-- User Avatar Placeholder -->
                                        <div class="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center text-white font-bold">
                                            {{ strtoupper(substr($share->sharedWith->name, 0, 1)) }}
                                        </div>
                                        
                                        <div>
                                            <p class="font-medium text-gray-900">{{ $share->sharedWith->name }}</p>
                                            <p class="text-sm text-gray-500">{{ $share->sharedWith->email }}</p>
                                        </div>
                                    </div>
                                    
                                    <div class="flex items-center gap-3">
                                        <!-- Permission Toggle -->
                                        <form method="POST" action="{{ route('tasks.share.update', [$task, $share]) }}">
                                            @csrf
                                            @method('PUT')
                                            <select name="permission" 
                                                    onchange="this.form.submit()"
                                                    class="rounded-md border-gray-300 text-sm">
                                                <option value="view" {{ $share->permission === 'view' ? 'selected' : '' }}>View Only</option>
                                                <option value="edit" {{ $share->permission === 'edit' ? 'selected' : '' }}>Can Edit</option>
                                            </select>
                                        </form>
                                        
                                        <!-- Remove Share -->
                                        <form method="POST" 
                                              action="{{ route('tasks.share.destroy', [$task, $share]) }}"
                                              onsubmit="return confirm('Remove access for {{ $share->sharedWith->name }}?');">
                                            @csrf
                                            @method('DELETE')
                                            <button type="submit" 
                                                    class="text-red-600 hover:text-red-800">
                                                <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
                                                </svg>
                                            </button>
                                        </form>
                                    </div>
                                </div>
                            @endforeach
                        </div>
                    @else
                        <div class="text-center py-8 bg-gray-50 rounded-lg">
                            <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
                            </svg>
                            <p class="mt-2 text-gray-600">Task not shared yet</p>
                            <p class="text-sm text-gray-500 mt-1">Share this task with team members to collaborate.</p>
                        </div>
                    @endif
                </div>
            </div>

            <!-- Share Information -->
            <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
                <h4 class="font-semibold text-blue-900 mb-2">About Task Sharing</h4>
                <ul class="text-sm text-blue-800 space-y-1">
                    <li>• <strong>View Only:</strong> Users can see task details and download attachments</li>
                    <li>• <strong>Can Edit:</strong> Users can edit task details, change status, and upload files</li>
                    <li>• Only you (the task owner) can delete the task or remove sharing</li>
                    <li>• Shared users will receive an email notification</li>
                </ul>
            </div>
        </div>
    </div>
</x-app-layout>

Shared Tasks Index

Create resources/views/tasks/shared.blade.php:

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Tasks Shared With Me') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6">
                    @if($sharedTasks->count() > 0)
                        <div class="space-y-4">
                            @foreach($sharedTasks as $task)
                                <div class="border rounded-lg p-4 hover:shadow-md transition">
                                    <div class="flex justify-between items-start">
                                        <div class="flex-1">
                                            <div class="flex items-center gap-2 mb-2">
                                                <h3 class="text-lg font-semibold">
                                                    <a href="{{ route('tasks.show', $task) }}" 
                                                       class="text-blue-600 hover:text-blue-800">
                                                        {{ $task->title }}
                                                    </a>
                                                </h3>
                                                
                                                <!-- Permission Badge -->
                                                @php
                                                    $userShare = $task->shares->first();
                                                @endphp
                                                <span class="px-2 py-1 text-xs rounded-full {{ $userShare->permission === 'edit' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' }}">
                                                    {{ $userShare->permission === 'edit' ? 'Can Edit' : 'View Only' }}
                                                </span>
                                            </div>
                                            
                                            @if($task->description)
                                                <p class="text-gray-600 mt-1">
                                                    {{ Str::limit($task->description, 100) }}
                                                </p>
                                            @endif
                                            
                                            <div class="flex items-center gap-4 mt-3 text-sm text-gray-500">
                                                <span class="flex items-center gap-1">
                                                    <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
                                                    </svg>
                                                    Shared by {{ $task->user->name }}
                                                </span>
                                                
                                                <x-status-badge :status="$task->status" />
                                                <x-priority-indicator :priority="$task->priority" />
                                                
                                                @if($task->due_date)
                                                    <span>Due: {{ $task->due_date->format('M d, Y') }}</span>
                                                @endif
                                            </div>
                                            
                                            <!-- Categories -->
                                            @if($task->categories->count() > 0)
                                                <div class="flex gap-2 mt-2">
                                                    @foreach($task->categories as $category)
                                                        <span class="px-2 py-1 text-xs rounded-full text-white"
                                                              style="background-color: {{ $category->color }}">
                                                            {{ $category->name }}
                                                        </span>
                                                    @endforeach
                                                </div>
                                            @endif
                                        </div>
                                    </div>
                                </div>
                            @endforeach
                        </div>
                        
                        <!-- Pagination -->
                        <div class="mt-6">
                            {{ $sharedTasks->links() }}
                        </div>
                    @else
                        <!-- Empty State -->
                        <div class="text-center py-12">
                            <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
                            </svg>
                            <h3 class="mt-2 text-sm font-medium text-gray-900">No shared tasks</h3>
                            <p class="mt-1 text-sm text-gray-500">Tasks shared with you will appear here.</p>
                        </div>
                    @endif
                </div>
            </div>
        </div>
    </div>
</x-app-layout>

Adding Share Button to Task View

Update the task show page header section in resources/views/tasks/show.blade.php:

<div class="flex justify-between items-center">
    <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        {{ __('Task Details') }}
    </h2>
    <div class="flex gap-2">
        @if($task->user_id === auth()->id())
            <a href="{{ route('tasks.share.create', $task) }}" 
               class="bg-purple-500 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded flex items-center gap-2">
                <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path>
                </svg>
                Share
            </a>
        @endif
        
        <a href="{{ route('tasks.edit', $task) }}" 
           class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
            Edit Task
        </a>
        <a href="{{ route('tasks.index') }}" 
           class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
            Back to List
        </a>
    </div>
</div>

Adding Shared Tasks to Navigation

Update resources/views/layouts/navigation.blade.php:

<x-nav-link :href="route('tasks.index')" :active="request()->routeIs('tasks.*')">
    {{ __('My Tasks') }}
</x-nav-link>

<x-nav-link :href="route('tasks.shared')" :active="request()->routeIs('tasks.shared')">
    {{ __('Shared With Me') }}
</x-nav-link>

<x-nav-link :href="route('categories.index')" :active="request()->routeIs('categories.*')">
    {{ __('Categories') }}
</x-nav-link>

Step 6: Email Notifications

Let’s set up email notifications for task sharing.

Creating Mailable Class

php artisan make:mail TaskShared

Edit app/Mail/TaskShared.php:

<?php

namespace App\Mail;

use App\Models\Task;
use App\Models\TaskShare;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class TaskShared extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(
        public Task $task,
        public TaskShare $share,
        public User $sharedBy
    ) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: "{$this->sharedBy->name} shared a task with you",
        );
    }

    public function content(): Content
    {
        return new Content(
            view: 'emails.task-shared',
        );
    }

    public function attachments(): array
    {
        return [];
    }
}

Creating Email Template

Create resources/views/emails/task-shared.blade.php:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        body {
            font-family: Arial, sans-serif;
            line-height: 1.6;
            color: #333;
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
        }
        .header {
            background-color: #3B82F6;
            color: white;
            padding: 20px;
            text-align: center;
            border-radius: 5px 5px 0 0;
        }
        .content {
            background-color: #f9fafb;
            padding: 20px;
            border: 1px solid #e5e7eb;
        }
        .task-details {
            background-color: white;
            padding: 15px;
            border-radius: 5px;
            margin: 15px 0;
        }
        .badge {
            display: inline-block;
            padding: 4px 12px;
            border-radius: 12px;
            font-size: 12px;
            font-weight: bold;
        }
        .badge-view {
            background-color: #E5E7EB;
            color: #374151;
        }
        .badge-edit {
            background-color: #D1FAE5;
            color: #065F46;
        }
        .button {
            display: inline-block;
            padding: 12px 24px;
            background-color: #3B82F6;
            color: white;
            text-decoration: none;
            border-radius: 5px;
            margin: 15px 0;
        }
        .footer {
            text-align: center;
            margin-top: 20px;
            color: #6B7280;
            font-size: 12px;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>Task Shared With You</h1>
    </div>
    
    <div class="content">
        <p>Hello!</p>
        
        <p><strong>{{ $sharedBy->name }}</strong> has shared a task with you.</p>
        
        <div class="task-details">
            <h2 style="margin-top: 0;">{{ $task->title }}</h2>
            
            @if($task->description)
                <p>{{ $task->description }}</p>
            @endif
            
            <p>
                <strong>Permission Level:</strong> 
                <span class="badge badge-{{ $share->permission }}">
                    {{ $share->permission === 'edit' ? 'Can Edit' : 'View Only' }}
                </span>
            </p>
            
            @if($task->due_date)
                <p><strong>Due Date:</strong> {{ $task->due_date->format('F j, Y') }}</p>
            @endif
            
            <p><strong>Priority:</strong> {{ ucfirst($task->priority) }}</p>
            <p><strong>Status:</strong> {{ ucfirst(str_replace('_', ' ', $task->status)) }}</p>
        </div>
        
        <center>
            <a href="{{ route('tasks.show', $task) }}" class="button">
                View Task
            </a>
        </center>
        
        <p style="margin-top: 20px; font-size: 14px; color: #6B7280;">
            @if($share->permission === 'edit')
                You can view and edit this task, change its status, and upload attachments.
            @else
                You can view this task and download any attachments.
            @endif
        </p>
    </div>
    
    <div class="footer">
        <p>This is an automated email from Task Manager.</p>
        <p>If you have questions, contact {{ $sharedBy->name }} at {{ $sharedBy->email }}</p>
    </div>
</body>
</html>

Configuring Mail Settings

Update your .env file with mail configuration:

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your_username
MAIL_PASSWORD=your_password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="noreply@taskmanager.com"
MAIL_FROM_NAME="${APP_NAME}"

For Development: Use Mailtrap for testing emails without sending real emails.

For Production: Use services like SendGrid, Mailgun, Amazon SES, or Postmark.

Step 7: Activity Tracking

Let’s add basic activity tracking to see who did what.

Creating Activity Model and Migration

php artisan make:model Activity -m

Edit the migration database/migrations/xxxx_create_activities_table.php:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('activities', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->foreignId('task_id')->constrained()->onDelete('cascade');
            $table->string('action'); // created, updated, completed, shared, etc.
            $table->text('description')->nullable();
            $table->json('changes')->nullable(); // Store what changed
            $table->timestamps();
            
            $table->index(['task_id', 'created_at']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('activities');
    }
};

Run migration:

php artisan migrate

Setting Up Activity Model

Edit app/Models/Activity.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Activity extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id',
        'task_id',
        'action',
        'description',
        'changes',
    ];

    protected $casts = [
        'changes' => 'array',
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function task(): BelongsTo
    {
        return $this->belongsTo(Task::class);
    }
}

Adding Activity Relationship to Task

Update app/Models/Task.php:

public function activities(): HasMany
{
    return $this->hasMany(Activity::class);
}

Creating Activity Trait

Create app/Traits/LogsActivity.php:

<?php

namespace App\Traits;

use App\Models\Activity;

trait LogsActivity
{
    public static function bootLogsActivity()
    {
        static::created(function ($model) {
            $model->logActivity('created', 'Task created');
        });

        static::updated(function ($model) {
            if ($model->isDirty('status')) {
                $model->logActivity('status_changed', "Status changed from {$model->getOriginal('status')} to {$model->status}");
            }
            
            if ($model->isDirty()) {
                $changes = $model->getChanges();
                unset($changes['updated_at']);
                
                if (!empty($changes)) {
                    $model->logActivity('updated', 'Task updated', $changes);
                }
            }
        });

        static::deleted(function ($model) {
            if ($model->isForceDeleting()) {
                $model->logActivity('deleted', 'Task permanently deleted');
            } else {
                $model->logActivity('deleted', 'Task deleted');
            }
        });
    }

    public function logActivity($action, $description, $changes = null)
    {
        return $this->activities()->create([
            'user_id' => auth()->id() ?? $this->user_id,
            'action' => $action,
            'description' => $description,
            'changes' => $changes,
        ]);
    }
}

Using the Trait in Task Model

Update app/Models/Task.php:

<?php

namespace App\Models;

use App\Traits\LogsActivity;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Task extends Model
{
    use HasFactory, SoftDeletes, LogsActivity;
    
    // ... rest of model code
}

Displaying Activity Log

Add activity section to task show page in resources/views/tasks/show.blade.php:

<!-- Activity Log -->
<div class="border-t pt-4 mt-4">
    <h3 class="text-sm font-medium text-gray-700 mb-3">Activity Log</h3>
    
    @if($task->activities->count() > 0)
        <div class="space-y-2">
            @foreach($task->activities()->latest()->limit(10)->get() as $activity)
                <div class="flex items-start gap-3 text-sm">
                    <div class="flex-shrink-0 w-2 h-2 mt-2 rounded-full 
                        {{ $activity->action === 'created' ? 'bg-green-500' : '' }}
                        {{ $activity->action === 'updated' ? 'bg-blue-500' : '' }}
                        {{ $activity->action === 'status_changed' ? 'bg-purple-500' : '' }}
                        {{ $activity->action === 'deleted' ? 'bg-red-500' : '' }}">
                    </div>
                    <div class="flex-1">
                        <p class="text-gray-900">
                            <strong>{{ $activity->user->name }}</strong> {{ $activity->description }}
                        </p>
                        <p class="text-gray-500 text-xs">
                            {{ $activity->created_at->diffForHumans() }}
                        </p>
                    </div>
                </div>
            @endforeach
        </div>
        
        @if($task->activities->count() > 10)
            <p class="text-xs text-gray-500 mt-3">Showing latest 10 activities</p>
        @endif
    @else
        <p class="text-sm text-gray-500">No activity yet</p>
    @endif
</div>

Step 8: Storage Optimization and File Management

Adding File Size Limits

Create a config file for uploads config/uploads.php:

<?php

return [
    'max_file_size' => env('UPLOAD_MAX_FILE_SIZE', 10240), // KB (10MB default)
    
    'allowed_mimes' => [
        'pdf',
        'doc', 'docx',
        'xls', 'xlsx',
        'ppt', 'pptx',
        'txt', 'csv',
        'jpg', 'jpeg', 'png', 'gif', 'svg',
        'zip', 'rar', '7z',
    ],
    
    'per_user_limit' => env('UPLOAD_PER_USER_LIMIT', 104857600), // bytes (100MB default)
];

Tracking User Storage Usage

Add method to User model:

// In app/Models/User.php

public function totalStorageUsed()
{
    return $this->hasMany(Attachment::class)->sum('file_size');
}

public function totalStorageUsedHuman()
{
    $bytes = $this->totalStorageUsed();
    $units = ['B', 'KB', 'MB', 'GB'];
    
    for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
        $bytes /= 1024;
    }
    
    return round($bytes, 2) . ' ' . $units[$i];
}

public function canUploadFile($fileSize)
{
    $limit = config('uploads.per_user_limit');
    return ($this->totalStorageUsed() + $fileSize) <= $limit;
}

Adding Storage Check to Upload

Update the store method in AttachmentController:

public function store(Request $request, Task $task)
{
    // ... existing authorization code ...

    $request->validate([
        'file' => [
            'required',
            'file',
            'max:' . config('uploads.max_file_size'),
            'mimes:' . implode(',', config('uploads.allowed_mimes')),
        ],
    ]);

    $file = $request->file('file');
    
    // Check storage limit
    if (!auth()->user()->canUploadFile($file->getSize())) {
        return redirect()
            ->back()
            ->with('error', 'Storage limit exceeded. Please delete some files or contact support.');
    }

    // ... rest of upload code ...
}

Creating Storage Dashboard

Create a simple storage overview page. Add to PreferenceController:

public function storage()
{
    $user = auth()->user();
    
    $stats = [
        'total_files' => $user->hasMany(Attachment::class)->count(),
        'total_size' => $user->totalStorageUsed(),
        'total_size_human' => $user->totalStorageUsedHuman(),
        'limit' => config('uploads.per_user_limit'),
        'limit_human' => $this->formatBytes(config('uploads.per_user_limit')),
        'percentage' => ($user->totalStorageUsed() / config('uploads.per_user_limit')) * 100,
    ];
    
    $recentFiles = Attachment::where('user_id', $user->id)
        ->with('task')
        ->latest()
        ->limit(10)
        ->get();
    
    $largestFiles = Attachment::where('user_id', $user->id)
        ->with('task')
        ->orderByDesc('file_size')
        ->limit(10)
        ->get();
    
    return view('preferences.storage', compact('stats', 'recentFiles', 'largestFiles'));
}

private function formatBytes($bytes)
{
    $units = ['B', 'KB', 'MB', 'GB'];
    for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
        $bytes /= 1024;
    }
    return round($bytes, 2) . ' ' . $units[$i];
}

Add route:

Route::get('/preferences/storage', [PreferenceController::class, 'storage'])->name('storage.index');

Create view resources/views/preferences/storage.blade.php:

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Storage Usage') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
            
            <!-- Storage Overview -->
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6">
                    <h3 class="text-lg font-semibold mb-4">Storage Overview</h3>
                    
                    <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
                        <div class="text-center p-4 bg-blue-50 rounded-lg">
                            <p class="text-3xl font-bold text-blue-600">{{ $stats['total_files'] }}</p>
                            <p class="text-sm text-gray-600 mt-1">Total Files</p>
                        </div>
                        
                        <div class="text-center p-4 bg-green-50 rounded-lg">
                            <p class="text-3xl font-bold text-green-600">{{ $stats['total_size_human'] }}</p>
                            <p class="text-sm text-gray-600 mt-1">Space Used</p>
                        </div>
                        
                        <div class="text-center p-4 bg-purple-50 rounded-lg">
                            <p class="text-3xl font-bold text-purple-600">{{ $stats['limit_human'] }}</p>
                            <p class="text-sm text-gray-600 mt-1">Total Limit</p>
                        </div>
                    </div>
                    
                    <!-- Progress Bar -->
                    <div>
                        <div class="flex justify-between items-center mb-2">
                            <span class="text-sm font-medium text-gray-700">Storage Usage</span>
                            <span class="text-sm font-medium text-gray-700">{{ number_format($stats['percentage'], 1) }}%</span>
                        </div>
                        <div class="w-full bg-gray-200 rounded-full h-4">
                            <div class="h-4 rounded-full {{ $stats['percentage'] > 80 ? 'bg-red-500' : ($stats['percentage'] > 50 ? 'bg-yellow-500' : 'bg-green-500') }}"
                                 style="width: {{ min($stats['percentage'], 100) }}%">
                            </div>
                        </div>
                        <p class="text-xs text-gray-500 mt-1">
                            {{ $stats['total_size_human'] }} of {{ $stats['limit_human'] }} used
                        </p>
                    </div>
                    
                    @if($stats['percentage'] > 80)
                        <div class="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
                            <p class="text-sm text-red-800">
                                ⚠️ You're running low on storage space. Consider deleting some old files.
                            </p>
                        </div>
                    @endif
                </div>
            </div>

            <!-- Recent Files -->
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6">
                    <h3 class="text-lg font-semibold mb-4">Recent Files</h3>
                    
                    @if($recentFiles->count() > 0)
                        <div class="space-y-2">
                            @foreach($recentFiles as $file)
                                <div class="flex items-center justify-between p-3 bg-gray-50 rounded">
                                    <div class="flex-1">
                                        <p class="font-medium text-sm">{{ $file->file_name }}</p>
                                        <p class="text-xs text-gray-500">
                                            Task: 
                                            <a href="{{ route('tasks.show', $file->task) }}" class="text-blue-600 hover:text-blue-800">
                                                {{ $file->task->title }}
                                            </a>
                                        </p>
                                    </div>
                                    <div class="text-right">
                                        <p class="text-sm font-medium">{{ $file->file_size_human }}</p>
                                        <p class="text-xs text-gray-500">{{ $file->created_at->diffForHumans() }}</p>
                                    </div>
                                </div>
                            @endforeach
                        </div>
                    @else
                        <p class="text-gray-500 text-sm">No files uploaded yet</p>
                    @endif
                </div>
            </div>

            <!-- Largest Files -->
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6">
                    <h3 class="text-lg font-semibold mb-4">Largest Files</h3>
                    
                    @if($largestFiles->count() > 0)
                        <div class="space-y-2">
                            @foreach($largestFiles as $file)
                                <div class="flex items-center justify-between p-3 bg-gray-50 rounded">
                                    <div class="flex-1">
                                        <p class="font-medium text-sm">{{ $file->file_name }}</p>
                                        <p class="text-xs text-gray-500">
                                            Task: 
                                            <a href="{{ route('tasks.show', $file->task) }}" class="text-blue-600 hover:text-blue-800">
                                                {{ $file->task->title }}
                                            </a>
                                        </p>
                                    </div>
                                    <div class="text-right flex items-center gap-3">
                                        <p class="text-sm font-medium text-red-600">{{ $file->file_size_human }}</p>
                                        <form method="POST" action="{{ route('attachments.destroy', [$file->task, $file]) }}"
                                              onsubmit="return confirm('Delete this file?');">
                                            @csrf
                                            @method('DELETE')
                                            <button type="submit" class="text-red-600 hover:text-red-800">
                                                <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
                                                </svg>
                                            </button>
                                        </form>
                                    </div>
                                </div>
                            @endforeach
                        </div>
                    @else
                        <p class="text-gray-500 text-sm">No files uploaded yet</p>
                    @endif
                </div>
            </div>
        </div>
    </div>
</x-app-layout>

Add link to navigation:

<x-dropdown-link :href="route('storage.index')">
    {{ __('Storage Usage') }}
</x-dropdown-link>

Step 9: Security Best Practices

File Upload Security Checklist

  1. Validate File Types:
// Always validate MIME types, not just extensions
'file' => 'required|mimes:pdf,jpg,png|max:10240'
  1. Rename Files:
// Don't use original filename directly
$filename = time() . '_' . Str::random(10) . '.' . $file->extension();
  1. Store Outside Public Directory:
// Use storage/app instead of public for sensitive files
$path = $file->store('private/attachments');
  1. Check File Size:
// Prevent denial of service
if ($file->getSize() > 10 * 1024 * 1024) {
    throw new \Exception('File too large');
}
  1. Scan for Viruses (Production):
// Use packages like ClamAV for virus scanning
// composer require xenolope/quahog
  1. Rate Limiting:
// In RouteServiceProvider or routes file
Route::middleware(['throttle:10,1'])->group(function () {
    Route::post('/tasks/{task}/attachments', ...);
});

Permission Security

// Always verify ownership or share access
public function canUserAccessTask(User $user, Task $task): bool
{
    return $task->user_id === $user->id || 
           $task->shares()->where('shared_with_user_id', $user->id)->exists();
}

public function canUserEditTask(User $user, Task $task): bool
{
    return $task->user_id === $user->id || 
           $task->shares()
                ->where('shared_with_user_id', $user->id)
                ->where('permission', 'edit')
                ->exists();
}

What We’ve Accomplished

Congratulations! You’ve built a complete collaborative task management system with:

  • ✅ File upload system with validation and security
  • ✅ File management (upload, download, delete)
  • ✅ Storage optimization and limits
  • ✅ Task sharing with permission levels
  • ✅ User collaboration features
  • ✅ Email notifications for sharing
  • ✅ Activity tracking and audit log
  • ✅ Storage usage dashboard
  • ✅ Secure file handling
  • ✅ Permission management
  • ✅ Shared tasks view
  • ✅ File type restrictions
  • ✅ Storage quotas per user

Quick Recap

Key Commands Used

# Storage
php artisan storage:link

# Controllers & Mail
php artisan make:controller AttachmentController
php artisan make:controller TaskShareController
php artisan make:mail TaskShared

# Models & Migrations
php artisan make:model Activity -m
php artisan migrate

Important Concepts Learned

  • File upload and storage in Laravel
  • Filesystem configuration and disks
  • File validation and security
  • Permission-based sharing system
  • Email notifications with Mailables
  • Activity logging and audit trails
  • Storage optimization and quotas
  • Secure file downloads
  • User collaboration patterns
  • MIME type handling

Additional Resources

Homework Challenge

Before Part 6, try these exercises:

  1. Add File Preview:
    • Show image previews inline
    • PDF preview with iframe
    • Document preview with Google Docs Viewer
  2. Advanced Sharing:
    • Share via link (public URLs)
    • Expiring share links
    • Share with groups/teams
  3. Enhanced Activity Log:
    • Filter activities by action type
    • Export activity log to CSV
    • Real-time activity updates
  4. File Organization:
    • Organize files by category
    • Add file tags
    • Search through file contents

Share your implementations in the comments!


Need Help?

Common questions:

  • Files not uploading? Check php.ini upload_max_filesize and post_max_size
  • Storage link broken? Run php artisan storage:link again
  • Emails not sending? Check .env mail configuration
  • Permission denied? Verify storage directory permissions

Leave your questions in the comments!

Need to review workflows? Go back to Part 4: Categories & Status Management


Leave a Reply

Your email address will not be published. Required fields are marked *