Posted in

Laravel 12 Tutorial for Beginners: Authentication and Database Setup (Part 2)

Laravel 12 Tutorial Part 2

Welcome back to our Laravel 12 Task Manager series! In Part 1, we set up our development environment and created our first Laravel project. Now it’s time to build the foundation of our application with user authentication and a proper database structure.

By the end of this tutorial, you’ll have a fully functional authentication system and understand how to design database tables for your Task Manager application.

What You’ll Learn in This Tutorial

  • Installing and configuring Laravel Breeze for authentication
  • Understanding authentication scaffolding
  • Creating database migrations for tasks
  • Designing relationships between users and tasks
  • Understanding Eloquent ORM and models
  • Working with migration files
  • Testing your authentication system

Prerequisites

Before starting this tutorial:

If you haven’t completed Part 1, go back and set up your environment first!

Why Authentication Matters

User authentication is crucial for any web application. It allows us to:

  • Identify individual users
  • Protect sensitive data
  • Personalize user experiences
  • Track user-specific tasks
  • Implement authorization and permissions

Laravel makes authentication incredibly simple with its built-in packages.

Understanding Laravel Breeze

Laravel offers several authentication options:

  • Laravel Breeze – Minimal, simple authentication scaffolding (we’ll use this)
  • Laravel Jetstream – More features, including teams and API support
  • Laravel Fortify – Headless authentication backend
  • Custom Authentication – Build your own from scratch

Why Breeze? It’s perfect for beginners – lightweight, easy to understand, and provides all essential features without overwhelming complexity.

Step 1: Installing Laravel Breeze

Let’s start by installing Laravel Breeze in our Task Manager project.

1.1 Install Breeze via Composer

Open your terminal, navigate to your project directory, and run:

cd task-manager
composer require laravel/breeze --dev

This command installs Breeze as a development dependency. You’ll see Composer downloading the package and its dependencies.

Expected output:

Using version ^2.x for laravel/breeze
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies
...
Package operations: 1 install, 0 updates, 0 removals

1.2 Install Breeze Scaffolding

Now let’s install the Breeze scaffolding. Breeze offers different stacks – we’ll use Blade (Laravel’s templating engine):

php artisan breeze:install blade

What just happened? Breeze installed:

  • Login and registration views
  • Controllers for authentication
  • Routes for auth endpoints
  • Tailwind CSS for styling
  • Alpine.js for interactivity

1.3 Install and Compile Frontend Dependencies

Breeze uses Tailwind CSS for styling. Let’s install and compile the assets:

npm install
npm run dev

Keep the npm run dev process running in a separate terminal window. This watches for file changes and automatically recompiles assets.

1.4 Run Migrations

Breeze includes migrations for authentication tables. Let’s run them (If not migrated automatically):

php artisan migrate

You’ll see new tables being created:

Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated:  2019_12_14_000001_create_personal_access_tokens_table (45.67ms)
Migrating: 2024_01_01_000000_create_cache_table
Migrated:  2024_01_01_000000_create_cache_table (23.45ms)
Migrating: 2024_01_01_000001_create_jobs_table
Migrated:  2024_01_01_000001_create_jobs_table (67.89ms)

Step 2: Testing Authentication

Let’s verify that authentication is working correctly.

2.1 Start Your Development Server

If not already running:

php artisan serve

2.2 Access the Application

Open your browser and visit: http://localhost:8000 or http://127.0.0.1:8000

You should now see:

  • A Register link in the top right
  • A Log in link next to it

2.3 Create Your First User

  1. Click Register
  2. Fill in the registration form:
    • Name: Your Name
    • Email: your.email@example.com
    • Password: password123 (minimum 8 characters)
    • Confirm Password: password123
  3. Click Register

You should be automatically logged in and redirected to the dashboard!

2.4 Explore Authentication Features

Laravel Breeze provides these pages out of the box:

  • Dashboard (/dashboard) – Protected route, only accessible when logged in
  • Profile (/profile) – Update user information and password
  • Login (/login) – Sign in existing users
  • Register (/register) – Create new accounts
  • Forgot Password – Password reset functionality
  • Logout – Sign out functionality

Try navigating to these pages and testing the functionality.

Step 3: Understanding the Authentication Flow

Let’s understand what’s happening behind the scenes.

Authentication Routes

Open routes/auth.php:

<?php

use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\RegisteredUserController;
// ... other imports

Route::middleware('guest')->group(function () {
    Route::get('register', [RegisteredUserController::class, 'create'])
        ->name('register');

    Route::post('register', [RegisteredUserController::class, 'store']);

    Route::get('login', [AuthenticatedSessionController::class, 'create'])
        ->name('login');

    Route::post('login', [AuthenticatedSessionController::class, 'store']);
    
    // ... more routes
});

Route::middleware('auth')->group(function () {
    Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
        ->name('logout');
    
    // ... more routes
});

Key concepts:

  • middleware('guest') – Routes only accessible to non-authenticated users
  • middleware('auth') – Routes only accessible to authenticated users
  • Controllers handle the logic for each action

User Model

Open app/Models/User.php:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }
}

Understanding the code:

  • $fillable – Columns that can be mass-assigned
  • $hidden – Attributes hidden from JSON responses
  • casts() – Automatically cast attributes to specific types
  • password is automatically hashed when set

Step 4: Database Planning for Task Manager

Before creating migrations, let’s plan our database structure.

Tables We Need

1. users (already created by Breeze)

  • id
  • name
  • email
  • password
  • email_verified_at
  • remember_token
  • created_at
  • updated_at

2. tasks (we’ll create this)

  • id
  • user_id (who created the task)
  • title
  • description
  • priority (low, medium, high)
  • status (pending, in_progress, completed, archived)
  • due_date
  • created_at
  • updated_at

3. categories (for organizing tasks)

  • id
  • user_id
  • name
  • color
  • created_at
  • updated_at

4. task_category (pivot table for many-to-many relationship)

  • task_id
  • category_id

5. task_shares (for sharing tasks between users)

  • id
  • task_id
  • shared_by_user_id
  • shared_with_user_id
  • permission (view, edit)
  • created_at
  • updated_at

6. attachments (for file uploads)

  • id
  • task_id
  • user_id
  • file_name
  • file_path
  • file_size
  • mime_type
  • created_at
  • updated_at

Database Relationships Diagram

users
  |
  |-- has many --> tasks
  |-- has many --> categories
  |-- has many --> task_shares (as sharer)
  |-- has many --> task_shares (as recipient)
  
tasks
  |-- belongs to --> user
  |-- belongs to many --> categories
  |-- has many --> attachments
  |-- has many --> task_shares

categories
  |-- belongs to --> user
  |-- belongs to many --> tasks

attachments
  |-- belongs to --> task
  |-- belongs to --> user

Step 5: Creating the Tasks Migration

Let’s create our first custom migration for the tasks table.

5.1 Generate Migration File

php artisan make:migration create_tasks_table

Output:

Created Migration: 2026_01_18_123456_create_tasks_table

This creates a new file in database/migrations/ with a timestamp prefix.

5.2 Edit the Migration File

Open the newly created file database/migrations/2026_01_18_123456_create_tasks_table.php:

<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->string('title');
            $table->text('description')->nullable();
            $table->enum('priority', ['low', 'medium', 'high'])->default('medium');
            $table->enum('status', ['pending', 'in_progress', 'completed', 'archived'])->default('pending');
            $table->date('due_date')->nullable();
            $table->timestamps();
            
            // Indexes for better query performance
            $table->index('user_id');
            $table->index('status');
            $table->index('due_date');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('tasks');
    }
};

Understanding the migration:

  • id() – Auto-incrementing primary key
  • foreignId('user_id')->constrained() – Creates a foreign key to users table
  • onDelete('cascade') – Delete tasks when user is deleted
  • string('title') – VARCHAR column for task title
  • text('description')->nullable() – TEXT column, optional
  • enum() – Restricts values to specific options
  • default() – Sets default value
  • date('due_date')->nullable() – Optional due date
  • timestamps() – Creates created_at and updated_at columns
  • index() – Improves query performance on frequently searched columns

Step 6: Creating the Categories Migration

php artisan make:migration create_categories_table

Edit the generated file:

<?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('categories', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->string('name');
            $table->string('color', 7)->default('#3B82F6'); // Hex color code
            $table->timestamps();
            
            // Ensure unique category names per user
            $table->unique(['user_id', 'name']);
            $table->index('user_id');
        });
    }

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

New concepts:

  • string('color', 7) – VARCHAR with max length 7 (for #RRGGBB)
  • unique(['user_id', 'name']) – Composite unique constraint (users can’t have duplicate category names)

Step 7: Creating the Pivot Table Migration

php artisan make:migration create_category_task_table

Edit the file:

<?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('category_task', function (Blueprint $table) {
            $table->id();
            $table->foreignId('category_id')->constrained()->onDelete('cascade');
            $table->foreignId('task_id')->constrained()->onDelete('cascade');
            $table->timestamps();
            
            // Prevent duplicate assignments
            $table->unique(['category_id', 'task_id']);
        });
    }

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

Pivot table naming convention: Laravel expects pivot tables to be named with both table names in alphabetical order, singular form, separated by underscore: category_task.

Step 8: Creating Task Shares Migration

php artisan make:migration create_task_shares_table

Edit the file:

<?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('task_shares', function (Blueprint $table) {
            $table->id();
            $table->foreignId('task_id')->constrained()->onDelete('cascade');
            $table->foreignId('shared_by_user_id')->constrained('users')->onDelete('cascade');
            $table->foreignId('shared_with_user_id')->constrained('users')->onDelete('cascade');
            $table->enum('permission', ['view', 'edit'])->default('view');
            $table->timestamps();
            
            // Prevent duplicate shares
            $table->unique(['task_id', 'shared_with_user_id']);
            $table->index('shared_with_user_id');
        });
    }

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

Note: constrained('users') explicitly specifies the related table when the column name doesn’t follow the convention.

Step 9: Creating Attachments Migration

php artisan make:migration create_attachments_table

Edit the file:

<?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('attachments', function (Blueprint $table) {
            $table->id();
            $table->foreignId('task_id')->constrained()->onDelete('cascade');
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->string('file_name');
            $table->string('file_path');
            $table->unsignedBigInteger('file_size'); // in bytes
            $table->string('mime_type');
            $table->timestamps();
            
            $table->index('task_id');
        });
    }

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

Step 10: Running All Migrations

Now let’s create all these tables in our database:

php artisan migrate

Expected output:

Verify Tables in Database

You can verify that the tables were created:

Using Tinker:

php artisan tinker

Then run:

DB::select('SHOW TABLES');

Using phpMyAdmin: Visit http://localhost/phpmyadmin and check your task_manager database.

Step 11: Creating Eloquent Models

Models represent database tables in your application. Let’s create models for our new tables.

11.1 Create Task Model

php artisan make:model Task

Edit app/Models/Task.php:

<?php

namespace App\Models;

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

class Task extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id',
        'title',
        'description',
        'priority',
        'status',
        'due_date',
    ];

    protected $casts = [
        'due_date' => 'date',
    ];

    /**
     * Get the user that owns the task.
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    /**
     * Get the categories for the task.
     */
    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Category::class)->withTimestamps();
    }

    /**
     * Get the attachments for the task.
     */
    public function attachments(): HasMany
    {
        return $this->hasMany(Attachment::class);
    }

    /**
     * Get the shares for the task.
     */
    public function shares(): HasMany
    {
        return $this->hasMany(TaskShare::class);
    }

    /**
     * Scope a query to only include tasks of a given status.
     */
    public function scopeStatus($query, $status)
    {
        return $query->where('status', $status);
    }

    /**
     * Scope a query to only include overdue tasks.
     */
    public function scopeOverdue($query)
    {
        return $query->where('due_date', '<', now())
                     ->where('status', '!=', 'completed');
    }
}

Understanding the Task model:

  • $fillable – Mass-assignable attributes
  • $casts – Automatically converts due_date to Carbon instance
  • user() – Defines relationship: task belongs to one user
  • categories() – Many-to-many relationship with categories
  • withTimestamps() – Maintains created_at/updated_at in pivot table
  • scopeStatus() – Query scope for filtering by status
  • scopeOverdue() – Query scope for overdue tasks

[Screenshot placeholder: Task model code in editor]

11.2 Create Category Model

php artisan make:model Category

Edit app/Models/Category.php:

<?php

namespace App\Models;

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

class Category extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id',
        'name',
        'color',
    ];

    /**
     * Get the user that owns the category.
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    /**
     * Get the tasks for the category.
     */
    public function tasks(): BelongsToMany
    {
        return $this->belongsToMany(Task::class)->withTimestamps();
    }
}

11.3 Create TaskShare Model

php artisan make:model TaskShare

Edit app/Models/TaskShare.php:

<?php

namespace App\Models;

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

class TaskShare extends Model
{
    use HasFactory;

    protected $fillable = [
        'task_id',
        'shared_by_user_id',
        'shared_with_user_id',
        'permission',
    ];

    /**
     * Get the task that is shared.
     */
    public function task(): BelongsTo
    {
        return $this->belongsTo(Task::class);
    }

    /**
     * Get the user who shared the task.
     */
    public function sharedBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'shared_by_user_id');
    }

    /**
     * Get the user with whom the task is shared.
     */
    public function sharedWith(): BelongsTo
    {
        return $this->belongsTo(User::class, 'shared_with_user_id');
    }
}

11.4 Create Attachment Model

php artisan make:model Attachment

Edit app/Models/Attachment.php:

<?php

namespace App\Models;

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

class Attachment extends Model
{
    use HasFactory;

    protected $fillable = [
        'task_id',
        'user_id',
        'file_name',
        'file_path',
        'file_size',
        'mime_type',
    ];

    /**
     * Get the task that owns the attachment.
     */
    public function task(): BelongsTo
    {
        return $this->belongsTo(Task::class);
    }

    /**
     * Get the user that uploaded the attachment.
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    /**
     * Get human-readable file size.
     */
    public function getFileSizeHumanAttribute(): string
    {
        $bytes = $this->file_size;
        $units = ['B', 'KB', 'MB', 'GB'];
        
        for ($i = 0; $bytes > 1024; $i++) {
            $bytes /= 1024;
        }
        
        return round($bytes, 2) . ' ' . $units[$i];
    }
}

New concept: getFileSizeHumanAttribute() is an accessor – you can access it as $attachment->file_size_human.

11.5 Update User Model

Edit app/Models/User.php to add relationships:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Authenticatable
{
    // ... existing code ...

    /**
     * Get the tasks for the user.
     */
    public function tasks(): HasMany
    {
        return $this->hasMany(Task::class);
    }

    /**
     * Get the categories for the user.
     */
    public function categories(): HasMany
    {
        return $this->hasMany(Category::class);
    }

    /**
     * Get tasks shared by this user.
     */
    public function sharedTasks(): HasMany
    {
        return $this->hasMany(TaskShare::class, 'shared_by_user_id');
    }

    /**
     * Get tasks shared with this user.
     */
    public function receivedShares(): HasMany
    {
        return $this->hasMany(TaskShare::class, 'shared_with_user_id');
    }
}

Step 12: Testing Models with Tinker

Let’s test our models and relationships using Laravel Tinker.
First run composer dump-autoload

php artisan tinker

Create a Test Category

$user = User::first(); // Get the first user

$category = new Category();
$category->user_id = $user->id;
$category->name = 'Work';
$category->color = '#EF4444';
$category->save();

// Or use mass assignment
Category::create([
    'user_id' => $user->id,
    'name' => 'Personal',
    'color' => '#10B981'
]);

Create a Test Task

$task = Task::create([
    'user_id' => $user->id,
    'title' => 'Complete Laravel Tutorial',
    'description' => 'Finish Part 2 of the Laravel series',
    'priority' => 'high',
    'status' => 'in_progress',
    'due_date' => now()->addDays(7)
]);

Test Relationships

// Get user's tasks
$user->tasks;

// Get task's owner
$task->user;

// Attach categories to task
$task->categories()->attach([1, 2]); // Category IDs

// Get task's categories
$task->categories;

// Detach a category
$task->categories()->detach(1);

Query Examples

// Get all pending tasks
Task::status('pending')->get();

// Get overdue tasks
Task::overdue()->get();

// Get tasks with specific category
Category::find(1)->tasks;

// Get user's completed tasks
User::first()->tasks()->where('status', 'completed')->get();

Exit Tinker:

exit

Step 13: Understanding Eloquent Relationships

Let’s dive deeper into the relationships we’ve defined.

One-to-Many Relationships

User has many Tasks:

// In User model
public function tasks(): HasMany
{
    return $this->hasMany(Task::class);
}

// Usage
$user = User::find(1);
$tasks = $user->tasks; // Get all tasks for this user

Task belongs to User:

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

// Usage
$task = Task::find(1);
$owner = $task->user; // Get the owner of this task

Many-to-Many Relationships

Task belongs to many Categories:

// In Task model
public function categories(): BelongsToMany
{
    return $this->belongsToMany(Category::class)->withTimestamps();
}

// Usage
$task = Task::find(1);

// Attach categories
$task->categories()->attach([1, 2, 3]);

// Sync categories (replace all)
$task->categories()->sync([1, 2]);

// Detach specific categories
$task->categories()->detach([1]);

// Detach all
$task->categories()->detach();

Querying Relationships

// Eager loading (prevents N+1 problem)
$tasks = Task::with(['user', 'categories', 'attachments'])->get();

// Conditional eager loading
$tasks = Task::with(['categories' => function ($query) {
    $query->where('name', 'Work');
}])->get();

// Check relationship existence
$tasksWithCategories = Task::has('categories')->get();
$tasksWithAtLeast3Categories = Task::has('categories', '>=', 3)->get();

// Where has (filter by relationship)
$workTasks = Task::whereHas('categories', function ($query) {
    $query->where('name', 'Work');
})->get();

Step 14: Database Seeding (Optional)

Seeders help populate your database with test data.

Create a Seeder

php artisan make:seeder CategorySeeder

Edit database/seeders/CategorySeeder.php:

<?php

namespace Database\Seeders;

use App\Models\User;
use App\Models\Category;
use Illuminate\Database\Seeder;

class CategorySeeder extends Seeder
{
    public function run(): void
    {
        $user = User::first();

        $categories = [
            ['name' => 'Work', 'color' => '#EF4444'],
            ['name' => 'Personal', 'color' => '#10B981'],
            ['name' => 'Urgent', 'color' => '#F59E0B'],
            ['name' => 'Shopping', 'color' => '#8B5CF6'],
            ['name' => 'Health', 'color' => '#EC4899'],
        ];

        foreach ($categories as $category) {
            Category::create([
                'user_id' => $user->id,
                'name' => $category['name'],
                'color' => $category['color'],
            ]);
        }
    }
}

Run the Seeder

php artisan db:seed --class=CategorySeeder

Step 15: Migration Best Practices

Rolling Back Migrations

If you make a mistake:

# Rollback last batch
php artisan migrate:rollback

# Rollback all migrations
php artisan migrate:reset

# Rollback and re-run all migrations
php artisan migrate:refresh

# Rollback, re-run, and seed
php artisan migrate:refresh --seed

Checking Migration Status

php artisan migrate:status

Creating Indexes

Always add indexes to columns frequently used in WHERE clauses:

$table->index('user_id');
$table->index(['status', 'due_date']); // Composite index

Foreign Key Constraints

Always use proper foreign key constraints:

// Good - with constraint
$table->foreignId('user_id')->constrained()->onDelete('cascade');

// Not recommended - without constraint
$table->unsignedBigInteger('user_id');

Common Migration Errors and Solutions

Error 1: “SQLSTATE[42S01]: Base table or view already exists.”

Cause: Table already exists in the database

Solution:

php artisan migrate:rollback
php artisan migrate

Error 2: “SQLSTATE[23000]: Integrity constraint violation”

Cause: Foreign key constraint fails (referenced table doesn’t exist)

Solution: Ensure migrations run in the correct order. The table being referenced must be created first.

Error 3: “Syntax error or access violation: 1071 Specified key was too long”

Cause: String column too long for index (common with older MySQL versions)

Solution: In app/Providers/AppServiceProvider.php:

use Illuminate\Support\Facades\Schema;

public function boot(): void
{
    Schema::defaultStringLength(191);
}

Testing Your Authentication System

Let’s perform comprehensive testing of our authentication.

Test 1: Registration

  1. Visit /register
  2. Create account with:
    • Name: Test User
    • Email: test@example.com
    • Password: password123

Expected: Redirected to dashboard, logged in automatically

Test 2: Logout and Login

  1. Click logout
  2. Visit /login
  3. Enter credentials
  4. Click “Log in”

Expected: Redirected to dashboard

Test 3: Password Reset

  1. Logout
  2. Click “Forgot password?”
  3. Enter your email
  4. Check the terminal running npm run dev for email output (in local development)

Test 4: Protected Routes

  1. Logout
  2. Try to visit /dashboard directly

Expected: Redirected to login page

Test 5: Profile Update

  1. Login
  2. Visit /profile
  3. Update your name
  4. Click “Save”

Expected: Profile updated successfully

Understanding Middleware

Middleware provides a convenient mechanism for filtering HTTP requests.

Auth Middleware

Open routes/web.php:

Route::middleware('auth')->group(function () {
    Route::get('/dashboard', function () {
        return view('dashboard');
    })->name('dashboard');
});

Routes inside middleware('auth') require authentication.

Guest Middleware

Route::middleware('guest')->group(function () {
    Route::get('/login', [LoginController::class, 'index']);
});

Routes inside middleware('guest') are only accessible to non-authenticated users.

Creating Custom Middleware

php artisan make:middleware CheckTaskOwnership

We’ll implement this in Part 3!

Security Best Practices

1. Never Store Plain Passwords

Laravel automatically hashes passwords using bcrypt:

// Automatically hashed
User::create([
    'password' => 'secret123'
]);

// Manual hashing (if needed)
$hashedPassword = Hash::make('secret123');

2. Use Mass Assignment Protection

Always define $fillable or $guarded:

// Good - explicitly define fillable fields
protected $fillable = ['title', 'description'];

// Alternative - guard sensitive fields
protected $guarded = ['is_admin'];

3. CSRF Protection

Laravel includes CSRF protection. Always use @csrf in forms:

<form method="POST" action="/tasks">
    @csrf
    <!-- form fields -->
</form>

4. SQL Injection Prevention

Eloquent automatically protects against SQL injection:

// Safe - parameterized query
Task::where('user_id', $userId)->get();

// Dangerous - never do this
DB::select("SELECT * FROM tasks WHERE user_id = " . $userId);

What We’ve Accomplished

Congratulations! You’ve built a solid foundation for your Task Manager application:

  • Installed and configured Laravel Breeze authentication
  • Created a complete authentication system (register, login, logout, password reset)
  • Designed a comprehensive database structure for task management
  • Created migrations for all necessary tables
  • Built Eloquent models with proper relationships
  • Tested authentication and database functionality
  • Learned about Eloquent relationships and querying
  • Implemented security best practices

What’s Next?

In Part 3: Building CRUD Operations, we’ll:

  • Create controllers for managing tasks
  • Build forms for creating and editing tasks
  • Implement validation
  • Create views with Blade templates
  • Add pagination and filtering
  • Implement search functionality

Quick Recap

Commands We Used

# Authentication
composer require laravel/breeze --dev
php artisan breeze:install blade
npm install && npm run dev

# Migrations
php artisan make:migration create_tasks_table
php artisan migrate
php artisan migrate:rollback
php artisan migrate:status

# Models
php artisan make:model Task
php artisan make:model Category

# Testing
php artisan tinker

Key Concepts Learned

  • Authentication flow and middleware
  • Database design and relationships
  • Migration file structure
  • Eloquent ORM and models
  • Query scopes and accessors
  • Relationship types (one-to-many, many-to-many)
  • Eager loading and N+1 problem
  • Security best practices

Additional Resources

Homework Challenge

Before moving to Part 3, try these exercises:

  1. Create a Task via Tinker:
    • Use Tinker to create 5 tasks with different priorities and statuses
  2. Test Relationships:
    • Create categories and attach them to tasks
    • Query tasks that belong to specific categories
  3. Create a Seeder:
    • Build a TaskSeeder that creates 20 sample tasks
  4. Explore Accessors:
    • Add an accessor to the Task model that returns a formatted due date

Share your progress in the comments! I’d love to see what you’ve built.

Drop your questions in the comments, and I’ll help you out!

Missed Part 1? Go back to Part 1: Environment Setup


Leave a Reply

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